0 介绍
2021年秋季15445的第四个project是并发控制相关的内容,主要是实现一个LockManager。这个LockManager可以依据事务需要进行tuple级别的琐管理,总的来说还是比较考验多线程编程的基本功,希望读者在做之前理解mutex、condition_variable的工作原理以便调试bug,这些内容在我之前的博客做过总结,同时对数据库系统概念这本书里的相关章节进行阅读。下面介绍一下具体实现方案,本文依然不会提供源码。同时推荐这篇知乎文章,我做的时候收到不少启发,写得也比我详细。
1 LockManager
LockManager是这个lab的核心内容,也是后面两个内容的基础,我们主要考虑实现四个函数。
bool LockShared(Transaction *txn, const RID &rid); //加shared lock
bool LockExclusive(Transaction *txn, const RID &rid); //加exclusive lock
bool LockUpgrade(Transaction *txn, const RID &rid); //锁升级
bool Unlock(Transaction *txn, const RID &rid); // 解锁
在实现之前,我为LockRequestQueue添加了一个成员如下:
class LockRequestQueue {
public:
std::list<LockRequest> request_queue_;
// for notifying blocked transactions on this rid
std::condition_variable cv_;
// txn_id of an upgrading transaction (if any)
txn_id_t upgrading_ = INVALID_TXN_ID;
std::mutex queue_latch_; //管理请求
};
queue_latch可以用于同一tuple不同事务之间的锁管理。
LockShared的实现思路为:对于事务txn,我们首先做一些判断,如果隔离级别为READ_UNCOMMITTED,则抛异常出来;如果txn已经是aborted或者shrink,也不能授予锁;下面涉及lock_table_的读写,注意合理使用latch_:我们检查一下lock_table_里rid对应的request_queue_是否为空,若为空,则可授予锁,在request_queue_中加入对应lock_request元素;否则就要进行判断,在适当的时候赋予锁:
while (txn不是aborted状态 && 不满足得到锁的条件) {
lock_request_queue_.cv_.wait(queue_latch_);
}
那么如何判断得到锁的条件呢,需要对当前request之前的request判断,只有之前的request都是sharedlock请求且都授予了锁,当前锁才能授予。授予锁时修改request的granted标致和txn的SharedLockSet即可。
LockExclusive的实现和前者大同小异,有所区别的是仅有当前request是request_queue的第一个元素才能赋予锁。
LockUpgrade是将一个shared锁升级为exclusive锁,我们首先找到txn持有rid的shared锁(找不到就报错),将读锁从request_queue_中删去,然后将exclusive锁放入request_queue_第一个未授予锁的request之前,接下来判断是否可以授予锁即可,同LockExclusive一致。
Unlock操作是解锁,只需将插入的request从request_queue从队列里删除即可,然后使用
lock_request_queue_.cv_.notify_all()通知其他线程获取锁。顺便注意隔离级别不同,某些操作不太一样,REPEATABLE_READ在解锁结束将txn状态设为shrink,READ_COMMITTED仅在写锁解锁结束将txn状态设为shrink,最后更新一下txn的LockSet即可。
2 DEADLOCK PREVENTION
这一部分内容是做死锁预防,而且规定使用wound-wait算法,算法内容为:
问题的关键是如何判断两个事务谁比较年轻。本lab里事务txn的txn_id_起到逻辑时钟的作用,也就是说,txn_id_越大,越年轻。同时有一点需要注意的是,在本lab里即使一个年轻txn没有持有数据,老的txn也会让它aborted,这一点我不太明白,但测试如此。
这一部分的实现只需在1的while判断之前加入一个遍历,即若当前txn年老于request_queue_中的txn,则将这个事务的状态设置为aborted,设置方式为:
TransactionManager::GetTransaction(年轻txn->txn_id_)->SetState(TransactionState::ABORTED);
遍历结束后若有abort其他事务的情况,lock_request_queue_.cv_.notify_all()通知其他事务自我检查;在while判断结束后若事务是因为abort原因结束的while,直接抛出异常即可。
3 CONCURRENT QUERY EXECUTION
这一节内容很简单,就是修改一下lab3中的几个executor让他们可以支持不同隔离级别的并发查询,具体就是scan、insert、update、delete。具体规则为:
scan在每次读取tuple时加一个shared_lock即可(READ_UNCOMMITTED下不加锁),READ_COMMITTED在读取结束后解锁。
insert操作不需要考虑tuple读取,但是在插入tuple后,若隔离级别不是READ_UNCOMMITTED,为插入的tuple加入exclusive锁
update通过child_executor得到一个tuple,若该tuple已经被上了shared锁,则LockUpgrade为写锁,否则直接上写锁;update结束后,READ_COMMITTED下解开exclusive锁。
delete操作类似update,注意调用:
txn->GetIndexWriteSet()->emplace_back(IndexWriteRecord(*rid, table_info_->oid_, WType::DELETE, key_tuple,
index_info->index_oid_, exec_ctx_->GetCatalog()));
方便roll back。
4 结束
我在做ab4的时候还遇到一个unordered_map的rehash问题,导致gradescope上的死锁测试超时,解决方案在此。
最后上一下分数。这四个lab前前后后大概花了两个月时间去做,还是收获了不少,祝愿大家也能有所进步!