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是一节值得学习的课,做这个实验的过程收获很多,祝大家也能早日通关,加油!