本lab将实现一个锁管理器,事务经过锁管理器获取锁,事务管理器根据状况决定是否授予锁,或是阻塞等待其它事务释放该锁。html
众所周知,事务具备以下属性:git
将对数据对象Q的操做进行抽象,read(Q):取数据对象Q,write(Q)写数据对象Q。github
考虑事务T1,T1从帐户A向帐户B转移50。算法
T1: read(A); A := A - 50; write(A); read(B); B := B + 50; write(B).
事务T2将帐户A的10%转移到帐户B。安全
T2: read(A); temp := A * 0.1; A := A - temp; write(A); read(B); B := B + temp; write(B).
假设帐户A、B初始值分别为1000和2000。
咱们将事务执行的序列称为schedule。以下面这个schedule,T1先执行完,而后执行T2,最终的结果是具备一致性的。咱们称这种schedule为serializable schedule。数据结构
T1 T2 read(A); A := A - 50; write(A); read(B); B := B + 50; write(B). read(A); temp := A * 0.1; A := A - temp; write(A); read(B); B := B + temp; write(B).
可是看下面这个shedule:并发
T1 T2 read(A); A := A - 50; read(A); temp := A * 0.1; A := A - temp; write(A); read(B); write(A); read(B); B := B + 50; write(B). read(B); B := B + temp; write(B).
执行完帐户A和B分别为950和2100。显然这个shecule不是serializable schedule。函数
考虑连续的两条指令I和J,若是I和J操做不一样的数据项那么,这两个指令能够交换顺序,不会影响schedule的执行结果。若是I和J操做相同的数据项,那么只有当I和J都是read(Q)时才不会影响schedule的结果。若是两条连续的指令,操做相同的数据项,其中至少一个指令是write,那么I和J是conflict的。ui
若是schedule S连续的条指令I和J不conflict,咱们能够交换它们执行的顺序,从而产生一个新的schedlue S',咱们称S和S'conflict equivalent。若是S通过一系列conflict equivalent变换,和某个serializable schedule等价,那么咱们称S是conflict serializable。3d
好比下面这个schedule S:
T1 T2 read(A); write(A); read(A); write(A); read(B); write(B); read(B); write(B);
通过屡次conflict equivalent变换,生成新的schedule S',S'是serializable schedule。
T1 T2 read(A); write(A); read(B); write(B); read(A); write(A); read(B); write(B);
因此S是conflict serializable的。
前面提到多个事务并发执行的时候,可能出现数据不一致得状况。一个很显然的想法是加锁来进行并发控制。
可使用共享锁(lock-S),排他锁(lock-X)。
问题来了。
在何时加锁?何时释放锁?
考虑下面这种加解锁顺序:
事务一从帐户B向帐户A转移50。
T1: lock-X(B); read(B); B := B - 50; write(B); unlock(B); lock-X(A); read(A); A := A + 50; write(A); unlock(A).
事务二展现帐户A和B的总和。
T2: lock-S(A); read(A); unlock(A); lock-S(B); read(B); unlock(B); display(A+B).
可能出现这样一种schedule:
T1 T2 lock-X(B); read(B); B := B - 50; write(B); unlock(B); lock-S(A); read(A); unlock(A); lock-S(B); read(B); unlock(B); display(A+B). lock-X(A); read(A); A := A + 50; write(A); unlock(A).
假设初始时A和B分别是100和200,执行后事务二显示A+B为250,显然出现了数据不一致。
咱们已经加了锁,为何还会出现数据不一致?
问题出在T1过早unlock(B)。
这时引入了two-phase locking协议,该协议限制了加解锁的顺序。
该协议将事务分红两个阶段,
Growing phase:事务能够获取锁,可是不能释听任何锁。
Shringking phase:事务能够释放锁,可是不能获取锁。
最开始事务处于Growing phase,能够随意获取锁,一旦事务释放了锁,该事务进入Shringking phase,以后就不能再获取锁。
按照two-phase locking协议重写以前的转帐事务:
事务一从帐户B向帐户A转移50。
T1: lock-X(B); read(B); B := B - 50; write(B); lock-X(A); read(A); A := A + 50; write(A); unlock(B); unlock(A).
事务二展现帐户A和B的总和。
T2: lock-S(A); read(A); lock-S(B); read(B); display(A+B). unlock(A); unlock(B);
如今不管如何都不会出现数据不一致的状况了。
课本的课后题15.1也要求咱们证实two-phase locking(如下称2PL rule)的正确性。我看了下解答,用的是反正法。我还看到一个用概括法证的,比较有趣。
前提:
目标:
证实Sn是conflict serializable的schedule。
证实开始:
起始步骤,n = 1的状况:
T1遵照2PL rule。
S1这个schedule只包含T1。
显然S1是conflict serializable的schedule。
迭代步骤:
迭代假设:假设Sn-1是T1, T2, ... Tn−1造成的一个schedule,而且Sn-1是conflict serializable的schedule。咱们须要证实Sn-1是conflict serializable的schedule,Sn也是conflict serializable的schedule。
假设Ui(•)是事务i的解锁操做,而且是schedule Sn中第一个解锁的操做:
能够证实,咱们能够将事务i全部ri(•) and wi(•)操做移到Sn的最前面,而不会引发conflict。
证实以下:
令Wi(Y)是事务i的任意操做,Wj(Y)是事务j的一个操做,而且和Wi(Y)conflict。等价于证实不会出现以下这种状况:
假设出现了这种状况,那么必然有以下加解锁顺序:
又由于全部事务都遵照2PL rule,因此必然有以下加解锁顺序:
冲突出现了,Ui(•)应该是Sn中第一个解锁操做,可是如今倒是Uj(Y)。因此假设不成立,因此结论:"咱们能够将事务i全部ri(•) and wi(•)操做移到Sn的最前面,而不会引发conflict"成立。
咱们将事务i的全部操做移到schedule最前面,
又由于Sn-1是conflict serializable的因此Sn是conflict serializable的。
证实完毕
two-phase locking能够保证conflict serializable,但可能会出现死锁的状况。
考虑这个schedule片断:
T1 T2 lock-X(B); read(B); B := B - 50; write(B); lock-S(A); read(A); lock-S(B); lock-X(A);
T1和T2都遵循2PL rule,可是T2等待T1释放B上的锁,T1等待T2释放A上的锁,形成死锁。
有两类基本思路:
这里介绍wait-die这种死锁预防机制,该机制描述以下:
事务Ti请求某个数据项,该数据项已经被事务Tj获取了锁,Ti容许等待当且仅当Ti的时间戳小于Tj,不然Ti将被roll back。
为何该机制能保证,不会出现死锁的状况呢?
若是Ti等待Tj释放锁,咱们记Ti->Tj。那么系统中全部的事务将组成一个称做wait-for graph的有向图。容易证实:wait-for graph出现环和系统将出现死锁等价。
wait-die这种机制就能防止出现wait-for graph出现环。为何?由于wait-die机制只容许时间戳小的等待时间戳大的事务,也就是说在wait-for graph中任意一条边Ti->Tj,Ti的时间戳都小于Tj,显然不可能出现环。因此不会出现环,也就不可能出现死锁。
事务管理器LockManager对外提供四个接口函数:
能够用以下数据结构来实现:
每一个数据项对应一个链表,该链表记录请求队列。
当一个请求到来时,若是请求的数据项当前没有任何事务访问,那么建立一个空队列,将当前请求直接放入其中,受权经过。若是不是第一个请求,那么将当前事务加入队列,只有当前请求以前的请求和当前请求兼容,才受权,不然等待。
在哪里调用LockManager呢?
page/table_page.cpp中的TablePage类用于插入,删除,更新,查找表记录。在执行插入,删除,查找前都会获取相应的锁,确保多个事务同时操做相同数据项是安全的。
LockManager的具体代码能够参考个人手实现:https://github.com/gatsbyd/cmu_15445_2018
参考资料: