PROJECT #4 - CONCURRENCY CONTROL
note:最后一个项目啦,拖了好久勒,现来更新 ing,欢迎品读~
题目
第四个编程项目是在你的数据库系统中实现一个锁管理器,然后用它来支持并发查询的执行。锁管理器负责跟踪发给事务的元组级锁,并支持共享和独占锁,根据隔离级别适当地授予和释放锁。
在本项目中,将要实现以下隔离级别:
Repeatable reads:可重复读取只允许读取已committed的数据,并进一步要求,在一个事务对一个数据项的两次读取之间,不允许其他事务对其进行更新。然而,相对于其他事务,该事务可能不是可序列化的。例如,当它在搜索满足某些条件的数据时,一个事务可能会找到一些由已承诺的事务插入的数据,但可能不会找到由同一事务插入的其他数据(没有索引锁导致的幻读)。
Read committed :读取承诺只允许读取已committed的数据,但不要求可重复读取。例如,在事务对一个数据项的两次读取之间,另一个事务可能已经更新了该数据项并提交。
Read Uncommitted:读取未提交的数据允许读取未提交的数据。它是SQL允许的最低隔离级别。
要点
-
需要注意以上隔离级别的区别,首先是第一个可重复读,它要求事务一直持有读锁,直到事务提交才释放,于是需要在顺序扫描的初始化中,需要判断隔离级别,如果是可重复读,则需要对该表加全局读锁,保证可重复读,而第二个读已提交,他要求读取已经提交的数据,也就是说只需要在读的时候持有锁就行,读完就可以解锁,不用等到提交才解锁,最后一个读未提交,可以脏读,不需要加锁,如果加锁就abort。
-
关于读锁、写锁、可升级锁的加锁方式:
要理解为什么要加锁,说白了就是保证资源的合理分配,谁先用,谁的优先级就大!其次,对于死锁也要采取策略:
这里选择第二种死锁策略:Wound-Wait
-
读锁:获取该资源的等待列表,将请求加入该请求列表,接着引入死锁策略,防止死锁,这里需要注意,当abort一个事务后,不仅需要将其从等待列表中删除,还需要使用notify函数,及时通知已经中止的线程,使其在等待中及时退出(这就是条件变量的强大,可以在线程间进行通讯)。还有一点要注意的是,由于需要进行等待,当多个高优先级的事务提交后,会导致资源的等待列表的迭代器失效,为此需要重新进行获取,然后继续等待,这个问题我遇到多次,这也是多线程导致的问题。
-
写锁:和读锁差不多,只需要改一下列表的等待条件和死锁策略即可,同时,当事务状态为中止时,需要及时返回false,最后在获得写锁的条件是:当前事务的状态不是中止状态同时等待列表的长度为一(也就是只有该写请求)。
-
升级锁:这里需要注意upgrading_的使用,这个变量主要是保证该资源的等待列表不能出现两个升级的事务,因为这样会导致死锁,其次,升级锁也需要保证该事务在这之前已经获得读锁,接着是死锁策略的实现和之前一样,最后获得升级锁的条件与写锁一致,但最后需要将upgrading复位,便于下一个事务获取升级锁。
-
解锁:首先是两阶段锁的判定,在这里需要注意只有当隔离级别是可重复读的时候才需要保证,因为只有在要提交的时候才需要进行解锁。
-
关于条件变量的使用
- **condition_variable是一个类,搭配互斥量mutex来用,这个类有它自己的一些函数,这里就主要讲wait函数和notify_all和notify_one(后面简称notify)函数,故名思意,wait就是有一个等待的作用,notify就是有一个通知的作用。 简而言之就是程序运行到wait函数的时候会先在此阻塞,然后自动unlock,那么其他线程在拿到锁以后就会往下运行,当运行到notify()函数的时候,就会唤醒wait函数,然后自动lock并继续下运行。**为此,在进行线程间的调度的时候,需要将两者进行搭配使用,才能达到预料的效果。
-
关于索引更新的问题
- 由于在进行删除或者更新的时候,需要进行索引的插入或者更新,为此在回退的时候需要进行回退操作,其中包括索引的回退,这是在测试案例里面要求的一项,而tableheap里面会进行tablewriteset的维护,于是在进行更新或者删除的时候已经完成事务对tablewriteset的维护,但是tableindexwriteset没有相应的维护,为此需要在删除和更新索引后,也要对该事务的索引修改集进行更新。
-
关于多线程的调试心得
-
由于在测试案例里面存在多个线程并发的情况,为此在调试时增加了调试的难度,这里将其中的技术进行归纳与总结:
-
常用的gdb命令:
(1)查看可切换调试的线程:info threads
(2)切换调试的线程:thread 线程id
(3)只运行当前线程:set scheduler-locking on
(4)运行全部的线程:set scheduler-locking off
-
-
在进行debug的时候,常会碰见死锁需要认真考量,最好进行绘制其wait for graph,在进行调试的时候,也不是很方便,尤其是没理解条件变量的情况下。
-
-
关于lambda表达式的用法
-
都说lambda的引入后,使得代码的可读性和便捷性
-
语法:
[ capture ] ( params ) opt -> ret { body; }; // [函数对象参数] (函数参数)修饰符->返回值类型{函数体};
[capture] 标识一个Lambda表达式的开始,这一部分是不可以忽略的。函数对象参数只能使用到定义该Lambda表达式为止定义过的局部变量,包括Lambda表达式所在类的成员变量。函数参数有以下几种形式:
空:代表不捕获Lambda表达式外的变量;
&:代表以引用传递的方式捕获Lambda表达式外的变量;
=:代表以值传递的方式捕获Lambda表达式外的变量,即以const引用的方式传值;
this:表示Lambda表达式可以使用Lambda表达式所在类的成员变量;拓展:
a或=a:表示以值引用的方式传递变量a aa,即const int a,在函数体内不可改变a的值;但是可以对Lambda表达式使用mutable修饰符修饰,使得函数对象参数可以进行赋值,但是该函数对象参数不是被修改为引用传递方式,下面进行细说;
&a:表示以引用传递的方式传递变量a aa,在函数体内可以改变a的值;
x,&y:x为值传递方式,y为引用传值方式;
=,&x,&y:除x,y为引用传递方式以外,其他参数都为值传递方式进行传递;
&,x,y:除x,y为值传递方式以外,其他参数都为引用传递方式进行传递;
-
-
关于在执行器中加入并发控制的问题
- 顺序扫描:根据事务的隔离级别进行加锁,加读锁
- 删除:根据事务的级别加锁,加写锁或者升级锁
- 更新:根据事务的级别加锁,加写锁或者升级锁
总结
终于完成了整个项目,这让我感受到了C++的强大,数据库的魅力,Orz~,还有测试案例,Andy Team真是太厉害了,项目在测试中不断修改,才能达到完美,试问哪里还有这么好的地儿呢哈哈哈哈。RESPECT!