【数据库系统原理】第27篇:基于锁的并发控制:两阶段锁协议(2PL)及其死锁博弈 📅 2026/6/26 8:35:14 一、锁的基本逻辑兼容矩阵与锁粒度锁的本质是一种协议——事务在访问数据项之前必须向锁管理器声明意图锁管理器根据当前其他事务持有锁的情况决定是授予锁还是阻塞等待。每个锁具有一个类型共享锁或排他锁作用于一个数据项从一行到一个表由持有它的当前活跃事务所拥有。共享锁S锁用于读操作。一个事务获取S锁后可以安全地读取数据项因为系统保证在S锁存续期间没有其他事务能够修改该数据项。多个事务可以同时对同一数据项持有S锁——这是共享锁名称的由来。排他锁X锁用于写操作。一个事务获取X锁后可以安全地修改数据项因为系统保证在X锁存续期间没有任何其他事务能够读取或修改该数据项。S锁与X锁之间的相容关系构成了锁的兼容矩阵。S锁与S锁相容——多个事务可以同时读取同一数据。S锁与X锁不相容——有事务在读取时其他事务不能写入有事务在写入时其他事务不能读取。X锁与X锁不相容——一个事务正在修改的数据其他事务不能同时修改。这个兼容矩阵是锁管理器调度加锁请求的基本规则。锁的粒度决定了锁的作用范围——表级锁、页级锁、行级锁、甚至列级锁。锁粒度影响了并发度和锁管理的开销。表级锁最为粗放——一个事务锁定整张表其他事务对该表的任何读写都被阻塞并发度极低但管理开销极小。行级锁粒度最细——不同事务可以同时操作同一表中的不同行并发度高但管理开销大且需要更多的内存来维护锁表。多数现代数据库系统默认使用行级锁但在特定情况下自动升级为表级锁以降低管理开销。第28篇将深入探讨多粒度锁与意向锁的机制。二、两阶段锁协议可串行化的充分条件有了锁的基本工具下一个理论问题是什么样的加锁行为模式能够保证并发调度是可串行化的答案是两阶段锁协议。两阶段锁协议可以精确陈述为每个事务在执行过程中分为两个连续的阶段——增长阶段和收缩阶段。在增长阶段事务可以获取锁但不可以释放任何锁。在收缩阶段事务可以释放锁但不可以再获取任何新的锁。一旦事务释放了第一个锁它就不可逆地进入了收缩阶段。2PL并不要求事务在提交前持有所有锁——它可以释放锁只是释放后不能再获取。2PL对加锁的时机也持开放态度——事务不需要在开始时就一次性获取所有需要的锁可以在执行过程中逐步加锁只要遵守“先释放后就不能再加”的规则。2PL为何能保证可串行化其证明的核心思想是如果所有事务都遵守2PL则并发调度产生的依赖图是无环的。依赖图的节点是事务若T₁释放锁L后T₂获取了锁L则存在一条有向边T₁→T₂T₁先于T₂。若依赖图无环则存在事务的一个拓扑排序该排序就是一个等价的串行调度。2PL的充分性并不依赖于锁的类型——无论是二值锁还是多模式锁是表锁还是行锁只要遵守两阶段规则可串行化就得到保证。这一理论结果的深远意义在于它将可串行化这一看似难以把握的全局属性化简为每个事务独立遵循的局部加锁规则——系统无需监控全局调度只需在锁管理器中执行兼容矩阵检查可串行化自动成立。2PL是充分条件而非必要条件。存在不遵循2PL却仍然可串行化的调度——例如一个仅读取的事务可以在任何时刻释放读锁而不影响可串行化。但2PL的可贵之处在于其普适性无需分析事务的语义无需预知事务的未来操作仅凭加锁行为的结构约束即可保证正确性。三、严格两阶段锁对级联回滚的防御基本的2PL协议保证可串行化但存在一个致命弱点——它无法防止级联回滚。当事务T₁释放了某数据项上的锁后事务T₂获取该锁并基于T₁写入的数据进行了进一步修改。如果T₁随后回滚T₂基于已回滚数据的所有修改都需要被撤销——更糟糕的是如果T₃又基于T₂的修改进行了修改T₃也需要回滚回滚沿着依赖链条级联传播可能导致大量事务被迫中止。级联回滚的根源在于事务在提交前释放了锁——这允许其他事务读取或修改尚未提交的数据。修复这一缺陷的方案是将锁的释放时机推迟到事务提交之后。这一策略在2PL之上附加了一条更严格的时间约束称为严格两阶段锁。严格2PL规定事务持有的所有排他锁必须在事务提交或回滚之后才能释放。共享锁可以在事务结束前释放因为仅读取不会导致级联回滚。严格2PL彻底防止了级联回滚——在T₁提交之前其持有的X锁一直有效其他事务无法读取或修改T₁修改过的数据。只有T₁成功提交后X锁释放其他事务才能安全地基于已提交数据继续工作。如果T₁回滚其修改对外部不可见级联回滚从根本上被杜绝。严格2PL是大多数商业数据库系统所实际采用的并发控制协议——不是理论上的基本2PL而是实践中更强的严格2PL。代价是锁的持有时间延长并发度有所降低——因为X锁直到事务结束才释放冲突事务的等待时间增加。但这一代价被级联回滚风险的消除所充分抵偿——在事务可能因各种原因违反约束、死锁、显式回滚而中止的生产环境中级联回滚的连锁效应可能导致系统吞吐量的灾难性下降。四、死锁锁机制的结构性困局锁机制在保障可串行化的同时不可避免地引入了一个新的困局——死锁。死锁发生时两个或多个事务各自持有对方需要的资源上的锁形成循环等待没有任何一个事务能够继续推进系统陷入僵局。一个经典的死锁场景涉及两个事务的交叉加锁。事务T₁先获取了行A上的X锁意图稍后获取行B上的X锁。事务T₂先获取了行B上的X锁意图稍后获取行A上的X锁。T₁等待T₂释放B的锁T₂等待T₁释放A的锁——谁也无法前进形成死锁。死锁的成因是事务在执行过程中逐步获取锁且持有已获取的锁——这正是2PL增长阶段的特征。如果所有事务在开始时就一次性获取全部所需锁预声明所有锁死锁理论上可以被防止——因为一旦某个事务获取了所有锁就不会在等待其他锁。但预声明策略在实际中并不可行——许多事务需要的锁集合取决于查询结果在执行开始前无法预知。因此任何基于锁的并发控制系统都必须正视死锁的存在并采取处理策略。处理死锁的策略分为两大类——死锁预防和死锁检测——两者代表了截然不同的哲学取向。五、死锁预防宁可错杀不可僵持死锁预防策略的核心思想是防止死锁的必要条件之一被满足。死锁的形成需要四个必要条件同时成立——互斥资源不能被共享、持有并等待事务持有锁的同时等待新锁、非剥夺已获取的锁不能被强制夺走、循环等待等待关系形成环。预防策略通过破坏其中某一条件来杜绝死锁。破坏持有并等待条件的方法是要求事务在开始前一次性申请全部锁。如果任何锁无法获取事务不持有任何锁进入等待状态不会出现持有部分锁却等待新锁的局面。如前所述这一策略的可行性受限于事务的不可预知性。破坏非剥夺条件的方法是允许系统强制夺走事务持有的锁。当事务T₁等待的锁被T₂持有时系统不是让T₁等待而是回滚T₂或T₁之一夺走其锁分配给等待方。这种抢占式调度需要选择牺牲者——通常选择回滚代价较小的事务如运行时间较短、已完成修改较少的事务。抢占的代价是事务被强制回滚消耗了计算资源在高冲突环境下可能导致事务被反复抢占、始终无法完成称为活锁。破坏循环等待条件的方法是对所有可锁资源进行全局排序要求事务只能按照排序顺序申请锁。例如所有事务必须先锁定表A再锁定表B绝不允许先锁定B再锁定A。这样即使出现等待等待关系也不会形成环——持有A锁的事务可能等待B锁持有B锁的事务不能等待A锁。全局排序策略在理论上可以完美防止死锁但在实践中要求所有事务遵循统一的锁顺序这需要全局性的编码规范约束和持续的代码审查在大型系统和不完全受控的应用环境中难以严格执行。六、死锁检测让死锁发生再解决它死锁检测策略采取了与预防策略截然不同的态度——不试图阻止死锁的发生而是允许死锁发生依靠周期性检查发现死锁并强制解除。这种“事后处置”策略的前提假设是死锁在实际系统中并不频繁发生预防死锁的代价预声明、抢占、全局排序高于偶尔检测并解除的代价。死锁检测的标准算法是等待图。等待图是一个有向图节点是活跃事务。如果事务Tᵢ正在等待事务Tⱼ持有的锁则存在一条从Tᵢ指向Tⱼ的有向边。系统周期性构建等待图检测是否存在环路。如果存在环路说明一组事务陷入死锁。一旦检测到死锁系统需要选择一个牺牲者事务——该事务将被强制回滚以打破等待环。牺牲者的选择通常是回滚代价最小的事务——例如已执行时间最短、已插入或更新的行数最少、日志记录量最少的事务。同一事务如果反复被选为牺牲者可能陷入饥饿因此实际系统通常会记录事务的回滚次数或启动时间戳作为选择牺牲者的辅助依据。等待图检测的工程代价在于构建等待图的算法复杂度。在拥有数万个并发连接的生产数据库中构建全局等待图并检测环路的操作可能非常昂贵。因此实际系统通常采用优化策略——仅在事务等待锁超过一定时间阈值后才将其纳入等待图分析范围而非在每次锁请求时都进行全局检测。许多系统同时结合了锁等待超时机制——如果事务等待锁的时间超过预设阈值如30秒系统直接回滚该事务无需等待死锁检测周期。七、现实系统中的工程选择主流数据库系统在死锁处理策略上的选择呈现出高度的一致性——绝大多数选择了死锁检测而非死锁预防。MySQL的InnoDB引擎在检测到死锁时自动回滚代价最小的事务并返回死锁错误给客户端。PostgreSQL同样采用等待图周期性检测策略在检测到死锁时中止其中一个事务。Oracle和SQL Server也遵循类似的检测-回滚模式。这种趋同选择反映了工程经验的一致判断在实际工作负载中死锁的发生频率通常较低通过周期性检测来处理死锁的代价远低于预防策略所带来的并发度损失和编码约束成本。当死锁确实频繁发生时例如由于应用程序的不合理锁顺序设计数据库管理员倾向于通过调整应用程序的加锁顺序、优化索引以减少锁范围、或拆分大事务来根治死锁的高发根因而非在数据库层面转向预防策略。少数专用数据库系统——如某些内存数据库和实时数据库——采用了预防策略通常是基于事务优先级的抢占以避免死锁检测的不确定性延迟对实时性能的影响。这些系统的共同特征是事务执行时间极短、冲突率高、延迟可预测性要求高——这是预防策略的适用边界。八、结语锁的哲学锁是并发控制中最古老、最直观、也最深刻的技术。它的原理朴素——通过互斥来消除竞争通过等待来化解冲突。它的理论基石坚实——两阶段锁协议为可串行化提供了一个简洁而普适的充分条件。它的实践困局真实——死锁是锁机制无法彻底绕过的内源性博弈需要被检测和化解。然而锁的朴素性也构成了它的局限——在高并发读写混合负载下锁的互斥特性导致读写相互阻塞写写串行等待系统的并发吞吐量受到锁竞争强度的硬约束。这是否意味着存在一种不同的并发控制范式能让读操作与写操作互不阻塞下一篇我们将探索多版本并发控制MVCC——一种通过为每条数据保留多个历史版本来实现读写不互斥的技术以及在快照隔离下运行的事务所面临的新类型并发异常。