在lab4中实现一个基于 2PL 的并发控制方式,自动为并发事务执行加锁解锁,提供可串行化能力并实现可重复读、读已提交、读未提交三种隔离度
REPEATABLE_READ
、READ_COMMITTED
和 READ_UNCOMMITTED
三种隔离级别,支持 SHARED
、EXCLUSIVE
、INTENTION_SHARED
、INTENTION_EXCLUSIVE
和 SHARED_INTENTION_EXCLUSIVE
五种锁,支持 table 和 row 两种锁粒度,支持锁升级。SeqScan
、Insert
和 Delete
算子,加上适当的锁以实现并发的查询。
lock manager处理锁请求流程:
以上图为例,对某个表A。
具体思路参考:CMU15445-2022 P4 Concurrency Control
2PL不可避免的会产生死锁,所以要及时检测死锁打破依赖。这一节比较简单,bustub 会在创建 lock_manger 时,在后台创建一个周期性的死锁检测线程。
利用dfs 查询是否存在圈,释放最后的事务
尽管数据库在死锁问题上普遍采用检测和解除的方法处理死锁,而不是预防。对DBMS预防死锁、活锁的方法还是有必要学习的。
三种主要策略:
一次性封锁(类似于静态资源分配,操作系统知识)
每个事务必须一次将所有要用的数据加锁,否则不能继续执行。
顺序封锁(请求序列,破坏循环等待条件,操作系统知识)
预先为数据对象规定一个封锁顺序,所有事务按照顺序进行封锁。
以上两种方法都不适用,第一种效率很低,第二种执行困难。
第三种方法,也是比较难理解的一种方法:
时间戳(这个按照书面语挺难理解的,下面是我转述的,希望更好理解)
每个事务都给它一个时间戳,当A申请资源锁的时 候,B已经获得了锁,有以下两个策略
wait-die(等待死亡):是一种非剥夺策略,老的事务等待新的事务释放资源,即若A比B老,则等待B执行结束,否则A卷回(roll-back),一段时间后会以原先的时间戳继续申请。老的才有资格等,年轻的全部卷回。
wound-wait(伤害-等待):是一种剥夺策略,如果A比B年轻,A才等待,A比B老,则杀死B,B回滚。换句话说,老事务不等待"你",直接杀死"你",抢占资源,小孩子才等。
等待死亡的特点是不剥夺,但只有老的有资格等待。
伤害等待的特点是老的不等待,直接把你干掉,新的才去等待。
从某种意义上来说,这两者很类似,都是老事务优先(否则就会有饿死现象)
总结
两个方法都保证事务执行是单向的(要么老的等新的(等现存的持有锁的新事务结束,而不是说等所有新的事务申请结束了才执行老的),要么新的等老的),不会出现循环等待,从而避免了死锁,也都确保了老事务的优先权,不会活锁,所以时间戳法是可采纳的。
但二阶段锁也有一些问题:级联回滚(Cascading Aborts)
如下所示,T1释放锁之后,T2事务开始被执行,T2对A的操作是基于T1对A进行临时修改后的版本进行的,如果T1事务没有提交而是被abort了,那么T2必须跟着T1一起回滚(如果T2进行的是读操作,那么这也被称为脏读,"dirty reads")
级联回滚本质上的原因是T2事务在T1事务更新得到的临时版本的数据上进行了操作,那我们可以通过一些手段让T2不在T1修改得到的临时版本上进行操作:比如说,可以让事务先获取各个需要获取的锁,等到它commit时再一次性把这些锁释放掉,这样的话,T2就不可能在临时版本上进行操作,因为当T2能获得锁执行事务时,和它访问共享数据的其他事务已经被提交了。这个方法也被称为严格二阶段锁(Strong Strict 2PL,简称SS2PL),如下图中被红色方框圈出的部分所描述的那样,可以解决脏读的问题
2PL(2 Phase Locking), 锁分两阶段,一阶段申请,一阶段释放
S2PL(Strict 2PL),在2PL的基础上,写锁保持到事务结束
SS2PL( Strong 2PL),在2PL的基础上,读写锁都保持到事务结束