2021 CMU-15445/645 Project #4 : Concurrency Control 【完】

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前前后后大概花了两个月时间去做,还是收获了不少,祝愿大家也能有所进步!
在这里插入图片描述

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值