拖了好久终于开始作实验4了。lab4有三个大任务1. Lock Manager、2. DEADLOCK DETECTION 、3. CONCURRENT QUERY EXECUTION。这里20年的lab好像和以前的不太同样记得以前有日志和错误恢复lab的。不过就作这个最新的了。html
这个任务只须要修改两个文件concurrency/lock_manager.cpp
和concurrency/lock_manager.h
。这里cmu已经给咱们提供了和事物相关的一些函数。在include/concurrency/transaction.h
。这里的==锁管理==(下用LM表示)针对于tuple级别。咱们须要对lock/unlock
请求做出正确的行为。若是出现错误应该抛出异常。node
1. 请仔细阅读位于lock_manager.h内的LockRequestQueue类
这会帮助你肯定哪些事物在等待一个锁🔒c++
2. 建议使用std::condition_variable
来通知那些等待锁的事物git
3. 使用shared_lock_set_
和exclusive_lock_set_
来区别共享锁和排他锁。这样当TransactionManager
想要提交和abort事物的时候。LM就能够合理的释放锁github
4. 你应该通读TransactionManager::Abort
来了解对于Abort
状态的事物。LM是如何释放锁的web
==一些参考阅读资料==算法
一、两个经常使用的互斥对象:std::mutex(互斥对象),std::shared_mutex(读写互斥对象) 二、三个用于代替互斥对象的成员函数,管理互斥对象的锁(都是构造加锁,析构解锁):std::lock_guard用于管理std::mutex,std::unique_lock与std::shared_lock管理std::shared_mutex。
X锁(排他锁,Exclusive Locks)
当加X锁的时候,表示咱们要写这个tuple。当一个事物拥有排他锁时,其余任何事务必须等到X锁被释放才能对该页进行访问;X锁一直到事务结束才能被释放。下面来看一个例子
T1: update table set column1='hello' where id<1000
T2: update table set column1='world' where id>1000
对于这个sql语句而言。加入事物T1先到达。这个过程T1会对id < 1000的记录施加排他锁,可是因为T2的更新和T1并没有关系,因此它不会阻塞T2的更新
一样看下面的例子。
T1: update table set column1='hello' where id<1000
T2: update table set column1='world' where id>900
如同上例,若是T1先达,T2马上也到,T1加的排他锁会阻塞T2的update.
对于本实验的实现。咱们须要记住。
若是有事物对当前rid加了共享锁。则必须等共享锁释放以后才能再加X锁 若是有事物对当前rid加了X锁以后,则在该X锁释放以前,不会有任何的锁被施加
S锁(共享锁,Shared Locks)
多个事务可封锁一个共享页;任何事务都不能修改该页; 一般是该页被读取完毕,S锁当即被释放。
本实验对于S锁的实现要和U锁结合起来。所以会在下面说明。
U锁(更新锁,Updated Locks)
为了解决死锁。引入了更新锁,看下面的例子
----------------------------------------
T1:
begin tran
select * from table(updlock) (加更新锁)
update table set column1='hello'
T2:
begin tran
select * from table(updlock)
update table set column1='world'
----------------------------------------
事物T1加更新锁的意思就是。我如今虽然只想读。可是我要预留一个写的名额,所以当有事物施加U锁以后,其余事物便不能加U锁。好比本例,T1执行select,加更新锁。T2运行,准备加更新锁,但发现已经有一个更新锁在那儿了,只好等。
除此以外,更新锁能够和读操做共存这也是咱们这个实验实现时须要重点考虑的
----------------------------------------
T1: select * from table(updlock) (加更新锁)
T2: select * from table(updlock) (等待,直到T1释放更新锁,由于同一时间不能在同一资源上有两个更新锁)
T3: select * from table (加共享锁,但不用等updlock释放,就能够读)
----------------------------------------
所以在咱们这个实验实现的时候咱们须要注意
若是有事物对当前rid加了更新锁。则不容许加X和S锁 当被读取的页将要被更新时,则升级为X锁;U锁要一直到事务结束时才能被释放。
注:不会附上不少代码。(听Andy教授的话)
1. 对于S锁
只须要考虑下面这些状况
Lock_queue
中有X锁则须要wait
简单附上一些代码
if (mode == LockMode::SHARED) {
auto shared_wait_for = [&]() { return !lock_queue.upgrading_ && !lock_queue.hasExclusiveLock(txn); };
while (!shared_wait_for()) {
lock_queue.cv_.wait(_lock);
}
txn->GetSharedLockSet()->emplace(rid);
}
lock_queue.hasExclusiveLock(txn)
函数
就是用来判断是否有排他锁
inline bool hasExclusiveLock(Transaction *txn) {
std::list<LockRequest>::iterator curr = request_queue_.begin();
for (; curr != request_queue_.end(); curr++) {
if (curr->lock_mode_ == LockMode::EXCLUSIVE) {
return true;
}
}
return false;
}
2. 对于X锁
一样附上一些简单的代码
if (mode == LockMode::EXCLUSIVE) {
auto exclusive_wait_for = [&]() { return !lock_queue.upgrading_ && lock_queue.request_queue_.size() == 1; };
while (!exclusive_wait_for()) {
lock_queue.cv_.wait(_lock);
}
txn->GetExclusiveLockSet()->emplace(rid);
}
和共享锁的实现基本相似
附上带注释的核心逻辑代码。基本能够说明白这个地方
// 标记位更新。代表如今正处于等待update锁阶段
queue.upgrading_ = true;
// 假如说当前的request_queue中只有当前update lock这一个请求。则能够加U锁,不然应该wait
while (lock_table_[rid].request_queue_.size() != 1) {
queue.cv_.wait(unique_lock);
}
// 加X锁。并把标记位重制
queue.request_queue_.back() = LockRequest(txn->GetTransactionId(), LockMode::EXCLUSIVE);
queue.upgrading_ = false;
这个任务要求你的LM可以进行死锁检测。死锁检测算法就是最多见的资源分配图算法
下面用cmu课上ppt的例子来看一下这个算法。
核心就是若是事物Ti在等待事物Tj释放锁。则画一条从i-->j的边。若是检测完全部的冲突事务。若是出现环则表示出现了死锁。若是没有环则表示没有死锁。
这样造成了一个环就发生了死锁,这个时候就须要abort
就直接附上代码不说废话
void LockManager::AddEdge(txn_id_t t1, txn_id_t t2) {
for (const auto &txn_id : waits_for_[t1]) {
if (txn_id == t2) {
return;
}
}
waits_for_[t1].push_back(t2);
}
void LockManager::RemoveEdge(txn_id_t t1, txn_id_t t2) {
LOG_DEBUG("we can remove edge");
auto &vec = waits_for_[t1];
for (auto iter = vec.begin(); iter != vec.end(); ++iter) {
if (*iter == t2) {
vec.erase(iter);
return;
}
}
}
这个函数的实现。就是对于咱们上面算法的实现。因为依赖图是一个有向图。所以咱们须要知道如何判断一个有向图是否有环。
用简单的dfs就能够实现这一功能。固然除了一个visited
数组来判断这个元素是否被遍历过以外。咱们还须要另一个数组recStack
用来 keep track of vertices in the recursion stack.
具体的过程就和下图同样
下面附上上面那个算法的代码实习。可是关于本任务须要的代码没有给出
// This function is a variation of DFSUtil() in https://www.geeksforgeeks.org/archives/18212
bool Graph::isCyclicUtil(int v, bool visited[], bool *recStack)
{
if (visited[v] == false)
{
// Mark the current node as visited and part of recursion stack
visited[v] = true;
recStack[v] = true;
// Recur for all the vertices adjacent to this vertex
list<int>::iterator i;
for(i = adj[v].begin(); i != adj[v].end(); ++i)
{
if( !visited[*i] && isCyclicUtil(*i, visited, recStack) )
return true;
else if (recStack[*i])
return true;
}
}
recStack[v] = false; // remove the vertex from recursion stack
return false;
}
由于构建图以前咱们要获取全部已经加锁和等待锁的事物id。
这里用两个函数来实现这两个步骤
这里附上找到全部等待锁事物的函数。另外一个再也不给出。
std::unordered_set<txn_id_t> getWaitingSet() {
std::list<LockRequest>::iterator wait_start;
std::unordered_set<txn_id_t> blocking;
// 遍历找到wait_start的位置
std::list<LockRequest>::iterator curr = request_queue_.begin();
for (; curr != request_queue_.end() && (curr->lock_mode_ == LockMode::SHARED || curr ->lock_mode_ == LockMode::EXCLUSIVE); curr++) {
}
wait_start = curr;
for (; wait_start != request_queue_.end(); wait_start++) {
if (GetTransaction(wait_start->txn_id_)->GetState() != TransactionState::ABORTED) {
blocking.insert(wait_start->txn_id_);
}
}
return blocking;
}
};
Read Uncommitted(读取未提交内容)
在该隔离级别,全部事务均可以看到其余未提交事务的执行结果。本隔离级别不多用于实际应用,由于它的性能也不比其余级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
就比如还没肯定的消息,你却先知道了发布出去,最后又变动了,这样就发生了错误
Read Committed(读取提交内容)
这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它知足了隔离的简单定义:一个事务只能看见已经提交事务所作的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read
),由于同一事务的其余实例在该实例处理其间可能会有新的commit,因此同一select可能返回不一样结果。
Repeatable Read(可重读)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到一样的数据行。不过理论上,这会致使另外一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另外一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。
因为对于整个表的遍历。也就是读操做,在并发的状况下可能发生错误。因此必须加以控制
这里附上一些加锁的代码进行解释
首先是对于隔离级别的区分
READ_UNCOMMITTED
则不须要加锁
READ_COMMITTED
或者
REPEATABLE_READ
则须要判断。若是当前rid没有被加锁。则加上共享锁
Transaction *txn = exec_ctx_->GetTransaction();
LockManager *lock_mgr = exec_ctx_->GetLockManager();
if (lock_mgr != nullptr) {
switch (txn->GetIsolationLevel()) {
case IsolationLevel::READ_UNCOMMITTED:
break;
case IsolationLevel::READ_COMMITTED:
case IsolationLevel::REPEATABLE_READ:
RID r = iter->GetRid();
if (txn->GetSharedLockSet()->empty() && txn->GetExclusiveLockSet()->empty()) {
lock_mgr->LockShared(txn, r);
txn->GetSharedLockSet()->insert(r);
}
break;
}
}
这个更新比较简单。
主要有下面两个原则
update
的时候须要把它更新成update Lock
if (lock_mgr != nullptr) {
if (txn->IsSharedLocked(*rid)) {
lock_mgr->LockUpgrade(txn, *rid);
txn->GetSharedLockSet()->erase(*rid);
txn->GetExclusiveLockSet()->insert(*rid);
} else if (txn->GetExclusiveLockSet()->empty()) {
lock_mgr->LockExclusive(txn, *rid);
}
}
对于delete的并发控制和update的彻底同样。只须要加入上面的原则便可
总算磕磕绊绊把四个lab都写完了。感谢在知乎和github
以及qq群里面获得的各类帮助。后面准备配合这门课的要求把对应章节的书的内容读一下。同时把以前没有写完的上课笔记写完。顺带有时间把前面lab的博客改一下。由于实现方面有了变化。后面就准备开搞下一个lab了。不知道你们是推荐824分布式仍是推荐os那。