尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MySQL死锁排查与治理:从原理到实战,告别重启服务

MySQL死锁排查与治理:从原理到实战,告别重启服务 一次线上故障复盘往往会暴露团队对数据库并发的真实理解水平。前阵子一个朋友遇到的情况很典型线上 MySQL 某个核心表突然频繁出现锁等待超时用户下单偶尔报错监控里Deadlock found的记录开始冒出来。团队内部一讨论就有人提了句“要不重启一下服务试试”。这个回答在面试场景里会被直接判负在真实生产环境里更是会把问题拖得更严重。重启服务确实能清掉一部分连接和事务状态但死锁的本质是事务之间的资源竞争顺序出了问题。进程一重启事务确实没了但表结构、索引、业务请求模式、事务隔离级别、SQL 执行计划全都还在。只要触发死锁的流量模式还在重启之后大概率还会复现。这就像电梯超载报警后把门关一下再打开看似解决了实际上载重问题根本没变。这篇文章想聊清楚三件事MySQL 死锁到底是怎么产生的线上出现死锁时正确的排查顺序是什么以及从长期看怎么通过索引、事务设计和批量更新策略降低死锁概率。如果你正准备 Java 面试这篇文章也能帮你把“死锁”从背概念升级成一套可复述、可推导的实战答案。1. 先搞清楚死锁是什么以及它和“慢查询”“锁等待”的区别很多候选人一听到死锁第一反应是背死锁四要素互斥、持有并等待、不可剥夺、循环等待。这没错但面试官真正想听的往往不是这四句口诀而是你能不能把这些概念落到 MySQL 的锁机制里。1.1 死锁不是“卡住”而是多个事务互相等对方手里的锁先给死锁一个更具体的定义两个或多个事务各自持有部分资源锁又在等待对方持有的锁形成循环等待。MySQL 的 InnoDB 引擎检测到死锁后会选择一个代价较小的事务回滚另一个事务继续执行。这也是为什么我们经常看到报错信息里带Deadlock found when trying to get lock; try restarting transaction。为了把话说清楚我用一个最简单的例子来模拟场景。假设一张订单表order_info主键id还有一个业务编号字段order_no上建了唯一索引。事务 A 执行UPDATE order_info SET status PAID WHERE order_no NO1001; UPDATE order_info SET status PAID WHERE order_no NO1002;事务 B 执行UPDATE order_info SET status SHIPPED WHERE order_no NO1002; UPDATE order_info SET status SHIPPED WHERE order_no NO1001;如果事务 A 先锁住了NO1001再去申请NO1002的锁事务 B 反过来先锁住了NO1002再去申请NO1001的锁两边就谁也不让谁。InnoDB 的锁检测机制会发现这个循环等待然后回滚其中一个事务。这就是典型的“两个事务按相反顺序更新记录”。1.2 但线上大多数死锁根本不是两条 UPDATE 这么简单真实场景里两个事务互相等待的往往不是单条记录而是一组记录。比如事务 A 先按user_id查出一批订单逐条更新事务 B 先按shop_id查出一批订单逐条更新两个事务的更新集合有交集但加锁顺序完全不同。还有一种高频场景是批量更新和单条更新混跑。某个定时任务会按条件批量更新一个范围内的订单比如WHERE status PENDING同时用户请求会对单条订单发起更新。当批量任务先锁住了一些记录用户请求要更新其中一条而批量任务又需要申请另一条用户已经锁住的记录时也可能形成循环。所以理解死锁不能停留在“两个事务互相等锁”这个直觉层面要延伸到“锁的粒度、加锁顺序、索引选择”这些细节里。1.3 分清“死锁”“锁等待”和“慢查询”排查方向完全不同线上数据库出现异常时很多人会把死锁、锁等待、慢查询混为一谈。这会导致排查方向完全跑偏。死锁事务之间形成循环等待InnoDB 会主动回滚一个事务来打破循环。报错信息里通常有Deadlock found。锁等待超时一个事务在等待另一个事务释放锁等的时间超过了innodb_lock_wait_timeout的阈值默认 50 秒然后报错。报错信息通常是Lock wait timeout exceeded; try restarting transaction。慢查询SQL 执行时间超过long_query_time可能因为没有走索引、扫描行数太多、表数据量太大或者 CPU/IO 资源紧张。慢查询本身不一定涉及锁但慢事务长期持有锁会加剧锁等待甚至死锁。用一句话概括慢查询是“做得慢”锁等待是“等着别人做完”死锁是“大家一起等谁也做不完”。排查询问题时优先确认报错关键字再选择对应排查工具。方向错了后面所有操作都是在浪费时间。2. 排查死锁的正确姿势不是重启而是按顺序找证据回到开头那个场景。如果线上真的出现死锁第一件事肯定不是重启服务而是先收集证据。2.1 第一步先用 SQL 看当前锁等待和事务状态最直接的命令是SHOW ENGINE INNODB STATUS它能输出当前 InnoDB 引擎内部的事务、锁和死锁信息。在死锁发生后这个命令的LATEST DETECTED DEADLOCK部分会记录最近一次死锁的详细信息包括两个事务各自执行的 SQL、持有的锁、等待的锁以及被回滚的事务。不过要注意一点SHOW ENGINE INNODB STATUS里的死锁信息只会保留最近一次。如果死锁发生频率较高你只看到最后一次不一定能覆盖完整规律。所以更稳妥的做法是开启死锁日志SET GLOBAL innodb_print_all_deadlocks ON;开启后所有死锁信息都会写入 MySQL 错误日志方便后续分析。同时可以查询information_schema.INNODB_TRX查看当前正在运行的事务SELECT * FROM information_schema.INNODB_TRX\G这个表会显示事务 ID、状态、开始时间、执行的 SQL如果有、锁等待时间等字段。当线上出现锁等待超时时这张表的排查价值特别高能快速定位到“谁拿着锁不释放”。2.2 第二步把死锁信息里的表和 SQL 抓出来分析加锁顺序有了死锁日志不要急着改代码。先把日志里涉及的表、SQL、索引信息整理出来然后回答几个问题两个事务分别更新了哪些表每个事务内部按什么顺序加锁每个 UPDATE / DELETE 语句走了哪个索引是全表扫描还是走唯一索引/普通索引两个事务加锁集合是否有交集触发死锁的并发量大概是多少是不是某个大促、定时任务或批量导入期间举个例子。假设死锁日志里事务 1 执行了UPDATE order_info SET status ? WHERE order_no ?事务 2 执行了UPDATE order_info SET status ? WHERE id ?。表面看两者没有明显冲突但实际可能因为order_no上没有索引事务 1 的 UPDATE 变成了全表扫描锁了大量记录导致事务 2 按主键更新某条记录时也受影响。这就是一个很常见的坑UPDATE 语句的 WHERE 条件如果没有走索引InnoDB 会锁住扫描过程中访问到的所有记录而不是只锁目标记录。于是你以为只是在更新一条数据实际却锁了一片数据死锁和锁等待的概率都会上升。2.3 第三步查看表结构和执行计划确认索引是否真的被用上了如果死锁日志里涉及 UPDATE 或 DELETE必须用EXPLAIN查看执行计划确认是否走了正确索引。EXPLAIN SELECT * FROM order_info WHERE order_no NO1001;执行计划里重点看type和key字段。type如果是ALL说明全表扫描锁范围会很大type如果是ref或const说明走了普通索引或唯一索引锁范围会小很多。还有一个需要留意的点MySQL 的优化器不一定总按你认为的索引执行。尤其是在多列索引、复合条件、数据分布不均匀的情况下优化器可能选择另一个更“便宜”的索引结果加锁范围和你预期不一致。所以不要只在建表时确认索引存在还要在真实数据量下用EXPLAIN复查。2.4 第四步别急着处理“结果”先看事务上下文和隔离级别死锁日志里有时只能看到 SQL但问题往往出在事务上下文。比如一个 Java 方法里执行了多次 SQL 更新但事务是在 Service 层通过Transactional控制的。前半段更新表 A后半段更新表 B另一个方法恰好反过来。这种情况下两个事务在表 A 和表 B 上的加锁顺序不一致就会形成死锁。排查时要回到代码里看事务边界事务里包含几条 SQL这些 SQL 的执行顺序是什么事务里有没有远程调用、外部 HTTP 请求、文件操作等耗时动作事务里有没有先查后改查询结果和后续更新是否在同一事务内有没有把不应该放进事务的操作放进事务里导致锁持有时间过长事务越长持有的锁越多锁等待和死锁的概率就越高。这也是为什么很多团队在代码规范里强制要求事务内不做远程调用、不执行批量大数据量遍历、不写慢 SQL。3. 解决死锁的常见手段从代码层到数据库层依次打开死锁的解决方案没有一招通用的更高效的方式是按“代码层 → SQL 层 → 数据库配置层”的递进顺序处理。3.1 代码层统一加锁顺序是最有效的规避手段前面反复提到两个事务按相反顺序更新数据是死锁最常见的成因。因此第一个方案就是统一加锁顺序。比如订单流转涉及订单表、订单明细表、库存表。如果所有更新操作都严格按“订单表 → 订单明细表 → 库存表”的顺序加锁那么在单机多事务并发场景下循环等待的概率会明显下降。这个方案听起来简单但实际执行需要很强的代码规范约束。尤其当多人协作时A 服务更新了订单再更新库存B 服务更新了库存再更新订单线上就会埋雷。所以本质上这已经不是一个 DB 问题而是一个设计和代码评审问题。从工程实践看把“统一加锁顺序”写进设计规范比事后修复更有效。团队里可以约定多表更新必须按固定顺序排列并在代码评审时重点检查。3.2 SQL 层让 UPDATE / DELETE 走索引并且做到“一次锁最小范围”尽量让 WHERE 条件命中索引减少锁范围。-- 不推荐如果 status 选择性很低可能不走索引锁大量记录 UPDATE order_info SET status PAID WHERE status PENDING; -- 推荐按主键或唯一键批量更新每次锁定明确记录 UPDATE order_info SET status PAID WHERE id IN (101, 102, 103);另一个常见策略是“化整为零”。一个批量更新任务不要一次更新几万条记录而是分批更新每批几百条批与批之间短暂停顿。这样每批事务持有锁的时间更短与其他事务冲突的概率也会下降。-- 分批处理示例结构实际代码需要根据主键范围或游标实现 UPDATE order_info SET status PAID WHERE id BETWEEN 101 AND 200 AND status PENDING;注意批量 UPDATE 一次锁定的记录越多事务持有的锁就越多死锁风险不会因为参数好看而降低。这里真正的逻辑是“每批任务持有锁的时间尽量短”。3.3 数据库层适当调低锁等待超时让冲突事务更快失败重试innodb_lock_wait_timeout默认值是 50 秒。如果业务对一致性要求高但对单次等待容忍度低可以适当调低比如 5 到 10 秒。这样当一个事务等待锁的时间过长时会更快暴露问题而不是长时间占用连接和线程资源。但调低超时只是让问题提前暴露不能让死锁消失。更重要的是搭配应用层的重试机制。3.4 应用层捕获死锁异常后做有限次数重试Java 服务里处理 MySQL 死锁最通用的做法是捕获DeadlockLoserDataAccessExceptionSpring 框架中常见底层对应 MySQL 的 1213 错误然后做有限次数重试。Retryable( value DeadlockLoserDataAccessException.class, maxAttempts 3, backoff Backoff(delay 100, multiplier 2) ) public void updateOrderStatus(Long orderId, String status) { // 执行业务更新逻辑 }这里有一个非常关键的细节重试的是整个事务不是单条 SQL。因为死锁发生时整个事务可能已经被回滚必须在事务边界重新发起。如果只是重试一条 UPDATE但事务里的其他 SQL 已经丢了上下文数据会出现部分更新问题。重试也不能无限重试。通常建议最多 2 到 3 次每次间隔指数退避避免故障时继续叠加压力。如果重试后仍然失败应该记录完整事务上下文走人工补偿或消息队列重试通道。3.5 隔离级别降低隔离级别能不能治死锁要谨慎对待MySQL InnoDB 的默认隔离级别是REPEATABLE READ。可重复读在实现上通过临键锁Next-Key Lock避免幻读锁范围比读已提交READ COMMITTED更大。因此有的团队会把隔离级别降为READ COMMITTED减少锁范围从而降低死锁概率。但这里要区分“降低概率”和“解决问题”。隔离级别不是为死锁设计的开关它有独立的语义。如果业务已经依赖可重复读的行为比如同一事务内多次查询必须看到同一结果贸然降级会引发更严重的数据一致性风险。从工程经验看降低隔离级别只适合业务对幻读不敏感的场景而且必须经过充分测试。不要为了规避死锁而随意改隔离级别。4. 如何设计一套不容易死锁的更新逻辑一种可复用的判断框架死锁不能等发生了再去救火更应该在设计阶段就考虑并发更新路径。我平时写技术方案时会用一个简单的判断框架来评估死锁风险在这里分享给大家。4.1 判断更新链路时问四件事这条更新会同时触碰哪些表多表更新时所有事务是否按统一顺序加锁。WHERE 条件走不走索引如果不走索引锁范围会扩大到多少。事务时长是多少事务里有没有慢查询、远程调用、批量循环锁持有时间会持续多久。并发强度是多少是普通业务请求还是大批量导入、定时任务、秒杀活动。把四个问题的答案写出来死锁风险基本能判断一半。4.2 从设计上把更新路径收敛到“窄路径”一个更底层的思路是让每个事务尽量只在很窄的数据范围内加锁。具体做法有更新前先通过主键或唯一索引锁定目标再执行更新避免使用范围条件模糊更新避免在一个事务里先全表范围查询再更新如果必须批量更新优先按主键分批把读多写少的表拆出来减少不同事务在同一张表上的竞争。这个思路很像把一条双向四车道的路改成单向窄路通行能力看似受限但不容易堵死。4.3 可复用的排查清单我把线上死锁的处理流程整理成一个四步清单遇到问题可以直接按顺序执行。步骤操作目的1查看SHOW ENGINE INNODB STATUS或错误日志获取最近死锁的事务 SQL 与锁信息2查看INFORMATION_SCHEMA.INNODB_TRX确认当前活跃事务和锁等待状态3用EXPLAIN检查涉及 SQL 的执行计划确认 UPDATE/DELETE 是否命中索引锁范围是否过大4回到代码检查事务边界和加锁顺序定位多表更新顺序、事务内耗时操作、远程调用等隐患这个清单适合线上首次快速定位。如果死锁反复出现就需要进入长期治理阶段把死锁日志、事务时长、SQL 执行计划纳入监控体系。5. 实际项目中避免死锁的落地建议建表、事务、监控一个都不能少最后说几个和 Java 项目落地更相关的建议。死锁问题虽然发生在数据库层但根因往往在应用设计和运维习惯里。5.1 建表阶段就要考虑更新场景的索引很多表在设计时只考虑了查询场景忘记更新场景对索引的依赖。一个简单的判断标准是如果 WHERE 条件里经常出现某个字段并且这个字段的区分度足够高就应该建索引。即使某个字段本身不是高频查询条件但如果它频繁出现在 UPDATE/DELETE 的 WHERE 里索引也值得建。这里要注意索引不是越多越好。索引会影响插入和更新的性能也会增加存储成本。所以优先保证高频更新路径上的索引可用即可。5.2 事务里不要做“无关的事”我见过一个线上死锁案例一个事务里先更新订单状态再去调用远程库存服务远程服务响应超时事务迟迟不能提交订单表上的锁一直不释放其他请求全部排队。这种问题的根源不是锁机制而是事务边界设计不合理。远程调用、文件上传、发送消息、等待用户输入这些操作都不应该放在数据库事务里。事务应该只包含必要的数据库读写操作而且尽量短。5.3 监控死锁不能只靠“报错后有人看到”线上偶发一次死锁可以被当作是运气不好但频繁死锁一定说明系统存在结构性问题。建议在监控系统里配置死锁告警监控 MySQL 错误日志中的Deadlock found关键字监控SHOW ENGINE INNODB STATUS输出的最新死锁频率对事务超过预期时长的 SQL 设置慢查询告警对锁等待超时错误设置阈值。从工程经验看监控的价值不只是告警更是长期积累有了历史死锁样本才能判断“哪些表、哪些 SQL、哪些业务场景容易出问题”进而推动设计上的改进。5.4 不要指望“换一个数据库中间件”就能根治死锁分布式数据库、中间件、代理层能解决部分高可用和扩展性问题但对单事务内部的加锁顺序、索引选择、事务时长没有决定性的改变。如果业务层还在用相反顺序更新多张表无论底层换成什么数据库死锁或类似的冲突依然可能出现。所以我的建议是先解决应用层和 SQL 层的问题再考虑基础设施层的调整。把“改配置、换组件”当作最后手段而不是第一反应。6. 回到面试这道题到底想考什么很多 Java 面试题表面问的是技术知识点实际考的是你有没有真实处理线上问题的经验。以“线上 MySQL 出现死锁怎么办”为例面试官真正想看的是你能不能准确诊断死锁和锁等待是否分得清隔离级别是否理解锁粒度是否清楚。你能不能快速定位会用哪些命令看哪些日志如何找到涉及的事务和 SQL。你能不能给出可落地的方案不只会说“加索引”还要能说清楚为什么索引不够时需要调整批量更新策略。你有没有长期治理意识会不会留下监控、日志、重试机制、代码评审规范。如果你只回答“重启服务”说明你连问题都没定位清楚。如果你只回答“用SHOW ENGINE INNODB STATUS看一下”说明你有排查意识但缺少完整的处理链路。如果能从死锁日志切入分析加锁顺序、索引执行计划、事务边界、隔离级别和重试策略再给出一个从监控到长期治理的闭环思路这道题基本就是加分项了。再补充一个容易被忽略的面试细节回答时要分清楚“理论最优”和“现实可行”。理论上统一加锁顺序是最优雅的解决方案但真实项目里多个团队维护同一批表时统一顺序并不容易落地。所以我会建议先用最少侵入的方式分批更新、重试、缩短事务降低死锁影响再推动代码规范和索引治理。这样回答面试官能看出你不光是会背方案还理解方案在真实系统里的执行难度。7. 想清楚你是在“处理故障”还是在“治理隐患”回到文章开头那个场景。重启服务不能作为死锁的解决方案因为它只清掉了当前的事务状态没有改变产生死锁的请求模式。一次死锁发生后正确顺序永远是先收集证据再定位根因最后从应用、SQL、配置、监控四个层面推进治理。这类问题的本质不是“MySQL 报了一个错我怎么消掉它”而是“并发事务在竞争资源时如何通过设计让冲突概率降到可接受范围”。索引、事务长度、加锁顺序、批量策略、重试机制、监控告警这些东西组合起来才是数据库并发稳定性的真正底盘。如果你在准备 Java 面试建议不要只背“死锁四要素”而是准备一个自己真正理解过的案例从发现报错、查看死锁日志、用EXPLAIN验证执行计划、回到代码检查事务边界到最终通过调整更新顺序或分批策略解决。能把这个过程讲清楚比背一百个概念都有说服力。如果你是在处理线上问题记住一句话先别急着动任何配置先把死锁现场保存下来。清楚发生了什么远比尽快恢复更重要。
返回列表