当前位置: 首页 > news >正文

死锁的产生、检测与避免

在上一篇中,我们见证了 Next-Key Lock 如何阻止幻读。但锁是一把双刃剑——它保护数据一致性的同时,也带来了新的风险:死锁。当两个或多个事务互相持有对方需要的锁资源,形成循环等待时,所有参与者都无法继续执行,就像堵死在十字路口的车流。

本文将深入分析死锁的方方面面:

  • 死锁的四个必要条件(及破坏方法)
  • InnoDB 的死锁检测机制(等待图)
  • 死锁超时参数的作用与局限
  • 如何从SHOW ENGINE INNODB STATUS日志中解读死锁信息
  • 实战:亲手构造一个死锁场景并分析回滚结果
  • 避免死锁的编码与设计建议

读完本文,你将不仅能解释死锁的产生原理,还能在项目中主动规避和诊断死锁问题。


1. 死锁的四个必要条件

死锁并非数据库独有的概念,它是并发系统中普遍存在的问题。根据计算机科学的经典定义,死锁必须同时满足四个条件:

  1. 互斥(Mutual Exclusion):资源一次只能被一个进程(事务)持有。数据库中的 X 锁天然具有互斥性。
  2. 持有并等待(Hold and Wait):一个事务已经持有至少一个资源,又在等待其他事务释放的资源。
  3. 不可剥夺(No Preemption):已分配给事务的资源不能被强制夺走,只能由持有者自己释放。
  4. 循环等待(Circular Wait):存在事务的循环链:T1 等待 T2 持有的资源,T2 等待 T3 持有的资源,…,Tn 等待 T1 持有的资源。

破坏任意一个条件即可预防死锁

  • 破坏“互斥”:对于数据库锁资源不可能,因为数据一致性需要互斥。
  • 破坏“持有并等待”:一次性申请所有需要的锁(如LOCK TABLES,但并发度极差)。
  • 破坏“不可剥夺”:超时回滚事务,强制释放锁。
  • 破坏“循环等待”:按固定顺序访问资源(如总是先锁表 A 再锁表 B)。

InnoDB 实际采用的方法是检测死锁并回滚(而非预防),同时提供超时机制作为补充。


2. InnoDB 的死锁检测机制

2.1 等待图(Wait-for Graph)

InnoDB 内部维护了一个等待图数据结构:

  • 节点:每个事务。
  • 有向边:T1 → T2 表示“T1 正在等待 T2 释放的锁”。

每当一个事务因为锁而阻塞时,InnoDB 会将这条边加入等待图,然后运行**深度优先搜索(DFS)**检查是否出现了环。如果发现了环,就说明发生了死锁。

2.2 死锁解决策略

检测到死锁后,InnoDB 必须让至少一个事务回滚,以打破循环。选择牺牲品的原则是:回滚代价最小的事务——即修改行数最少的事务(由undo log的大小估算)。被选中的事务会收到错误:

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

此时,应用层应捕获这个错误,并在合适的时机重试整个事务

2.3 死锁检测的开关与开销

死锁检测由参数innodb_deadlock_detect控制(默认ON)。当并发线程非常多(数百上千)时,等待图会很大,每次检测的 DFS 开销会显著消耗 CPU。在极端高并发场景(如秒杀),可以考虑临时关闭死锁检测,依赖innodb_lock_wait_timeout来处理锁等待超时。


3. 锁等待超时参数

如果死锁检测被关闭,或者等待的锁并不构成死锁(而是长时间等待),InnoDB 通过超时机制避免事务无限等待。

关键参数:

  • innodb_lock_wait_timeout:一个事务等待行锁的最长时间(秒),默认50秒。超时后事务回滚,报错:
    ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
  • 设置过短:可能导致正常排队等待的事务被回滚(尤其在长事务场景)。
  • 设置过长:死锁时(若关闭检测)需要等很久才会被处理。

生产环境中,建议根据业务特点调整该值(如 5~20 秒),并对超时错误进行重试逻辑。


4. 如何从日志中分析死锁

当死锁发生时,InnoDB 会将死锁的详细信息记录到SHOW ENGINE INNODB STATUSLATEST DETECTED DEADLOCK部分,以及 MySQL 错误日志中。

关键信息解读

------------------------ LATEST DETECTED DEADLOCK ------------------------ 2025-06-07 10:30:00 0x7f8b2c001700 *** (1) TRANSACTION: TRANSACTION 4212345, ACTIVE 10 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s) MySQL thread id 8, OS thread handle 140234567890, query id 1234 localhost root updating UPDATE books SET stock = stock - 1 WHERE id = 1 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 5 page no 4 n bits 72 index PRIMARY of table `library_db`.`books` trx id 4212345 lock_mode X locks rec but not gap waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; ... *** (2) TRANSACTION: TRANSACTION 4212346, ACTIVE 8 sec starting index read mysql tables in use 1, locked 1 3 lock struct(s), heap size 1136, 2 row lock(s) MySQL thread id 9, OS thread handle 140234567891, query id 1235 localhost root updating UPDATE books SET stock = stock - 1 WHERE id = 2 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 5 page no 4 n bits 72 index PRIMARY of table `library_db`.`books` trx id 4212346 lock_mode X locks rec but not gap Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; ... *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 5 page no 5 n bits 72 index PRIMARY of table `library_db`.`books` trx id 4212346 lock_mode X locks rec but not gap waiting Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; ... *** WE ROLL BACK TRANSACTION (2)

解读要点:

  • (1) TRANSACTION(2) TRANSACTION分别列出了两个死锁参与者的事务 ID、执行的 SQL、持有和等待的锁。
  • HOLDS THE LOCK(S):当前持有的锁。
  • WAITING FOR THIS LOCK TO BE GRANTED:正在等待的锁。
  • 最后一句WE ROLL BACK TRANSACTION (2)说明 InnoDB 选择了事务 2 作为牺牲品。
  • lock_mode X locks rec but not gap表示记录锁(不是间隙锁)。

通过分析这两个事务持有和等待的锁,我们可以反向推导出业务逻辑哪里出现了循环等待。


5. 实战:构造死锁并分析

我们来亲手制造一个典型的死锁场景:两个事务以不同顺序更新相同的两行。

5.1 准备

USElibrary_db;CREATETABLEdeadlock_test(idINTPRIMARYKEY,valINT)ENGINE=InnoDB;INSERTINTOdeadlock_testVALUES(1,100),(2,200);

5.2 制造死锁

时间线(同时操作两个会话):

步骤会话 A会话 B
1START TRANSACTION;START TRANSACTION;
2UPDATE deadlock_test SET val=val+1 WHERE id=1;— 获得 id=1 的 X 锁
3UPDATE deadlock_test SET val=val+1 WHERE id=2;— 获得 id=2 的 X 锁
4UPDATE deadlock_test SET val=val+1 WHERE id=2;等待B 释放 id=2 的锁
5UPDATE deadlock_test SET val=val+1 WHERE id=1;等待A 释放 id=1 的锁
6死锁被检测到,其中一方回滚另一方成功执行

具体操作

会话 A

STARTTRANSACTION;UPDATEdeadlock_testSETval=val+1WHEREid=1;-- 第1步

会话 B

STARTTRANSACTION;UPDATEdeadlock_testSETval=val+1WHEREid=2;-- 第2步

会话 A

UPDATEdeadlock_testSETval=val+1WHEREid=2;-- 第3步,等待

会话 B

UPDATEdeadlock_testSETval=val+1WHEREid=1;-- 第4步,死锁触发

在几秒内(通常在步骤 4 执行后),其中一方会报错:

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

没有被回滚的一方可以正常COMMIT

5.3 分析死锁日志

立即执行:

SHOWENGINEINNODBSTATUS\G

找到LATEST DETECTED DEADLOCK部分,你会看到类似前面示例的信息,明确指出了两个事务各自持有和等待的锁,以及最终的牺牲品。

5.4 清理

DROPTABLEdeadlock_test;

6. 避免死锁的编码与设计建议

知道了死锁的成因,我们可以在设计和编码层面主动规避。

6.1 固定访问顺序

如果所有事务都按照相同的顺序访问资源(如总是先操作表 A 再操作表 B,总是先锁id=1再锁id=2),就不会形成循环等待。

实际做法

  • 对于关联表的更新,统一先更新主表,再更新子表。
  • 对于多条记录的更新,先按主键排序,再依次更新。

6.2 缩短事务

长事务持有锁的时间更长,与其他事务冲突的概率越大。应该:

  • 将非数据库操作(如远程 API 调用、文件读写)移出事务。
  • 先准备好数据,最后开启事务执行写入。
  • 避免在事务中等待用户交互。

6.3 减小锁范围

  • 使用精确的 WHERE 条件,确保走索引,避免全表扫描导致的锁膨胀。
  • 对于只读查询,使用快照读(普通SELECT)而非SELECT ... FOR SHARE
  • 在 RC 隔离级别下,间隙锁被禁用,可以降低死锁概率(但需注意幻读风险)。

6.4 使用低隔离级别

RC 隔离级别不使用间隙锁,锁范围更小,死锁概率低于 RR。对于大多数互联网业务,RC 是足够且更高效的选择。前提是应用程序能处理不可重复读,且复制格式使用 ROW 模式。

6.5 添加合适的索引

如果没有索引,一个UPDATE可能会锁住全表所有行(实际是扫描过程中对每行加锁再释放不符合条件的)。良好的索引让 InnoDB 能精确锁定目标行,大幅减少锁冲突。

6.6 重试机制

无论怎样预防,死锁仍可能发生。应用层必须实现死锁重试逻辑

  • 捕获死锁异常(SQLSTATE 40001或 error code 1213)
  • 等待一小段随机时间(退避)
  • 重新开始事务

大多数数据库框架(Spring、MyBatis 等)都提供了声明式或编程式的重试支持。


7. 小结

死锁是并发控制的阴暗面,但有规律可循:

  • 四个必要条件:互斥、持有并等待、不可剥夺、循环等待。缺一则不成立。
  • InnoDB 检测:维护等待图,DFS 发现环 → 回滚代价最小的事务。
  • 超时参数innodb_lock_wait_timeout是保底机制,防止无限等待。
  • 日志分析SHOW ENGINE INNODB STATUSLATEST DETECTED DEADLOCK包含完整死锁现场,通过“持有 + 等待”的对账可以定位问题 SQL。
  • 亲手构造:我们以不同顺序更新两行,成功触发死锁,并解读了日志。
  • 规避策略:固定访问顺序、缩短事务、精确索引、降低隔离级别、应用重试。

下一篇我们将进入MVCC 多版本并发控制,解开 InnoDB 最优雅的设计之一——无锁读背后的秘密,理解 ReadView 和版本链如何让读写互不阻塞。

思考题

  1. 如果关闭innodb_deadlock_detect,死锁会发生什么?如何被处理?
  2. 在你的系统中查看SHOW ENGINE INNODB STATUS,是否有历史死锁记录?尝试解读。
  3. 设计一个简单的转账流程(A → B,B → A 并发),分析是否可能死锁,并给出避免方案。

参考资料

  • MySQL 8.0 Reference Manual - Deadlocks in InnoDB
  • MySQL 8.0 Reference Manual - SHOW ENGINE INNODB STATUS
  • MySQL 8.0 Reference Manual - InnoDB Startup Options and System Variables

http://www.jsqmd.com/news/971439/

相关文章:

  • 智能无人机辅助V2V通信——应用于智慧城市(Matlab代码实现)
  • 尼日利亚空运清关机构口碑哪家好 - myqiye
  • 老字画怎么养护?这样存放越放越值钱 - 深鉴新闻
  • 2026年6月上海ISO三体系认证代办公司盘点:企业合规进阶必备指南
  • jQuery Mobile 导航栏
  • 传统软件公司如何转型AI Agent服务商
  • 2026年非变性二型胶原蛋白的代理商哪家靠谱 - 品牌排行榜
  • 【紧急提醒】CSDN AI营销套餐剩余权益即将清零!3步自查是否符合顺延资格,错过再等365天
  • TVA为什么是企业智能化升级的战略支点(17)
  • 基于功率分配与电压恢复的分布式二次控制研究(Simulink仿真实现)
  • 企业声誉管理选对不选贵(2026 年 6 月):四大技术流派拆解 + 高性价比服务商指南 - 玖叁鹿
  • 2026年石家庄空调移机服务推荐:5家专业公司全面盘点 - 本地品牌推荐
  • 简单理解:为什么Markdown文件比TXT文件更适合做笔记
  • 从依赖报错到CUDA加速:在Ubuntu 22.04上为OpenCV C++项目配置VSCode的完整心路历程
  • 数智赋能污水治理,视频孪生引领行业革新——黎阳之光智慧污水处理厂解决方案
  • Docker 基础实战完整指南
  • DownKyi终极指南:三步搞定B站8K视频下载与批量管理
  • 2026 沈阳防水补漏服务商口碑测评榜单|全屋渗漏维修机构优选指南 - 宅安选房屋修缮
  • 光伏电池MPPT与恒功率控制模式切换运行策略研究(Simulink仿真实现)
  • TVA为什么是企业智能化升级的战略支点(18)
  • 2026年 HC600/980QP 高强钢厂家推荐榜单:汽车轻量化QP钢核心供应商与最新选购指南 - 品牌发掘
  • Ruby MySQL 数据库操作指南
  • 【CSDN AI数字营销企业版报价解密】:20年IT采购专家亲授3步精准获取官方报价+避坑指南
  • NoFences:免费开源桌面整理神器,3分钟彻底告别Windows桌面混乱
  • 怎样高效使用百度网盘秒传技术:进阶用户的实战策略
  • BDF箱泵一体化厂家性价比实测:成都学校一站式消防泵站/成都学校不锈钢水箱/成都小区BDF装配式水箱/成都小区一体化生活泵站/选择指南 - 优质品牌商家
  • 电子元器件分销商转型:从信息差到技术增值的生存指南
  • 发电机故障暂态仿真及电压电流变化特性研究(Simulink仿真实现)
  • CSDN AI数字营销发票开具全解析(增值税专用发票支持条件首次官方披露)
  • 终极指南:深度解析OpenCore Legacy Patcher资源包处理与系统优化