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

MySQL 死锁产生原因与避免

我刚工作的时候,有个批量转账的需求:要从 A 账户转钱给 B 账户,同时从 B 账户转钱给 A 账户。结果上线后频繁死锁,用户投诉电话被打爆。

今天咱们就来聊聊 MySQL 死锁的产生原因与避免,看完这篇,你就能设计和排查无死锁的并发系统了。

死锁是啥?

死锁(Deadlock)指的是两个或多个事务互相等待对方释放锁,导致谁也执行不下去。

经典场景

时间轴: T1: 事务 A 持有锁 X,等待锁 Y T2: 事务 B 持有锁 Y,等待锁 X T3: 死锁!谁也执行不下去

MySQL 的处理:检测到死锁后,回滚其中一个事务,另一个事务继续执行。

死锁的四个必要条件

死锁的产生需要同时满足四个条件:

  1. 互斥(Mutual Exclusion):锁只能被一个事务持有
    1. 占有并等待(Hold and Wait):事务持有锁 X,还要等待锁 Y
    1. 不可抢占(No Preemption):锁只能自动释放(COMMIT/ROLLBACK),不能被强制剥夺
    1. 循环等待(Circular Wait):A 等 B,B 等 A
      只要打破其中一个条件,就能避免死锁。

死锁的常见场景

场景 1:不同顺序加锁

最常见的死锁场景:两个事务以不同顺序加锁。

事故现场
-- 会话 ABEGIN;UPDATEaccountsSETbalance=balance-100WHEREid=1;-- 锁住 id=1-- 会话 BBEGIN;UPDATEaccountsSETbalance=balance-50WHEREid=2;-- 锁住 id=2-- 会话 A(等待会话 B 释放 id=2)UPDATEaccountsSETbalance=balance+100WHEREid=2;-- 阻塞!-- 会话 B(等待会话 A 释放 id=1,死锁!)UPDATEaccountsSETbalance=balance+50WHEREid=1;-- 死锁!

结果:MySQL 检测到死锁,回滚其中一个事务。

解决方案 1:按固定顺序加锁

思路:所有事务都按相同的顺序加锁(比如按 ID 升序)。

-- 优化后:都按 ID 升序加锁-- 会话 ABEGIN;UPDATEaccountsSETbalance=balance-100WHEREid=1;-- 先锁 id=1UPDATEaccountsSETbalance=balance+100WHEREid=2;-- 再锁 id=2-- 会话 B(也要按 ID 升序)BEGIN;UPDATEaccountsSETbalance=balance-50WHEREid=1;-- 也要先锁 id=1(等会话 A 释放)-- 不会死锁!因为顺序一样

为什么能避免?不会形成循环等待(都先锁 id=1,再锁 id=2)。

解决方案 2:用SELECT ... FOR UPDATE一次性锁住

思路:用SELECT ... FOR UPDATE一次性锁住所有要修改的行。

-- 优化后:一次性锁住-- 会话 ABEGIN;SELECTbalanceFROMaccountsWHEREidIN(1,2)FORUPDATE;-- 一次性锁住 id=1 和 id=2UPDATEaccountsSETbalance=balance-100WHEREid=1;UPDATEaccountsSETbalance=balance+100WHEREid=2;COMMIT;-- 会话 B(也要一次性锁住)BEGIN;SELECTbalanceFROMaccountsWHEREidIN(1,2)FORUPDATE;-- 一次性锁住(等会话 A 释放)-- 不会死锁!

为什么能避免?一次性锁住所有行,不会占有并等待。

场景 2:索引没走,锁全表

经典事故WHERE条件没走索引,导致锁全表(InnoDB 的行锁退化成表锁)。

事故现场
-- age 没有索引BEGIN;UPDATEusersSETage=30WHEREage=25;-- 锁全表!-- 其他会话(阻塞!)UPDATEusersSETage=40WHEREid=1;-- 阻塞!

结果:所有对该表的操作都阻塞,形成"伪死锁"(不是真正的死锁,但表现一样)。

解决方案:给 WHERE 条件加索引

思路:确保WHERE条件走索引,只锁符合条件的行。

-- 优化后:给 age 加索引CREATEINDEXidx_ageONusers(age);BEGIN;UPDATEusersSETage=30WHEREage=25;-- 只锁 age=25 的行-- 其他会话(不阻塞)UPDATEusersSETage=40WHEREid=1;-- 不阻塞!

场景 3:大事务持锁太久

经典事故:事务里调用了外部 API(耗时 5 秒),导致持锁太久,其他事务都被阻塞。

事故现场
BEGIN;UPDATEaccountsSETbalance=balance-100WHEREid=1;-- 锁住 id=1-- 调用外部 API(耗时 5 秒,持锁 5 秒!)CALLexternal_api();COMMIT;

结果:其他要操作id=1的事务,都被阻塞 5 秒。

解决方案:缩小事务范围

思路:事务里只做数据库操作,不要做耗时操作(比如调用外部 API)。

-- 优化后:先调用外部 API,再开事务CALLexternal_api();-- 不持锁BEGIN;UPDATEaccountsSETbalance=balance-100WHEREid=1;COMMIT;-- 持锁时间极短

场景 4:间隙锁(Gap Lock)导致死锁

InnoDB 的间隙锁:在 REPEATABLE READ 隔离级别下,会用间隙锁防止幻读。

事故现场
-- 假设 age 有索引,值是:20, 25, 30-- 会话 ABEGIN;UPDATEusersSETage=26WHEREage>25ANDage<30;-- 锁住 (25, 30) 的间隙-- 会话 BBEGIN;UPDATEusersSETage=24WHEREage>20ANDage<25;-- 锁住 (20, 25) 的间隙-- 会话 A(等待会话 B 释放 (20, 25) 的间隙锁,死锁!)INSERTINTOusers(age)VALUES(22);-- 会话 B(等待会话 A 释放 (25, 30) 的间隙锁,死锁!)INSERTINTOusers(age)VALUES(27);

结果:MySQL 检测到死锁,回滚其中一个事务。

解决方案 1:缩小间隙锁范围

思路:尽量用精确匹配=),不要用范围查询(><BETWEEN)。

-- 优化后:用精确匹配-- 会话 ABEGIN;UPDATEusersSETage=26WHEREid=5;-- 精确匹配,只锁 id=5-- 会话 BBEGIN;UPDATEusersSETage=24WHEREid=3;-- 精确匹配,只锁 id=3
解决方案 2:用 READ COMMITTED 隔离级别

思路:READ COMMITTED 隔离级别不用间隙锁,只锁已存在的行。

-- 优化后:用 READ COMMITTEDSETSESSIONTRANSACTIONISOLATIONLEVELREADCOMMITTED;BEGIN;UPDATEusersSETage=26WHEREage>25ANDage<30;-- 不用间隙锁

注意:READ COMMITTED 可能有不可重复读的问题。

死锁的排查

第 1 步:查看最近一次死锁

SHOWENGINEINNODBSTATUS;

重点看LATEST DETECTED DEADLOCK部分。

输出解读
------------------------ LATEST DETECTED DEADLOCK ------------------------ 2024-01-15 10:30:00 0x7f8a1b2c1700 *** (1) TRANSACTION: TRANSACTION 123456, ACTIVE 2 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 10, OS thread handle 140237518456320, query id 98 localhost root updating UPDATE users SET age = 30 WHERE id = 1 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 2 page no 3 n bits 72 index PRIMARY of test.users trx id 123456 lock_mode X locks rec but not gap waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 4; hex 80000001; asc ii..; (1); 1: len 6; hex 000000000001; asc .........;; 2: len 7; hex 80000000000000; asc .........;; 3: len 4; hex 8000001e; asc .........; (30); *** (2) TRANSACTION: TRANSACTION 123457, ACTIVE 2 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 11, OS thread handle 140237518456321, query id 99 localhost root updating UPDATE users SET age = 40 WHERE id = 2 *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 2 page no 3 n bits 72 index PRIMARY of test.users trx id 123457 lock_mode X locks rec but not gap waiting Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 4; hex 80000002; asc ii..; (2); 1: len 6; hex 000000000002; asc .........;; 2: len 7; hex 80000000000000; asc .........;; 3: len 4; hex 80000028; asc .........; (40); *** WE ROLL BACK TRANSACTION (2)

关键信息

  1. 事务 1:执行UPDATE users SET age = 30 WHERE id = 1,在等待id=2的锁
    1. 事务 2:执行UPDATE users SET age = 40 WHERE id = 2,在等待id=1的锁
    1. 死锁!MySQL 回滚了事务 2

第 2 步:分析死锁原因

根据SHOW ENGINE INNODB STATUS的输出,分析:

  1. 哪两个事务互相等待?
    1. 各自持有了什么锁?在等待什么锁?
    1. SQL 是什么?为什么会产生死锁?

第 3 步:优化 SQL 或事务

根据分析结果,优化:

  1. 按固定顺序加锁
    1. 给 WHERE 条件加索引
    1. 缩小事务范围
    1. 缩小间隙锁范围

死锁的避免

1. 按固定顺序加锁(最重要!)

这是最重要的建议。所有事务都按相同的顺序加锁。

-- 错误:不同顺序加锁(可能死锁)-- 事务 A:先锁 id=1,再锁 id=2-- 事务 B:先锁 id=2,再锁 id=1-- 正确:都按 ID 升序加锁-- 事务 A:先锁 id=1,再锁 id=2-- 事务 B:也要先锁 id=1,再锁 id=2

2. 给 WHERE 条件加索引

确保WHERE条件走索引,只锁符合条件的行,不锁全表。

-- 错误:没走索引,锁全表UPDATEusersSETage=30WHEREage=25;-- age 没索引-- 正确:走索引,只锁符合条件的行CREATEINDEXidx_ageONusers(age);UPDATEusersSETage=30WHEREage=25;-- 走索引

3. 缩小事务范围

事务里只做数据库操作,不要做耗时操作(比如调用外部 API)。

-- 错误:事务里调用外部 API(持锁太久)BEGIN;UPDATEaccountsSETbalance=balance-100WHEREid=1;CALLexternal_api();-- 持锁 5 秒!COMMIT;-- 正确:先调用外部 API,再开事务CALLexternal_api();-- 不持锁BEGIN;UPDATEaccountsSETbalance=balance-100WHEREid=1;COMMIT;-- 持锁时间极短

4. 用SELECT ... FOR UPDATE一次性锁住

SELECT ... FOR UPDATE一次性锁住所有要修改的行,不会占有并等待。

-- 错误:分步加锁(可能死锁)BEGIN;UPDATEaccountsSETbalance=balance-100WHEREid=1;-- 先锁 id=1UPDATEaccountsSETbalance=balance+100WHEREid=2;-- 再锁 id=2(可能死锁)-- 正确:一次性锁住BEGIN;SELECTbalanceFROMaccountsWHEREidIN(1,2)FORUPDATE;-- 一次性锁住UPDATEaccountsSETbalance=balance-100WHEREid=1;UPDATEaccountsSETbalance=balance+100WHEREid=2;

5. 设置锁等待超时时间

设置innodb_lock_wait_timeout(默认 50 秒),避免无限等待。

-- 设置锁等待超时为 5 秒SETGLOBALinnodb_lock_wait_timeout=5;-- 或者修改配置文件(永久生效)-- my.cnf:[mysqld]innodb_lock_wait_timeout=5

如果等待超过 5 秒,MySQL 会报错:Lock wait timeout exceeded; try restarting transaction

6. 捕获死锁异常,自动重试

在应用层捕获死锁异常Deadlock found when trying to get lock),自动重试。

// 伪代码publicvoidtransfer(intfromUserId,inttoUserId,BigDecimalamount){intretryCount=0;while(retryCount<3){// 最多重试 3 次try{// 开启事务BEGIN;// 转账逻辑UPDATEaccountsSETbalance=balance-amountWHEREuser_id=fromUserId;UPDATEaccountsSETbalance=balance+amountWHEREuser_id=toUserId;// 提交事务COMMIT;// 成功,退出break;}catch(DeadlockExceptione){// 死锁异常,回滚,重试ROLLBACK;retryCount++;// 等待随机时间(避免再次冲突)Thread.sleep(newRandom().nextInt(1000));}}}``` ## 总结-**死锁**是两个或多个事务**互相等待对方释放锁**,导致谁也执行不下去--**死锁的四个必要条件**:互斥、占有并等待、不可抢占、循环等待--**死锁的常见场景**-1.**不同顺序加锁**(最常见)-2.**索引没走,锁全表**-3.**大事务持锁太久**-4.**间隙锁(GapLock)导致死锁**--**死锁的排查**:`SHOWENGINEINNODBSTATUS;`(看 `LATESTDETECTEDDEADLOCK` 部分)--**死锁的避免**-1.**按固定顺序加锁**(最重要!)-2.**WHERE条件加索引**-3.**缩小事务范围**-4.**用 `SELECT...FORUPDATE` 一次性锁住**-5.**设置锁等待超时时间**-6.**捕获死锁异常,自动重试**如果你能把死锁的四个必要条件、常见场景、排查方法、避免方案讲清楚,面试官绝对觉得你是高级开发。---**实战代码都在我本地跑过,你可以放心复制。**如果有问题,欢迎评论区交流!
http://www.jsqmd.com/news/886729/

相关文章:

  • 安全测试入门:每个开发都应该知道的10个常见漏洞
  • SMUDebugTool终极指南:如何深度掌控AMD Ryzen处理器的隐藏性能
  • 中兴光猫终极管理指南:解锁工厂模式与Telnet权限的实战教程
  • 如何进行TVA仿真引擎的“光照地狱”训练?
  • rk35xx 通过recovery升级问题
  • ssm高校推免报名系统(10102)
  • 企业级AI语音合成采购决策白皮书(2024真实报价单首次公开)
  • 本地Windows容器迁移至云服务器
  • 【MySQL数据库 | 第一篇】 概述
  • # AI音乐生成API的可控性与专业化演进研究
  • 配置OpenClaw Agent使用Taotoken作为后端模型提供商
  • 【Qwen3.6】关键技术:线性注意力(Linear Attention/DeltaNet)和标准多头注意力(Standard Attention)混合
  • 2024年网盘下载终极免费解决方案:八大平台直链解析技术深度解析
  • Windows终极PDF处理工具:3步免费安装Poppler完整指南
  • 如何处理AI生成代码中的错误
  • 5分钟搭建原神私服:KCN-GenshinServer终极图形化解决方案
  • DeepSeek幻觉问题深度复盘(2023–2024真实故障库首发):从token级偏差到语义坍塌的全链路溯源
  • Owl-Alpha 新手快速上手指南
  • LSTM 算法的完整计算过程
  • MySQL GROUP BY 原理与优化
  • 基于双T振荡器的正弦波LED调光电路设计与实践
  • Linux系统Vim编辑器
  • 你的企业还在用“人海战术”处理发票和报表?2026智能体进化论
  • 别再死磕理论了!用Python手搓一个蒙特卡洛强化学习小游戏(附完整代码)
  • pan-baidu-download:百度网盘多线程下载加速器架构解析与性能优化指南
  • 【绝密PEST压力测试报告】:Claude 3.5在金融/医疗/政务三大敏感领域的17项穿透式评估结果(仅剩最后87份)
  • 边缘AI落地总失败?DeepSeek架构的4层容错机制,92%故障在毫秒级自愈
  • DeepSeek多卡训练通信开销超62%?紧急发布:NCCL拓扑感知AllReduce重排+梯度压缩阈值动态调优指南
  • Neon Glowing效果失效全解析,深度解读--v 6.2下--style raw与--no ambient_light的冲突机制及绕过方案
  • 面试必问:Temperature=0为何仍不确定?真相揭秘