尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

【数据库系统原理】第27篇:基于锁的并发控制:两阶段锁协议(2PL)及其死锁博弈

【数据库系统原理】第27篇:基于锁的并发控制:两阶段锁协议(2PL)及其死锁博弈
📅 发布时间:2026/6/26 8:39:50

一、锁的基本逻辑:兼容矩阵与锁粒度

锁的本质是一种协议——事务在访问数据项之前必须向锁管理器声明意图,锁管理器根据当前其他事务持有锁的情况决定是授予锁还是阻塞等待。每个锁具有一个类型(共享锁或排他锁),作用于一个数据项(从一行到一个表),由持有它的当前活跃事务所拥有。

共享锁(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)——一种通过为每条数据保留多个历史版本来实现读写不互斥的技术,以及在快照隔离下运行的事务所面临的新类型并发异常。

相关新闻

  • 电赛实战指南:从硬件设计到软件调试的工程能力跃迁
  • FanControl深度配置指南:从基础控制到高级优化的完整解决方案
  • 前端播放flv

最新新闻

  • 解决苹果审核 4.3 问题的有效策略:实战经验分享,成功上架 App Store(附真实案例)
  • Dism++实战指南:5大核心模块深度剖析与Windows系统维护全攻略
  • 如何让JavaScript应用听懂你的日程安排?Sherlock自然语言事件解析器深度解析
  • PaperXie AI PPT 生成器:文稿一键转演示文稿,打破 PPT 制作的效率壁垒
  • 接口自动化测试覆盖率实战:从概念到CI/CD集成的完整策略
  • 几何美学与现代设计:为什么Montserrat字体成为开源字体的典范?

日新闻

  • Qwen2.5-Turbo百万上下文实战指南:百炼平台长文本处理全解析
  • 怎么监控对标账号更新,2026年作者监控工作流,5款深度对比
  • EdgeRemover:专业级Windows Edge浏览器管理工具,彻底解决顽固软件卸载难题

周新闻

  • Visual C++运行库修复终极指南:5分钟快速解决Windows软件启动错误
  • 手把手教你构建统计局地区经济数据爬虫:从环境搭建到数据持久化全指南
  • 2026多Agent深度解析:用AI团队替代单一模型,四种架构实战落地

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号