大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。
上周线上又报死锁了。应用日志里刷了一串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刷屏,盯着日志发呆"的时刻。
我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋