1. Overview
前面一小节(并发控制理论)介绍了如何并发执行多个事务,设置检测机制判断执行调度满足可串行化。那些方法假设DBMS已知所有负载,然而,在真实系统中,系统往往并不知道用户在未来可能发送的所有请求,在这种情况下如何保证最终产生可串行化调度是一个新的问题。本小节介绍的两阶段封锁协议(2PL)是其中一种解决方式,它是一种悲观的解决策略,相应的,还有一类乐观的解决策略。
2. Lock Types
在索引并发控制那一小节提到过,Lock和Latch的区别。Latch指物理层面对数据结构的锁,Lock指对逻辑层面的事务、数据库对象的锁。
锁的类型分为读锁和写锁,读锁之间不互斥,写锁与任意的锁都互斥。
为了在事务并发过程中避免读写冲突,可以使用锁对对象进行管理,Lock Manager中记录当前锁的分配情况。
下图是一个使用锁实现数据对象原子访问的例子,但仅仅这样这还不够,下面的例子不满足可串行化,出现了不可重复读的问题。
两阶段封锁(2PL)是一种能够无需提前知道未来所有事务内容的情况下保证调度冲突可串行化的一种并发协议。
两阶段封锁顾名思义分为两个阶段,分别是Growing阶段和Shrink阶段。在Growing阶段,事务从Lock Manager处获得执行事务所需要的所有数据对象的锁;随后进入Shrink阶段,只能释放Growing阶段获得的锁,不能够重新获取新的锁。
Shrink阶段,只能释放Growing阶段获得的锁,不能够重新获取新的锁(避免了提到的脏读问题)。
下面是一个运用2PL执行事务T1和事务T2的例子。
到目前为止介绍的2PL还不够完美,还会发生级联中止的问题(cascading abort)。
例如下图所示,若T1最终ABORT掉,由于T2已经读了T1写后的值,出现脏读问题,导致T2也要中止。若级联中止导致了DBMS事务冲突率较高,成功提交的事务变少影响性能,可以通过Strong Strict 2PL彻底避免。
Strong Strict 2PL指只有等到事务最终执行完,决定COMMIT前再释放所有锁(这里一直有个疑问,就是会不会COMMIT失败?)。
可以看出,Strong Strict 2PL可以彻底避免级联中止的问题,但这是一个trade-off的过程,使用Strong Strict 2PL的同时也降低了执行的并行度。
下图是一个使用Strong Strict 2PL计算A+B的例子,T2直到T1决定COMMIT了才能够获得A的写锁。
几种调度的关系如图所示。
到目前为止介绍的2PL还可能发生死锁的问题,如下图所示,T1和T2因为互相争用锁而发生死锁。
在发生事务死锁情况下,Wait For 图表现出是一个环。处理死锁的方式可以通过死锁检测或者提前预防死锁的发生。
对于死锁的检测,Wait For 图可以判断调度中是否有死锁,与上一小节介绍的冲突依赖图不同的是,对于依赖图中冲突的两个事务,先执行的事务指向后发生的事务;Wait For 图中的有向边Ti->Tj表示事务Ti等待事务Tj释放锁。
下图所示是一个死锁的例子。
一旦检测到有死锁产生,DBMS会回滚部分事务打破死锁状态,从而能够让系统继续运行,被回滚的事务重新执行。
允许根据不同规则来选择回滚的事务,目的还是想要回滚带来的损失最小,同时也要避免starvation(饥饿)问题。
同时,被回滚的事务可以考虑只回滚事务中导致死锁的操作,剩余的操作不回滚,减少回滚带来的损失。
也可以通过设置预防机制彻底避免死锁的发生,如下图所示的两种策略。第一种策略统一按照时间戳顺序发放锁,第二种相反,通过规定统一的锁分配策略来避免死锁的发生。
下图分别两种机制对锁争用的处理。
若一个查询涉及1000万行记录,lock manager同时管理1000万个锁将会造成较低的处理性能。
通过层级化锁可以达到对大规模tuple上锁的目的。
具体而言,这种锁叫Intention Lock。
IS、IX、SIX互斥情况。
下图是T1、T2、T3通过层次化锁实现并发执行的例子。
这种层级化锁机制优化了需要获取大规模锁的查询性能,同时也考虑了查询的并发性。