当前位置: 首页 > 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.gsyq.cn/news/1483169.html

相关文章:

  • 尼日利亚空运清关机构口碑哪家好 - myqiye
  • 智能无人机辅助V2V通信——应用于智慧城市(Matlab代码实现)
  • 2026年非变性二型胶原蛋白的代理商哪家靠谱 - 品牌排行榜
  • 简单理解:为什么Markdown文件比TXT文件更适合做笔记
  • 2026年石家庄空调移机服务推荐:5家专业公司全面盘点 - 本地品牌推荐
  • 从依赖报错到CUDA加速:在Ubuntu 22.04上为OpenCV C++项目配置VSCode的完整心路历程
  • TVA为什么是企业智能化升级的战略支点(18)
  • CSDN AI数字营销发票开具全解析(增值税专用发票支持条件首次官方披露)
  • 发电机故障暂态仿真及电压电流变化特性研究(Simulink仿真实现)
  • 电子元器件分销商转型:从信息差到技术增值的生存指南
  • 基于显式拓扑变量可靠性评估的双Q交直流混合配电网优化规划研究(Python代码实现)
  • 重磅汇总!2026AI写作辅助平台榜单(覆盖 99% 毕业生论文需求)
  • Spring AI 从入门到精通-Prompt 工程
  • 今日开源[第10期]ds4(DwarfStar) - zhang
  • Spring AI 从入门到精通-结构化输出
  • 2026年网架厂家推荐榜单:体育馆网架、煤棚网架、大跨度网架与螺栓球网架标杆品牌深度解析 - 品牌发掘
  • CAP定理(又称布鲁尔定理)指出:在分布式系统中,**一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)**
  • 手把手教你用Overleaf搞定IEEE会议论文(附CAC投稿避坑指南)
  • 水泵隔音降噪技术全解析:新能源噪音治理、新能源隔音降噪、机房噪音治理、水泵房噪音治理、车间噪音治理、车间隔音降噪选择指南 - 优质品牌商家
  • 德国酷贝漆靠谱吗? - myqiye
  • 工业用吸尘器厂家佛山排行榜2026:史沃斯稳居第一 - 工业清洁测评社
  • 关于下载pip install faiss-cpu失败的问题
  • 企业品牌声誉管理靠谱之选(2026年6月):AI舆情监测/危机处置/声誉修复三大技术流派全攻略 - 玖叁鹿
  • 5个突破性功能:重新定义英雄联盟游戏体验的一站式解决方案
  • 动平衡机靠谱品牌,中联试验机的口碑怎样 - mypinpai
  • 手绘字画和印刷字画怎么分?一招不再买错 - 深鉴新闻
  • 2026年薄型气凝胶价格,哪家更实惠 - mypinpai
  • 为什么Flameshot成为开发者最爱的开源截图工具?探索其技术架构与高效工作流
  • 语音钓鱼引发的数据泄露事件溯源与全域防御研究
  • 基于 Harmony 6.0 应用的孕期管理助手实现