死锁报错看了三遍没看懂?我拆给你看(附定位SQL)

简介: 从一次真实死锁现场切入,讲清行锁、间隙锁、插入意向锁的加锁机制与死锁形成原理,手把手教你怎么用show engine innodb status和information_schema定位死锁,并给出加锁顺序设计等避坑清单。

大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。

上周线上又报死锁了。应用日志里刷了一串ERROR 1213,事务回滚,用户那边直接看到"操作失败,请重试"。群里问了一圈,没人看懂死锁日志。我打开show engine innodb status,盯着那段英文看了十分钟,才理清两个事务是怎么卡死的。

今天把死锁这件事拆开讲。锁是怎么锁的,死锁怎么形成,日志怎么看,最后怎么解。看完你应该能自己定位死锁了。

一、死锁是怎么形成的

死锁,白话讲就是两个事务互相等对方手里的锁,谁也不让,就卡死了。

InnoDB的锁主要分共享锁和排他锁。共享锁(S锁)读的时候加,多个事务可以同时持有。排他锁(X锁)写的时候加,跟其他锁都互斥。一个事务持有X锁,别的事务想再加锁,只能等。

光有行锁还不够,InnoDB还有间隙锁。它锁的不是某一行,是一段范围,主要用来防幻读。还有一个插入意向锁,是插入前申请的。这几个锁凑一起,死锁就容易了。

我那次死锁,是两个事务的加锁顺序反了。事务A先锁了订单表的某一行,再去锁用户表。事务B先锁了用户表,再去锁订单表。两边都拿着对方想要的锁,互等,谁也不放手。

事务A: 锁订单行1 → 想要用户行5(被B持有)→ 等待
事务B: 锁用户行5 → 想要订单行1(被A持有)→ 等待
结果: A等B,B等A,InnoDB选择回滚一个,报1213

二、死锁报错长什么样

死锁发生时,InnoDB会主动回滚一个事务,让另一个继续。被回滚的那个会收到这么个错误:

ERROR 1213 (40001): Deadlock found when trying to get lock;
try restarting transaction

翻译过来:检测到死锁,请重试事务。注意,InnoDB不会两个都回滚,它回滚代价小的那个,另一个正常提交。所以死锁不一定会丢数据,但被回滚的那次操作,业务得重试。在MySQL 8.0中,死锁检测机制从“按需触发”变成了后台线程持续追踪等待图(wait-for graph),一旦形成环就毫秒级回滚。这使得死锁能被更快发现,但在高并发下,检测本身的开销也值得关注。

很多人看到1213就慌了,以为数据坏了。其实不用慌,数据没坏,就是一次事务被回滚了。真正要查的是:为什么会死锁,能不能从根上避免。

三、死锁日志怎么读

关键一步是打开死锁日志,命令是show engine innodb status。死锁信息在LATEST DETECTED DEADLOCK这一段。需要留意的是,这个段只保存最近一次死锁的详情,生产环境死锁一多,前面的记录就被覆盖了。如果死锁频繁发生,建议开启innodb_print_all_deadlocks参数,把所有死锁日志都记录到error log中,方便回溯。

SHOW ENGINE INNODB STATUS\G

日志会列出两个事务,各自持有什么锁、在等什么锁。读的时候抓三个重点。

第一个,看两个事务在等什么锁。日志里TRANSACTION A持有X锁,等S锁。TRANSACTION B持有S锁,等X锁。一对比,就知道谁卡谁。

第二个,看加锁涉及的表和索引。日志会指出锁在哪个表的哪个索引上,还有具体的锁模式。LOCK_GAP、LOCK_REC_NOT_GAP这些标记,能看出是不是间隙锁引起的。

第三个,看哪条SQL触发的。日志里会带出事务最后执行的SQL,就是从哪条语句开始卡的。

读懂了这三块,死锁的基本盘就清楚了:谁先锁的,卡在哪,哪条SQL引起的。

四、用information_schema查锁等待

死锁日志是"事后"的,事情已经发生了。要防患于未然,得实时盯锁等待。

在 MySQL 8.0 中,information_schema.innodb_lock_waits已被弃用,推荐使用performance_schema下的表来查询锁等待:

-- MySQL 8.0+ 查询锁等待
SELECT
  r.trx_id AS waiting_trx_id,
  r.trx_mysql_thread_id AS waiting_thread,
  b.trx_id AS blocking_trx_id,
  b.trx_mysql_thread_id AS blocking_thread,
  TIMESTAMPDIFF(SECOND, r.trx_started, NOW()) AS wait_seconds
FROM performance_schema.data_lock_waits w
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_engine_transaction_id
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_engine_transaction_id;

查出阻塞线程后,去SHOW PROCESSLIST看它在跑什么SQL,是不是一个长事务占着锁不放。

如果仍在使用MySQL 5.7,可以沿用information_schema.innodb_lock_waits的写法。生产环境建议先确认MySQL版本,再选择对应的查询方式。

五、死锁怎么解

定位到根因,解法就清楚了。我总结了三条。

第一,统一加锁顺序。业务里凡是会同时碰多张表的,锁的顺序必须一致。先锁A再锁B,所有事务都这么干,就不会互相等。

第二,让事务尽量短。锁持有时间越长,撞车的概率越大。把查询、计算放到事务外,事务里只留必要的写入,锁很快就释放。

第三,用一致的索引路径。两个事务走不同的索引,加锁的行可能不同,反而容易撞出间隙锁死锁。让相关查询走同一个索引,加锁范围一致。

还有两个参数,innodb_lock_wait_timeout和innodb_deadlock_detect,偶尔会用。前者是等锁超时时间,后者是死锁检测开关。这两个是兜底,别指望靠它们解决死锁。真正的解法,是业务层的加锁顺序。

避坑清单

别一看到1213就改** innodb_lock_wait_timeout 。死锁不是“等得不够久”,是加锁顺序的问题。调大超时,只是把问题往后拖,还可能让堵住的时间更长。我见过有人调到100秒,线上查询全堵了。

别顺手就KILL掉锁等待的线程。先看清楚是谁在持锁、持了多久。有可能是正常事务,只是跑得慢。乱杀线程,可能杀掉一个正在提交的业务事务,数据回滚,影响更大。

间隙锁最容易忽略。很多人以为死锁就是行锁互斥,其实间隙锁引起的死锁很常见,尤其插入操作。间隙锁本身是兼容的,两个事务可以在同一个间隙里同时加间隙锁。但一旦有事务想在这个间隙里插入数据,就会申请插入意向锁,而插入意向锁和间隙锁是冲突的。如果两个事务分别在对方的间隙里插数据,死锁就来了。所以插入操作密集的场景,尤其要留意间隙锁的覆盖范围。排查时多留意LOCK_GAP标记,我那次死锁就是两个事务插入时,间隙锁和插入意向锁撞了。

我的判断

死锁这东西,报错就一行,背后的锁机制却是一整套。它不像慢查询那么明显,更像是业务并发设计里藏着的雷。排查一次,你就能更懂InnoDB的加锁逻辑。我越来越觉得,死锁不是能靠调参调没的。真正的解法在业务层:加锁顺序、事务长度、索引路径。这些设计对了,死锁自然就少了。

遇到死锁别慌,也别急着改配置。先看懂日志,理清谁在等谁,再动手。这是我踩了无数次坑换来的经验。


你被死锁坑过吗?死锁日志看得懂不?评论区聊聊。我猜不少人都经历过"1213刷屏,盯着日志发呆"的时刻。

我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋

相关文章
|
1月前
|
存储 关系型数据库 MySQL
面试总问的B+树,我把磁盘IO到底怎么算的讲清楚了
从磁盘IO的底层约束讲起,逐层对比哈希、二叉、红黑树、B树与B+树,讲清MySQL为什么选B+树(矮胖树、顺序IO、范围查询、查询稳定),并用这套底层理解反过来指导覆盖索引、前缀索引、最左前缀等日常索引设计。
|
1月前
|
存储 SQL 容灾
共享存储集群 vs 分布式多副本:同城双活两条技术路线怎么选?
同城双活正在成为金融、政务等核心系统的容灾标配——RPO=0、RTO<30秒。但真正的落地远不止“两个机房各放一套数据库”那么简单。网络延迟的容忍度、脑裂预防机制、同步复制的性能代价、以及故障切换后的数据回滚,每一个环节都可能成为“最后一公里”的绊脚石。本文从容灾架构演进入手,拆解同城双活的核心技术原理、关键挑战与应对方案,并结合同城双中心方案及实测数据进行深度解析。
|
1月前
|
SQL 人工智能 运维
3个月AI Agent运维实测:慢SQL它管,根因还得我上
以三个月实测的视角,划清AI Agent自治运维的真实能力边界:巡检、慢SQL发现等重复活已可替代,复杂根因、变更审批、数据兜底仍需人把关,探讨DBA角色从救火队员向定规则、把关人的转型。
|
1月前
|
SQL 存储 关系型数据库
分区裁剪失效、DDL卡死、元数据爆炸:分区表的3个真实代价
很多人觉得分区表是“轻量级分库分表”——数据分开放、查询只扫一个区、过期数据直接DROP分区,听起来很完美。但分区表有严格的适用边界和隐藏代价:分区键选错导致所有查询都扫全部分区、分区数量过多导致DDL巨慢、跨分区查询比普通表还慢……本文从分区表的核心原理出发,拆解4种分区类型、3个真实踩坑场景,以及分区表与分库分表的本质区别,帮你一次性搞清楚到底该不该用。
|
1月前
|
SQL 运维 关系型数据库
2026年分布式数据库有哪些主流选择?先评估这5个维度再决定
分布式数据库选型,99%的文章在列表格比参数——但真正的决策关键不在厂商PPT里,在上线后的真实运维里。本文从网络延迟容忍度、SQL兼容性验证、在线扩容能力、全局索引代价、运维工具链成熟度5个维度出发,给出可落地的评估方法和决策建议,帮助你在选型阶段避开那些“只有上线后才会发现”的坑。
|
1月前
|
JSON 关系型数据库 MySQL
MySQL 5.7升级到8.0之后,JSON查询的性能瓶颈真的解决了吗?
MySQL 5.7引入原生JSON类型,8.0支持多值索引,至今已近十年。但大量开发人员仍在把JSON当“万能兜底字段”——不管什么数据都往里塞,等查询慢到怀疑人生才想起来排查。本文拆解JSON字段查询的5个高频踩坑场景,给出虚拟列索引、多值索引等正确的优化方案。
|
24天前
|
SQL 运维 算法
订单表上亿行,我按用户ID拆成128片之后怎么样了
从单表几千万行慢查询的痛点出发,讲清垂直拆分与水平拆分的区别、分片键怎么选、分片算法(hash取模/range/一致性hash)怎么权衡,以及分库分表带来的分布式ID、跨片查询、分布式事务等问题,给出避免过度拆分的避坑清单。
|
23天前
|
SQL Java 数据库连接
1万行插入13秒到0.9秒:ORM批量插入只差一个参数
从一次列表接口慢的排障讲起,发现2000多条一模一样的N+1查询。文章拆开ORM生成慢SQL的三类典型病:N+1懒加载、逐条批量插入(只差一个rewriteBatchedStatements参数)、隐式转换让索引白建。给出JOIN/批量IN/@BatchSize的取舍、MyBatis与JPA各自的修法,以及用performance_schema按SQL指纹抓N+1、测试环境打印真实SQL的协作办法。
|
27天前
|
SQL 关系型数据库 MySQL
别再盯着EXPLAIN的rows列了,8.0.18之后有更好的选择
EXPLAIN是DBA最常用的工具之一,但大多数人还在看type、rows、Extra这些传统字段——然后靠经验猜。MySQL 8.0.18开始引入的EXPLAIN ANALYZE,直接把实际执行时间和行数输出给你看,不用猜了。本文对比传统EXPLAIN和EXPLAIN ANALYZE的差异,展示如何用新工具把执行计划分析这件事从“猜”变成“看”。
|
18天前
|
存储 关系型数据库 MySQL
对账差了三毛钱,查完我把全部金额字段从DOUBLE改成了DECIMAL
一次财务对账差三毛钱的排查,牵出金额字段用浮点数的老坑。从IEEE 754为什么存不准0.1讲起,用同一批金额把FLOAT、DOUBLE、DECIMAL三种类型实测对比,再给出金额字段的选型、聚合与改表做法,附避坑清单。