CMU15445 踩坑指南-PROJECT #4 - CONCURRENCY CONTROL

PROJECT #4 - CONCURRENCY CONTROL

project4需要我们完成一个LockManager进行tuple级别的锁管理,即加减S/X锁(share/exclusive lock),并支持不同的隔离等级(isolation levels).然后在project2中的excutor里正确使用LockMagnger.

一些基础知识

两段锁协议(2PL):简单来说就将事务划分为两个阶段,生长阶段(growing/expanding)和衰退阶段(shrinking),生长阶段加锁,衰退阶段解锁,其中S2PL(strict 2-phase locking)只在事务提交时释放X锁
隔离等级(isolation):在本次实验中只需要这三个隔离等级

REPEATABLE READ:需要X锁和S锁,按照S2PL协议使用
READ COMMITTED:S锁用完就释放
READ UNCOMMITTED:不需要使用S锁

TASK 1 LOCK MANAGER

需要实现四个功能,加S锁LockShared(Transaction, RID),加X锁LockExclusive(Transaction, RID),S锁升级为X锁LockUpgrade(Transaction, RID),解锁Unlock(Transaction, RID).

简单分析一下头文件和我们的任务

LockManger:它做的事就是对于每一个Tuple,通过它的RID,找到对应的等待队列(RequestQueue),每次上锁时,通过判断等待队列的状态执行相应的操作.
RequestQueue:每一个Tuple都有一个等待队列,其中每个节点(Request)记录了申请锁的事务id(txn_id_),锁的类型(lock_mode_),是否已经获取到锁(granted_),并通过条件变量(cv_)来控制每一个请求(Request)的中断和执行.

显然等待队列会被不同的进程同时修改,这样会引起错误,因此需要在其中添加成员变量

std::mutex queue_latch_;

加S锁

  • 首先先进行几步判断:

1.事务状态是否为ABORTED,如果是直接返回false,否则继续执行;
2.事务状态是否为SHRINKING,如果是将事务设为ABORTED,并抛出异常,否则继续执行;
3.隔离等级是否为READ UNCOMMITTED,如果是不需要上S锁,将事务设为ABORTED,并抛出异常,否则继续执行;
4.是否已经获取到了S/X锁,如果是直接返回true.

  • 其次通过RID找到对应的等待队列,生成等待请求并插入队尾.
  • 然后循环判断是否合法,找到合法时机时获取锁.

在高并发场景中,使用循环等待非常的不合理,会占用一部分系统资源而不释放,造成忙等待,因此推荐使用条件变量(condition_variable).
cv的逻辑非常简单,我们只需要用到两个函数:

cv_.wait(mutex,bool()):第二个参数为判断是否继续执行的函数,返回true时继续执行,否则继续等待.
cv_.notify_all(): 唤醒所有等待的线程.

我的代码是这样写的

auto is_compatible = [&] {
  for (auto &request : lock_request_queue.request_queue_) {
    if (request.txn_id_ == lock_request.txn_id_) {
      return true;
    }
    if (request.lock_mode_ == LockMode::EXCLUSIVE) {
      return false;
    }
  }
  return true;
};

lock_request_queue.cv_.wait(
      queue_latch, [&is_compatible, &txn] { return is_compatible() || txn->GetState() == TransactionState::ABORTED; });

获取S锁的条件为:

1.该请求前全是S锁
2.该请求前的S锁都以及获取,即granted==true.

第二点感觉不太合理,因此我没加,也能过所有测试.

  • 获取锁,即在txn的shared_lock_set_中添加对应的rid

加X锁

前面的步骤与加S锁一致,循环判断需要修改为判断该请求是否为请求队列的第一个(X锁与其他锁不兼容).

升级锁

  • 事务状态判断,是否获取锁判断,与前面一致;
  • 升级锁之前需要确保以及获取了S锁;
  • 在相应的请求队列中找到该请求(获取S锁的请求),并将granted_设为false,lock_mode_设为EXCLUSIVE;
  • 循环等待加阻塞;
  • 满足上锁条件后,删除S锁,加上X锁,修改granted_和upgrading_(不改也能过测试).

解锁

  • 判断是否存在这个锁,没有就return false;
  • 从RequestQueue中删除相应的Ruquest;
  • 修改事务状态;
  • 删除锁;
  • 唤醒阻塞的线程cv_.notify_all().

注意

只有growing的时候解锁需要修改状态(abort和commit的时候也会解锁,此时不应该修改状态)
READ COMMITED的S锁用完就解锁,但是不修改状态

TASK 2 DEADLOCK PREVENTION

这次的实验使用的是wound-wait算法,这个算法的逻辑是:

遍历等待队列中排在该事务前的所有事务;
如果是上S锁,将前面的比该事务年轻的X锁(比较txn_id,id大的事务更年轻)全都设为ABORTED;
如果是上X锁,将前面的所有年轻事务设为ABORTED.

对前面的代码做以下修改:

  • 在每一次在请求队列插入请求后使用wound-wait修改请求队列
  • 修改上锁的函数,在每次唤醒条件加上txn->GetState() == TransactionState::ABORTED(前面的代码已经写上了),唤醒后判断事务状态是否为ABORTED,因为可能在在休眠过程中被其他事务干掉了.

TASK 3 CONCURRENT QUERY EXECUTION

给上一个project写的executor加上并发控制,也是使用我们之前写的LockManager.我们之前实现的是火山模型,每调用一次next()获取一个结果,解锁需要注意这一点.进行修改操作时需要保留修改记录(IndexWriteRecord)

SEQUENTIAL SCAN

  • 上锁:READ_UNCOMMITTED时什么都不做,其他情况上S锁
  • 解锁:READ_COMMITTED读完就解锁,REPEATABLE_READ不用管了,等事务提交或回滚时自动解锁.

INSERT

  • 在新插入的tuple上加X锁
  • 记录修改

UPDATE

  • 上X锁(或者S锁升级为X锁)然后就不用管它了
  • 记录修改

DELETE

  • 同上

如何记录修改

代码如下:

IndexWriteRecord idx_write_record(rid, table_info_->oid_, WType::INSERT, *tuple, *old_tuple, index->index_oid_,
                                      exec_ctx_->GetCatalog());
    txn->GetIndexWriteSet()->push_back(idx_write_record);

第4个参数是修改后的tuple,后面是修改前的.测试平台上的IndexWriteRecord是老版本的,只有6个参数,只要把transaction.h文件一起交上去可以了.

提交

由于版本问题,只把官网说的那些文件交上去是不够的,直接执行用下面这条指令就可以了

zip project4-submission.zip src/include/buffer/lru_replacer.h src/buffer/lru_replacer.cpp src/include/buffer/buffer_pool_manager_instance.h src/buffer/buffer_pool_manager_instance.cpp src/include/storage/page/hash_table_directory_page.h src/storage/page/hash_table_directory_page.cpp src/include/storage/page/hash_table_bucket_page.h src/storage/page/hash_table_bucket_page.cpp src/include/container/hash/extendible_hash_table.h src/container/hash/extendible_hash_table.cpp src/include/execution/execution_engine.h src/include/execution/executors/seq_scan_executor.h src/include/execution/executors/insert_executor.h src/include/execution/executors/update_executor.h src/include/execution/executors/delete_executor.h src/include/execution/executors/nested_loop_join_executor.h src/include/execution/executors/hash_join_executor.h src/include/execution/executors/aggregation_executor.h src/include/execution/executors/limit_executor.h src/include/execution/executors/distinct_executor.h src/execution/seq_scan_executor.cpp src/execution/insert_executor.cpp src/execution/update_executor.cpp src/execution/delete_executor.cpp src/execution/nested_loop_join_executor.cpp src/execution/hash_join_executor.cpp src/execution/aggregation_executor.cpp src/execution/limit_executor.cpp src/execution/distinct_executor.cpp src/include/storage/page/tmp_tuple_page.h src/concurrency/lock_manager.cpp src/include/concurrency/lock_manager.h src/include/concurrency/transaction.h

注意检查代码格式

一些报错

我在在线测试时遇到了一些本地测试没遇到的问题,如果遇到类似的报错可以按照我的思路排查.

  • Test [LockManagerTest.WoundWaitTest] timeout:一开始我在每次唤醒后都对等待队列使用了wound-wait算法,这种做法并不合理,而且循环次数太多,导致超时.
  • Test [GradingTransactionTest.UnrepeatableReadsTest] failed.:这个问题一般是释放锁的时机不对,X锁加上就不用管它了,S锁在READ COMMITTED时立即释放,其他情况不用管.
  • test_memory_safety (__main__.TestProject4) (0.0/5.0):内存泄漏的原因很多,但是这个实验用的容器都是内存安全的,检查一下是不是有些对象(或者是迭代器)被删除之后还在使用.

最后

距离上传project0已经三个月,中途断断续续写了三个月,趁着刚写完pro4,思路比较清晰就赶紧把4更新了,123后面会找机会补上,也欢迎大家指正错误.
cmu15445是一节值得学习的课,做这个实验的过程收获很多,祝大家也能早日通关,加油!

链接

PROJECT #0 - C++ PRIMER
PROJECT #1- BUFFER POOL

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值