☰
MySQL 线上报 Lock wait timeout 别慌:6 招定位锁等待与死锁
2026/10/15 17:23:33 网站建设 项目流程

凌晨两点告警群炸了:订单接口集体超时,日志里刷屏Lock wait timeout exceeded; try restarting transaction。登上去一看,SHOW PROCESSLIST里一堆 query 在排队,却看不出谁在挡谁。

这个场景几乎每个 DBA 和后端都撞过。InnoDB 的锁信息不在慢日志里、也不在业务日志里,得去 performance_schema 里翻。今天把线上排查锁等待和死锁的 6 招整理成一份可以直接照抄的清单,前 3 招负责"找到人",后 3 招负责"别再犯"。

先分清两种报错,入口不一样

报错

含义

排查入口

1205 Lock wait timeout

等行锁超过innodb_lock_wait_timeout(默认 50 秒),只回滚这一条 SQL

查锁等待视图,找 blocking_pid

1213 Deadlock found

双向循环等待被 InnoDB 主动检测出来,回滚了代价较小的一个事务

看SHOW ENGINE INNODB STATUS的 LATEST DETECTED DEADLOCK 段

很多人把这两种当成一回事去处理,结果翻半天日志找不到死锁记录——1205 根本不产生死锁记录,它只是"等太久了"。

第 1 招:一眼看谁在等谁

这是最常用的一招。有人还在网上抄 5.7 时代的information_schema.INNODB_LOCK_WAITS,注意 **MySQL 8.0 已经把这两张表删掉了**(5.7 就标了弃用),照搬会报 unknown table。8.0 直接用sys.innodb_lock_waits:

SELECT waiting_pid, waiting_query,
blocking_pid, blocking_query,
wait_age_secs,
sql_kill_blocking_connection
FROM sys.innodb_lock_waits\G

-waiting_pid:被卡的连接

-blocking_pid:挡住别人的元凶

-wait_age_secs:已经等了多少秒

- 最后一列直接把KILL xxx语句替你拼好了

一个常见的困惑:**blocking_query显示 NULL 很正常**。元凶事务可能正开着但闲着——早先执行完一条 SQL 就没再发语句,等着业务下一步。这时去看blocking_trx_started,就知道它把事务开了多久。

第 2 招:从事务反查连接和长事务

SHOW PROCESSLIST只能看到当前语句,看不到"这个连接把事务开了多久"。长事务才是锁等待的温床,去information_schema.innodb_trx里按开始时间倒着排:

SELECT trx_id, trx_mysql_thread_id,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS alive_secs,
trx_rows_locked, trx_rows_modified,
trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;

trx_rows_locked看锁了多少行,alive_secs看事务活了多久。线上常见画像:一个连接在事务里查完数据,转头去调第三方接口,接口超时 30 秒,期间它手里的行锁一个都不放,后面所有相关更新全部排队。

要把事务精确关联到 performance_schema 里的会话,用performance_schema.threads的PROCESSLIST_ID(就是SHOW PROCESSLIST里的 Id,也就是KILL命令用的那个 id)。注意别拿内部THREAD_ID去 kill,那是另一个体系。

第 3 招:看懂 LOCK_MODE,别被意向锁吓到

锁的明细在performance_schema.data_locks里。很多新人第一次查到LOCK_MODE = IX、LOCK_TYPE = TABLE就慌了,以为表被锁死。

其实要按LOCK_TYPE分开看:

LOCK_TYPE

LOCK_MODE

含义

会不会挡人

TABLE

IS / IX

意向锁,表示"有事务打算在这个表上加行锁"

只和表级锁(LOCK TABLES)冲突,不挡行操作

RECORD

X

Next-Key 锁:锁记录 + 记录前面的间隙(RR 默认)

挡本记录的写,也挡往间隙里插入

RECORD

X,REC_NOT_GAP

纯记录锁,只锁这一行

挡本记录的写

RECORD

X,GAP

纯间隙锁,只锁区间不锁记录

挡往间隙里插入,不挡已存在行的读写

判断谁真正挡了谁,别靠猜,直接看performance_schema.data_lock_waits的等待关系;最近一次死锁的完整细节(两个事务各持哪把锁、各在等哪把锁、最后回滚了谁)在SHOW ENGINE INNODB STATUS\G的 LATEST DETECTED DEADLOCK 段里。

锁类型对照

第 4 招:最高频的成因——没走索引的 UPDATE

线上"整张表被锁住"的案件,十有八九是这条:

-- status 没索引
UPDATE orders SET status = 2 WHERE status = 1;

执行计划走全表扫描,InnoDB 会给**扫到的每一行**都加锁;在 REPEATABLE-READ 下还伴随间隙锁,效果上等于把整张表锁了。看起来像"锁升级",其实没有锁升级这回事,就是锁的范围跟着扫描范围走。

习惯在建表和改锁问题之前先跑一遍:

EXPLAIN UPDATE orders SET status = 2 WHERE status = 1;

type = ALL、扫描行数接近全表,就是它了。补上索引,锁范围立刻从全表缩到命中的那几行。这是性价比最高的一个修复,没有之一。

顺带两个容易踩的坑:

1. **唯一索引上不存在的行也会被间隙锁挡住**。RR 下SELECT ... FOR UPDATE一条不存在的记录,锁的是它前后的间隙,别人往这个区间 INSERT 会被卡住。

2. **INSERT ... ON DUPLICATE KEY UPDATE和REPLACE INTO并发跑是死锁高发区**,撞的是唯一索引的间隙锁和 S 锁,典型表现就是偶发 1213,单并发复现不了。

第 5 招:从写法上减少锁冲突

找到了人、也修好了索引,还要管住代码里的事务写法。四条纪律按优先级排:

1. **多行更新固定顺序**。所有事务都按主键从小到大更新,循环等待天然不存在。这也是消灭死锁最根本的一条。

2. **大批量改小批量**。UPDATE ... LIMIT 1000配合循环,每批一个事务,锁持有时间从分钟级降到毫秒级。

3. **事务里不做慢操作**。远程调用、发消息、算大数,都挪到事务外。

4. **热点行考虑 READ-COMMITTED**。RC 下间隙锁基本关闭(外键和唯一键冲突检查除外),锁范围明显收窄;改之前和业务确认一致性语义。

-- 分批更新模板:固定顺序 + 小批量
UPDATE orders SET status = 2
WHERE status = 1
ORDER BY id
LIMIT 1000;
-- 影响行数为 0 时结束循环

MySQL 8.0 还给了两个开关:FOR UPDATE NOWAIT(遇到锁直接报错不排队)和FOR UPDATE SKIP LOCKED(跳过被锁的行)。做任务表抢任务的场景,SKIP LOCKED比自己造排队逻辑干净得多。

反例与推荐写法

第 6 招:参数与止血的正确姿势

三个参数值得提前配好:

参数

默认

作用与提醒

innodb_lock_wait_timeout

50

行锁等待超时秒数,会话级可改;**只对行锁等待生效,不影响死锁**

innodb_deadlock_detect

ON

超高并发打同一热点行时,检测本身会烧 CPU,官方允许关掉改靠超时兜底;普通业务别关

innodb_print_all_deadlocks

OFF

开启后每次死锁都写 error log,线上排查靠它攒证据

真要止血时,KILL的姿势有个大坑:

KILL QUERY 12345; -- 只杀当前正在跑的语句,事务还开着,锁一个不释放
KILL CONNECTION 12345; -- 断开连接并回滚事务,锁才真正放掉(KILL 12345 等价)

所以优先 kill 阻塞链条最上游的元凶(blocking_pid),别去杀等待者——杀掉一个等待者,它身后排队的照旧堵着。另外元凶如果是改了很多行的大事务,回滚本身要跑一阵子,锁释放有延迟,杀完用第 1 招确认sys.innodb_lock_waits清空了再收工。

常见故障速查表

现象

最可能的原因

第一步动作

报 1205 Lock wait timeout

行锁等太久

查sys.innodb_lock_waits找 blocking_pid

报 1213 Deadlock found

加锁顺序不一致等循环等待

看 error log / LATEST DETECTED DEADLOCK

大量 query 集体卡住

单个长事务持锁不放

查innodb_trx按trx_started排序

blocking_query为 NULL

元凶事务开着但空闲

看blocking_trx_started和trx_rows_locked

像"锁了全表"

更新条件没走索引

EXPLAIN验证,补索引

偶发 1213,难复现

唯一键冲突写法(ON DUPLICATE KEY/REPLACE)

固定加锁顺序,或改普通INSERT

写在最后

锁问题的排查链路其实很固定:**报错码定性 → 锁等待视图找元凶 → 事务表看持有时间 → LOCK_MODE 看锁粒度 → 索引和写法修根因 → 参数兜底**。前两招背熟,90% 的锁告警能在 5 分钟内定责。

真正值钱的是第 4、5 招的预防:建表时就让更新条件有索引可用,写代码时按顺序、小批量、事务里别干慢活。死锁和锁等待不是玄学,是写法问题的延迟爆发。

排查速查

这套排查路径在云贝教育的 MySQL 实战课里是拿真实故障库练的,命令怎么敲、结果怎么看,对着练一遍比自己摸半天强。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询