在lab4中实现一个基于 2PL 的并发控制方式,自动为并发事务执行加锁解锁,提供可串行化能力并实现可重复读、读已提交、读未提交三种隔离度
- Lock Manager:锁管理器,利用 2PL 实现并发控制。支持
REPEATABLE_READ
、READ_COMMITTED
和READ_UNCOMMITTED
三种隔离级别,支持SHARED
、EXCLUSIVE
、INTENTION_SHARED
、INTENTION_EXCLUSIVE
和SHARED_INTENTION_EXCLUSIVE
五种锁,支持 table 和 row 两种锁粒度,支持锁升级。 - Deadlock Detection:死锁检测,运行在一个 background 线程,每间隔一定时间检测当前是否出现死锁,并挑选合适的事务将其 abort 以解开死锁。
- Concurrent Query Execution:修改之前实现的
SeqScan
、Insert
和Delete
算子,加上适当的锁以实现并发的查询。
lock manager处理锁请求流程:
- 当一个新的加锁请求到达时,如果请求队列存在,他在对应资源的请求队列末尾添加一条记录,否则创建一个新的队列。
- 如果资源没有没有被锁住,那么授予锁。
- 如果已经有事务获取了锁,检查锁的兼容性,如果兼容并且先前锁请求都被授予,才能获取锁,否则只能等待。这里保证了锁的请求不会饥饿。
- 当解锁请求到达时,lock manger把对应的加锁记录从请求队列中移除。检查后续等待获取请求能否被授予锁。
以上图为例,对某个表A。
- t1时刻,txn1 事务发起锁请求,创建了一个新的 request queue,直接授予锁。
- t2时刻,txn2 事务发起 SIX 锁请求,但是 SIX 与 S 不兼容,所以 txn2 阻塞在队列中
- t3时刻,txn3 发起 IS 锁请求,尽管 IS 锁与 授予了的S 锁甚至 SIX 锁都兼容,但是因为 txn2 没有被授予,所以 txn3 也不能被授予。
- t4时刻,txn1解锁,这时 txn2 和 txn3 同时被授予。
具体思路参考:CMU15445-2022 P4 Concurrency Control
Deadlock Detection
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的基础上,读写锁都保持到事务结束