MySQL死锁排查最佳实践:锁机制原理与高频死锁模式分析

本文涉及的产品
PolarDB Agent Express,2核4GB
RDS AI 助手,专业版
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
简介: 以线上订单系统锁等待超时为切入,拆解InnoDB锁类型与死锁日志解读方法,总结交叉更新、唯一索引冲突、间隙锁重叠、外键级联锁四种高频死锁模式的排查与避坑经验

大家好,我是数据库小学妹 👋

前段时间被一个问题搞得焦头烂额。线上订单系统时不时报"Lock wait timeout exceeded"。重启应用能好一阵,过几天又来。一开始以为是慢查询,加了索引,没用。后来连续两天高峰期都出现了,订单创建成功率从99.9%掉到95%,客服开始投诉。

我当时第一反应是:是不是连接池不够?加了连接数,没好转。又以为是表锁,查了半天没发现LOCK TABLE语句。最后才搞明白——死锁。

排查过程挺折腾的。死锁发生得很快,可能就几十毫秒,InnoDB检测到会自动回滚其中一个事务。如果不主动去查日志,根本发现不了。

今天把排查过程中学到的东西整理出来。

30秒判断:是不是死锁

线上报锁等待超时,先别急着排查。跑一条SQL就能确认是不是死锁:

SHOW ENGINE INNODB STATUS\G

看输出里有没有"LATEST DETECTED DEADLOCK"这一段。有,就是死锁。没有,大概率是普通的锁等待超时(比如大事务持锁太久)。

还有一个更快的办法:

SELECT * FROM sys.innodb_lock_waits\G

如果有输出,说明当前有事务在等锁。看blocking_query字段,能直接定位是哪条SQL在阻塞别人。

确认是死锁之后,再往下看排查方法。

死锁是什么

简单说就是两个事务互相等对方释放资源,谁都走不动。

你拿着筷子等勺子,我拿着勺子等筷子,俩人就这么僵住了。数据库里也一样,事务A锁了行1想去锁行2,事务B锁了行2想去锁行1,互相等。

以前做设计的时候,我习惯把复杂流程画成流程图,理清依赖关系。死锁本质上就是流程图里出现了循环依赖。事务之间形成了一个环,谁都等谁,谁都走不了。

InnoDB检测到死锁会回滚其中一个事务,通常是undo log最少的那个,因为回滚成本最低。被回滚的事务会收到一个错误:ERROR 1213 (40001): Deadlock found when trying to get lock

先搞清楚InnoDB有哪些锁

要理解死锁,得先知道锁的类型。我刚接触锁的时候觉得概念特别多,后来自己画了一张表才理清楚。

行锁锁住索引记录。走索引才能用行锁,不走索引会退化成表锁。这个很多人踩过坑,包括我。我之前有个查询没走索引,结果行锁退化成表锁,整张表被锁住,其他事务全被阻塞。

间隙锁锁住索引记录之间的"间隙",防止其他事务在这个范围里插入数据。RR隔离级别下默认会有间隙锁。它是为了防止幻读,但副作用是:两个事务在同一个间隙上加间隙锁不冲突,有事务想INSERT才会被阻塞。

临键锁是行锁加间隙锁的组合,InnoDB默认用这个。它同时锁住记录本身和记录前面的间隙。

意向锁是表级锁,表示事务想加行锁。INSERT操作在间隙里还会加插入意向锁,多个INSERT可以共存。

这几种锁的兼容性决定了死锁会不会发生。我画了个简单的兼容矩阵:

行锁(X) 间隙锁(GAP) 插入意向锁
行锁(X) 冲突 不冲突 冲突
间隙锁(GAP) 不冲突 不冲突 冲突
插入意向锁 冲突 冲突 不冲突

看这个表就明白了:两个事务可以在同一个间隙里都加间隙锁,但有事务想在这个间隙里INSERT就会被阻塞。很多死锁就是这么来的。

怎么看死锁日志

遇到死锁第一步是看日志。

SHOW ENGINE INNODB STATUS

输出内容很多,找"LATEST DETECTED DEADLOCK"部分。里面有几个关键信息:事务信息(每个事务的ID、状态、等什么锁、持有什么锁)、锁信息(类型、模式、加在哪个表的哪个索引上)、被回滚的事务是哪个。

我第一次看这个日志的时候完全看不懂。后来发现一个技巧:先找"WE ROLL BACK TRANSACTION",确定被回滚的是哪个事务。然后看这个事务"HOLDS THE LOCK"和"WAITING FOR THIS LOCK",再看另一个事务的锁信息。两个事务的锁等待关系理清楚,死锁原因就出来了。

举个我实际遇到的例子。有一次线上报死锁,我导出日志,关键部分长这样:

LATEST DETECTED DEADLOCK
------------------------
2026-05-15 14:32:18 0x7f3c

*** (1) TRANSACTION:
TRANSACTION 2814735, ACTIVE 0 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 1523, OS thread handle 0x7f3d, query id 98234
*** WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 28 page no 4 n bits 72 index `idx_user_id` of table `orders`
trx id 2814735 lock_mode X waiting

*** (2) TRANSACTION:
TRANSACTION 2814734, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 1520, OS thread handle 0x7f3e, query id 98230
*** HOLDS THE LOCK(S):
RECORD LOCKS space id 28 page no 4 n bits 72 index `idx_user_id` of table `orders`
trx id 2814734 lock_mode X

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 28 page no 5 n bits 72 index `idx_order_no` of table `orders`
trx id 2814734 lock_mode X waiting

*** WE ROLL BACK TRANSACTION (1)

我当时看这段日志的思路是这样的:先看WE ROLL BACK TRANSACTION (1),知道事务1被回滚了。然后看事务1在等什么——它在等idx_user_id上的X锁。再看事务2——它持有idx_user_id上的X锁,同时在等idx_order_no上的X锁。

两个事务分别在等对方持有的锁。事务1拿到了idx_order_no的锁想去锁idx_user_id,事务2拿到了idx_user_id的锁想去锁idx_order_no。加锁顺序不一致,死锁了。

定位到原因之后,解决办法就清楚了:统一加锁顺序,让所有事务都按idx_user_id→idx_order_no的顺序去锁。

我遇到的几种死锁模式

排查了十几个案例,总结了几种常见情况。

事务交叉更新

两个事务以不同顺序更新同一批记录。这个是最常见的死锁模式。

-- 事务A
UPDATE orders SET status = 1 WHERE id IN (1, 2, 3);

-- 事务B
UPDATE orders SET status = 2 WHERE id IN (3, 2, 1);

我遇到的场景是订单批量状态更新。一个接口把订单从待支付改成已支付,另一个接口把同一批订单从待支付改成已取消。两个请求同时进来,IN列表里的顺序不一样,就死锁了。

日志里表现为:两个事务都在等idx_primary上的X锁,但各自已经持有了不同行的锁。

解决办法:统一按主键顺序更新。看起来简单,但线上代码多了很容易漏。我后来在代码规范里加了一条:批量UPDATE必须先对ID列表排序。

唯一索引冲突

INSERT遇到唯一索引冲突时,InnoDB会在冲突的索引记录上加共享锁。两个事务同时INSERT相同的唯一值就会互相等。

-- 事务A和事务B同时执行
INSERT INTO users (email) VALUES ('test@example.com');

我遇到的场景是用户注册。两个请求用同一个邮箱同时注册,都走唯一索引校验。第一个INSERT成功,第二个等锁。如果第一个事务还没提交,第二个就会一直等。如果这时候还有第三个请求也来注册同一个邮箱,就可能形成死锁。

日志里表现为:一个事务在等S锁(共享锁),另一个事务持有S锁但也在等某个X锁。

解决办法:INSERT前先查,或者用INSERT ... ON DUPLICATE KEY UPDATE。我后来在注册接口加了分布式锁,同一个邮箱同一时刻只允许一个注册请求进来。

间隙锁范围重叠

两个事务的查询条件命中同一个间隙,间隙锁互相阻塞。这种死锁最难排查,因为日志里的锁信息看起来是"空的"——锁的不是具体记录,而是一个范围。

-- 事务A(RR隔离级别)
SELECT * FROM orders WHERE id > 100 FOR UPDATE;
-- 锁住(100, +∞)的间隙

-- 事务B
INSERT INTO orders (id) VALUES (150);
-- 想在间隙里插入,被阻塞

我遇到的场景是后台管理系统的订单导出。导出功能用了一个范围查询加FOR UPDATE,锁定了一大批记录。同时用户在前台下单,INSERT操作被间隙锁阻塞。并发一上来就死锁了。

解决办法:缩小查询范围,或者用RC隔离级别。RC下没有间隙锁,但要注意RC下可能出现幻读。如果业务允许,RC是更简单的方案。

外键级联锁

父表有外键约束,子表INSERT时会锁父表对应记录。多个事务同时往子表插入关联到同一条父记录的数据就可能死锁。

这个坑我排查了半天才定位到。当时是一个订单系统,order_items表有外键关联orders表。多个订单项同时插入,都关联到同一个订单ID,互相等父表的锁。

日志里表现为:事务在等父表记录上的S锁,但这个S锁和另一个事务的INSERT意向锁冲突。

解决办法:减少外键约束,业务层保证数据一致性。我后来把外键去掉了,改成应用层校验。去掉之后这类死锁再没出现过。

排查工具

除了SHOW ENGINE INNODB STATUS,还有几个实用的。

sys库直接查锁等待:

SELECT * FROM sys.innodb_lock_waits\G

输出里有几个关键字段:waiting_trx_id(等待锁的事务)、blocking_trx_id(阻塞它的事务)、waiting_query(被阻塞的SQL)、blocking_query(阻塞别人的SQL)。看到blocking_query就能快速定位是哪条SQL导致的阻塞。

performance_schema更详细:

SELECT * FROM performance_schema.data_lock_waits;

能看到锁的类型、模式、加在哪个索引上。配合data_locks表可以看到所有锁的详细信息。

我排查的时候一般先用sys.innodb_lock_waits快速定位阻塞关系,再用SHOW ENGINE INNODB STATUS看完整的死锁日志。两个工具配合用,排查效率最高。

慢查询日志也要看。死锁发生时被回滚的事务的SQL会记录在里面,看看是不是查询条件导致锁范围过大。

MySQL 8.0的变化

MySQL 8.0对锁机制做了不少改进,值得了解。

Performance Schema里的锁相关表从metadata_locks拆分成了data_locks和data_lock_waits,查询更精确了。之前的INFORMATION_SCHEMA.INNODB_LOCKS在8.0被废弃了。

死锁检测的效率也有提升。8.0引入了死锁检测的并行化,高并发场景下死锁检测本身不再成为瓶颈。之前遇到过一个场景,几百个事务同时竞争,死锁检测本身消耗了不少CPU,8.0之后好多了。

还有一个变化:8.0默认隔离级别还是RR,但间隙锁行为更精细了。具体哪些场景优化了我说不太全,但实际用下来,死锁概率确实比5.7低了一些。

避坑清单

统一加锁顺序,多表更新时按固定顺序访问。这个规则看起来简单,但线上代码多了很容易漏。我后来的做法是在DAO层封装一个统一的批量更新方法,强制排序。

缩短事务时间,持锁时间越长死锁概率越高。我见过最长的一个事务持锁超过30秒,原因是事务里调了一个外部接口。后来把外部接口调用移到事务外面,死锁问题直接消失了。

走索引,不走索引退化成表锁。之前有个查询条件没走索引,行锁退化成表锁,整张表被锁住,其他事务全被阻塞。加了索引之后锁范围从整张表缩小到几行。

控制锁范围,避免大范围SELECT FOR UPDATE。后台导出、报表查询这类操作尽量不要加FOR UPDATE,用MVCC的快照读就够了。

RC隔离级别可以减少间隙锁。如果业务对幻读不敏感,RC是更简单的方案。但要注意RC下不可重复读,同一个事务里两次读可能结果不一样。

应用层做好重试机制,死锁防不住就做好善后。InnoDB会自动回滚死锁中undo log最少的事务,应用层捕获到1213错误码后重试就行。重试次数建议不超过3次,间隔递增。


我是数据库小学妹,咱们下篇见 👋

相关文章
|
2天前
|
云安全 人工智能 运维
阿里云SecOps Agent,全新安全跨产品执行体验
自然语言驱动 云安全中心/WAF/CFW/ 等多款安全产品联动
1577 1
|
1天前
|
机器学习/深度学习 人工智能 调度
🐴 HappyHorse 1.1 现已上线阿里云百炼!快来查收模型使用指南,现在调用享 6 折~
HappyHorse 1.1 是新一代视频生成大模型,全面升级动态表现力、角色一致性、指令遵循、视觉质感与音画协同能力。支持I2V/T2V/R2V三类生成,适配短剧、电商广告、品牌营销等场景,提供高质、流畅、可控的AI视频生产力。
426 2
🐴 HappyHorse 1.1 现已上线阿里云百炼!快来查收模型使用指南,现在调用享 6 折~
|
12天前
|
缓存 测试技术 API
Qwen 3.7 Plus 与 Max 实测:性价比与多模态能力差异解析(2026)
2026 年 6 月 1 日,阿里悄无声息地发布了 Qwen 3.7 Plus,距 Qwen 3.7 Max 上线刚好 11 天。同样的 1M 上下文,同样的 35 小时自治上限。但价格才是头条:Plus 是 0.40/M输入,Max是 2.50/M——便宜约 6 倍——并且还能看图、看视频。Vision Arena 上 Plus 已经排到 #16。所以这周真正值得讨论的问题不是”要不要为视觉能力买单”,而是”Max 凭什么用 6 倍价格换来 2 个百分点的 benchmark 领先”。
|
13天前
|
JavaScript 定位技术 API
CodeGraph 爆火:编程 Agent 需要的不是更多上下文,而是一张提前画好的代码地图
CodeGraph 是一款爆火的本地代码智能工具,通过 tree-sitter 解析 AST 构建结构化知识图谱(存于 SQLite),为编程 Agent 提前生成“代码地图”。它显著降低 Agent 在中大型项目中的探索成本——实测工具调用减少71%、Token 降57%、速度提升46%,支持19+语言及主流框架路由识别,完全离线、无需 API Key。
861 11
CodeGraph 爆火:编程 Agent 需要的不是更多上下文,而是一张提前画好的代码地图
|
1天前
|
数据采集 人工智能 搜索推荐
企业智能体的下半场,如何让智能体越用越聪明?
AgentLoop 正在邀测期,点击申请邀测资格。
182 124
|
13天前
|
人工智能 运维 JavaScript
阿里云Qoder CN(原通义灵码)全解析 产品形态、版本划分与技术适配说明
在AI辅助开发与智能办公工具持续普及的当下,阿里云旗下原通义灵码正式更名为Qoder CN,同时延伸出QoderWork CN、Qoder CN CLI、Qoder CN Mobile等多款配套产品,形成覆盖代码开发、日常办公、终端交互、移动端使用的完整工具矩阵。Qoder CN核心定位为AI智能编码助手,深度适配主流代码编辑器、集成开发环境以及终端场景;QoderWork CN则偏向桌面端综合办公辅助,二者面向不同使用场景,划分了多个版本档位,搭配差异化资源配额、功能权限与计费规则,同时兼容多款主流大模型。
917 8
|
9天前
|
人工智能 自然语言处理 算法
阿里云百炼Qwen 3.7 Plus与Max实测全解:性价比与多模态能力、成本深度对比
2026年,阿里云百炼平台推出的Qwen 3.7系列成为企业与开发者落地AI应用的核心选择,其中Qwen 3.7 Max与Plus作为两大旗舰版本,定位差异显著:Max是纯文本推理旗舰,专注高强度智能体与复杂逻辑任务;Plus则是多模态全能版,在保留强大文本能力的同时,补齐图像、视频理解能力,且价格大幅降低。本文基于2026年最新实测数据,从核心参数、文本能力、多模态能力、智能体表现、性价比与场景选型六大维度,全面解析两款模型的差异,为用户提供精准选型参考。
453 0
|
13天前
|
JSON 缓存 安全
通过 CC Switch 本地路由让 Codex CLI 接入 DeepSeek 等第三方模型
CC Switch 通过本地路由(`127.0.0.1:15721`)实现协议转换:将 Codex 的 Responses API 请求自动映射为 DeepSeek 等厂商的 Chat Completions 接口,兼容流式响应与工具调用,无需修改 Codex 源码,安全隔离 API Key。(239字)
2497 7
通过 CC Switch 本地路由让 Codex CLI 接入 DeepSeek 等第三方模型