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

MySQL 死锁产生原因与避免

我刚工作的时候有个批量转账的需求要从 A 账户转钱给 B 账户同时从 B 账户转钱给 A 账户。结果上线后频繁死锁用户投诉电话被打爆。今天咱们就来聊聊 MySQL 死锁的产生原因与避免看完这篇你就能设计和排查无死锁的并发系统了。死锁是啥死锁Deadlock指的是两个或多个事务互相等待对方释放锁导致谁也执行不下去。经典场景时间轴 T1: 事务 A 持有锁 X等待锁 Y T2: 事务 B 持有锁 Y等待锁 X T3: 死锁谁也执行不下去MySQL 的处理检测到死锁后回滚其中一个事务另一个事务继续执行。死锁的四个必要条件死锁的产生需要同时满足四个条件互斥Mutual Exclusion锁只能被一个事务持有占有并等待Hold and Wait事务持有锁 X还要等待锁 Y不可抢占No Preemption锁只能自动释放COMMIT/ROLLBACK不能被强制剥夺循环等待Circular WaitA 等 BB 等 A只要打破其中一个条件就能避免死锁。死锁的常见场景场景 1不同顺序加锁最常见的死锁场景两个事务以不同顺序加锁。事故现场-- 会话 ABEGIN;UPDATEaccountsSETbalancebalance-100WHEREid1;-- 锁住 id1-- 会话 BBEGIN;UPDATEaccountsSETbalancebalance-50WHEREid2;-- 锁住 id2-- 会话 A等待会话 B 释放 id2UPDATEaccountsSETbalancebalance100WHEREid2;-- 阻塞-- 会话 B等待会话 A 释放 id1死锁UPDATEaccountsSETbalancebalance50WHEREid1;-- 死锁结果MySQL 检测到死锁回滚其中一个事务。解决方案 1按固定顺序加锁思路所有事务都按相同的顺序加锁比如按 ID 升序。-- 优化后都按 ID 升序加锁-- 会话 ABEGIN;UPDATEaccountsSETbalancebalance-100WHEREid1;-- 先锁 id1UPDATEaccountsSETbalancebalance100WHEREid2;-- 再锁 id2-- 会话 B也要按 ID 升序BEGIN;UPDATEaccountsSETbalancebalance-50WHEREid1;-- 也要先锁 id1等会话 A 释放-- 不会死锁因为顺序一样为什么能避免不会形成循环等待都先锁 id1再锁 id2。解决方案 2用SELECT ... FOR UPDATE一次性锁住思路用SELECT ... FOR UPDATE一次性锁住所有要修改的行。-- 优化后一次性锁住-- 会话 ABEGIN;SELECTbalanceFROMaccountsWHEREidIN(1,2)FORUPDATE;-- 一次性锁住 id1 和 id2UPDATEaccountsSETbalancebalance-100WHEREid1;UPDATEaccountsSETbalancebalance100WHEREid2;COMMIT;-- 会话 B也要一次性锁住BEGIN;SELECTbalanceFROMaccountsWHEREidIN(1,2)FORUPDATE;-- 一次性锁住等会话 A 释放-- 不会死锁为什么能避免一次性锁住所有行不会占有并等待。场景 2索引没走锁全表经典事故WHERE条件没走索引导致锁全表InnoDB 的行锁退化成表锁。事故现场-- age 没有索引BEGIN;UPDATEusersSETage30WHEREage25;-- 锁全表-- 其他会话阻塞UPDATEusersSETage40WHEREid1;-- 阻塞结果所有对该表的操作都阻塞形成伪死锁不是真正的死锁但表现一样。解决方案给 WHERE 条件加索引思路确保WHERE条件走索引只锁符合条件的行。-- 优化后给 age 加索引CREATEINDEXidx_ageONusers(age);BEGIN;UPDATEusersSETage30WHEREage25;-- 只锁 age25 的行-- 其他会话不阻塞UPDATEusersSETage40WHEREid1;-- 不阻塞场景 3大事务持锁太久经典事故事务里调用了外部 API耗时 5 秒导致持锁太久其他事务都被阻塞。事故现场BEGIN;UPDATEaccountsSETbalancebalance-100WHEREid1;-- 锁住 id1-- 调用外部 API耗时 5 秒持锁 5 秒CALLexternal_api();COMMIT;结果其他要操作id1的事务都被阻塞 5 秒。解决方案缩小事务范围思路事务里只做数据库操作不要做耗时操作比如调用外部 API。-- 优化后先调用外部 API再开事务CALLexternal_api();-- 不持锁BEGIN;UPDATEaccountsSETbalancebalance-100WHEREid1;COMMIT;-- 持锁时间极短场景 4间隙锁Gap Lock导致死锁InnoDB 的间隙锁在 REPEATABLE READ 隔离级别下会用间隙锁防止幻读。事故现场-- 假设 age 有索引值是20, 25, 30-- 会话 ABEGIN;UPDATEusersSETage26WHEREage25ANDage30;-- 锁住 (25, 30) 的间隙-- 会话 BBEGIN;UPDATEusersSETage24WHEREage20ANDage25;-- 锁住 (20, 25) 的间隙-- 会话 A等待会话 B 释放 (20, 25) 的间隙锁死锁INSERTINTOusers(age)VALUES(22);-- 会话 B等待会话 A 释放 (25, 30) 的间隙锁死锁INSERTINTOusers(age)VALUES(27);结果MySQL 检测到死锁回滚其中一个事务。解决方案 1缩小间隙锁范围思路尽量用精确匹配不要用范围查询、、BETWEEN。-- 优化后用精确匹配-- 会话 ABEGIN;UPDATEusersSETage26WHEREid5;-- 精确匹配只锁 id5-- 会话 BBEGIN;UPDATEusersSETage24WHEREid3;-- 精确匹配只锁 id3解决方案 2用 READ COMMITTED 隔离级别思路READ COMMITTED 隔离级别不用间隙锁只锁已存在的行。-- 优化后用 READ COMMITTEDSETSESSIONTRANSACTIONISOLATIONLEVELREADCOMMITTED;BEGIN;UPDATEusersSETage26WHEREage25ANDage30;-- 不用间隙锁注意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执行UPDATE users SET age 30 WHERE id 1在等待id2的锁事务 2执行UPDATE users SET age 40 WHERE id 2在等待id1的锁死锁MySQL 回滚了事务 2第 2 步分析死锁原因根据SHOW ENGINE INNODB STATUS的输出分析哪两个事务互相等待各自持有了什么锁在等待什么锁SQL 是什么为什么会产生死锁第 3 步优化 SQL 或事务根据分析结果优化按固定顺序加锁给 WHERE 条件加索引缩小事务范围缩小间隙锁范围死锁的避免1. 按固定顺序加锁最重要这是最重要的建议。所有事务都按相同的顺序加锁。-- 错误不同顺序加锁可能死锁-- 事务 A先锁 id1再锁 id2-- 事务 B先锁 id2再锁 id1-- 正确都按 ID 升序加锁-- 事务 A先锁 id1再锁 id2-- 事务 B也要先锁 id1再锁 id22. 给 WHERE 条件加索引确保WHERE条件走索引只锁符合条件的行不锁全表。-- 错误没走索引锁全表UPDATEusersSETage30WHEREage25;-- age 没索引-- 正确走索引只锁符合条件的行CREATEINDEXidx_ageONusers(age);UPDATEusersSETage30WHEREage25;-- 走索引3. 缩小事务范围事务里只做数据库操作不要做耗时操作比如调用外部 API。-- 错误事务里调用外部 API持锁太久BEGIN;UPDATEaccountsSETbalancebalance-100WHEREid1;CALLexternal_api();-- 持锁 5 秒COMMIT;-- 正确先调用外部 API再开事务CALLexternal_api();-- 不持锁BEGIN;UPDATEaccountsSETbalancebalance-100WHEREid1;COMMIT;-- 持锁时间极短4. 用SELECT ... FOR UPDATE一次性锁住用SELECT ... FOR UPDATE一次性锁住所有要修改的行不会占有并等待。-- 错误分步加锁可能死锁BEGIN;UPDATEaccountsSETbalancebalance-100WHEREid1;-- 先锁 id1UPDATEaccountsSETbalancebalance100WHEREid2;-- 再锁 id2可能死锁-- 正确一次性锁住BEGIN;SELECTbalanceFROMaccountsWHEREidIN(1,2)FORUPDATE;-- 一次性锁住UPDATEaccountsSETbalancebalance-100WHEREid1;UPDATEaccountsSETbalancebalance100WHEREid2;5. 设置锁等待超时时间设置innodb_lock_wait_timeout默认 50 秒避免无限等待。-- 设置锁等待超时为 5 秒SETGLOBALinnodb_lock_wait_timeout5;-- 或者修改配置文件永久生效-- my.cnf:[mysqld]innodb_lock_wait_timeout5如果等待超过 5 秒MySQL 会报错Lock wait timeout exceeded; try restarting transaction。6. 捕获死锁异常自动重试在应用层捕获死锁异常Deadlock found when trying to get lock自动重试。// 伪代码publicvoidtransfer(intfromUserId,inttoUserId,BigDecimalamount){intretryCount0;while(retryCount3){// 最多重试 3 次try{// 开启事务BEGIN;// 转账逻辑UPDATEaccountsSETbalancebalance-amountWHEREuser_idfromUserId;UPDATEaccountsSETbalancebalanceamountWHEREuser_idtoUserId;// 提交事务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.rkmt.cn/news/1385205.html

相关文章:

  • Hugging Face 中tokenizer.json 和vocab.json 有区别?
  • AI 充电枪智能功率 MOSFET 完整选型方案
  • 玩转Hermes Agent|使用Lighthouse快速部署云上Hermes Agent-周红伟
  • 如何精准控制20QPS测试百度首页
  • 企业数据安全方案有哪些:2026年从风险评估到落地的完整指南 - 华旭传媒
  • 博弈论导向的车辆队列运动协同分层控制算法【附算法】
  • 企业级AI语音合成采购决策白皮书(2024真实报价单首次公开)
  • RTX51 Tiny内存冲突与ISD51调试器解决方案
  • 精准测试落地难?我用半年实践总结出这4条铁律
  • 机器学习入门:理解线性回归与逻辑(简化且附Python实战代码)
  • 2026年金华为餐饮企业提供SAAS收银系统的服务商综合分析与适配指南 - 万事通达
  • Claude code 接入 deepseek-v4-pro setting 文件配置
  • HTTP与HTTPS超详解:协议流程、报文结构、HTTPS加密、各版本区别、面试
  • Visual C++运行库合集终极指南:一键解决Windows应用程序依赖问题
  • ​用于雷达系统设计的 MATLAB 仿真附matlab代码
  • 2026软考中级软件设计师_考后分享
  • 基于GMR传感器的DIY示波器电流钳探头设计与实现
  • 暗黑破坏神2存档修改器:Diablo Edit2让你的游戏体验随心所欲
  • 打不开JupyterLab
  • 荣耀出征官网下载:1.03H经典副本复刻,高阶装备稳定掉落
  • 【DeepSeek性能测试黄金法则】:20年专家亲授5大避坑指南与实测调优参数清单
  • DeepSeek代码签名验证形同虚设?——用eBPF+Sigstore构建不可绕过的100%可信执行链(含生产环境一键部署脚本)
  • Claude端到端测试设计:从零搭建可审计、可回放、可量化的AI服务测试流水线(含开源Schema校验工具)
  • HDI 高密度互连板阶数的深度理解
  • DMA使用心得-STM32
  • 搜维尔科技:Xsens动作捕捉在人形机器人研发中的应用
  • 光轮智能 谢晨 访谈总结机器人仿真数据产业
  • 轻量化部署,异地机房快速接入,多机房管理不用再大动干戈
  • 基于ATtiny84的智能冰箱监控器:低功耗温度与门状态监测方案
  • 2026年平价国风键帽测评:浮光窑开PBT键帽深度解析