别傻傻看 SHOW PROCESSLIST 了!这才是排查 Lock wait timeout 的正确姿势
不要等到数据库报警了才去 Google 怎么查锁。避免长事务,大批处理用 LIMIT 分批。监控长事务(Long Transaction)。记住。找到,确认无误后,KILL之。

你的 Java 日志里突然疯狂报错:java.sql.SQLException: Lock wait timeout exceeded; try restarting transaction
第一反应:
很多人的第一反应是执行 SHOW PROCESSLIST;。
结果看到一堆 Sleep 状态的连接,或者是几百个 UPDATE 正在 Updating。你看着这堆列表,完全不知道是谁阻塞了谁。
痛点:SHOW PROCESSLIST 只能看到谁在干活,或者谁在睡觉,但它看不出谁拿着锁不放。
神器降临:
MySQL 5.7 (及 8.0+) 自带的 sys 库中,有一个被严重低估的视图:sys.innodb_lock_waits。它能像侦探一样,直接告诉你:“线程 A 正在等待锁,而锁被线程 B 拿着,线程 B 正在执行(或执行过)这条 SQL…”
1. 核心原理:不再盲猜
在 MySQL 内部,锁的信息分布在 information_schema 的三张表里(INNODB_TRX, INNODB_LOCKS, INNODB_LOCK_WAITS)。
- TRX: 当前运行的事务。
- LOCKS: 锁的具体信息。
- WAITS: 谁在等谁。
要把这三张表关联起来看懂,需要写很复杂的 JOIN 语句。而 sys.innodb_lock_waits 视图就是官方帮我们写好的这个复杂 JOIN。它直接展示了 Blocker (阻塞者) 和 Waiter (等待者) 的关系。
2. 实战演练:一键抓捕
请把这条 SQL 加入你的收藏夹或常用的数据库管理工具(Navicat/DBeaver)的 Snippet 中。
神级查询 SQL:
SELECT
-- 1. 等待的时间 (以此排序,看谁等最久)
wait_age AS '等待时长',
-- 2. 受害者 (谁被锁住了?)
waiting_pid AS '受害者ID',
waiting_query AS '受害者想执行的SQL',
-- 3. 罪魁祸首 (谁拿着锁?)
blocking_pid AS '肇事者ID',
blocking_trx_started AS '肇事事务开始时间',
-- 4. 关键证据 (肇事者最后执行的 SQL)
-- 注意:如果肇事者正在 Sleep,这里显示的是它最后执行的那条语句
blocking_query AS '肇事者最后执行的SQL',
-- 5. 锁的类型
locked_table AS '被锁表名',
locked_type AS '锁类型'
FROM sys.innodb_lock_waits
ORDER BY wait_age DESC;
执行结果示例:
| 等待时长 | 受害者ID | 受害者SQL | 肇事者ID | 肇事者最后执行的SQL |
|---|---|---|---|---|
| 00:00:45 | 102 | UPDATE stock SET num = num -1… | 88 | NULL (或者某个 SELECT) |
分析:
- ID 为 102 的线程已经被卡了 45 秒。
- 凶手是 ID 为 88 的线程。
- 下一步行动: 直接
KILL 88;即可解救众生。
3. 三大实战场景
场景一:未提交的事务 (The Zombie Transaction)
现象: blocking_query 显示为 NULL 或者一条很简单且已经执行完的 SQL,但 blocking_pid 的状态是 Sleep。
原因: 这是最坑的场景。通常是开发者在本地终端连测试库,执行了 BEGIN; UPDATE ...; 然后忘记 COMMIT,就去吃饭了。或者代码中开启了事务,但因为某个逻辑 Bug(如死循环、外部 HTTP 调用超时)导致迟迟没有 Commit。
排查: sys.innodb_lock_waits 能一眼看出这个 Sleep 的线程 88 拿着锁不放。
场景二:间隙锁 (Gap Lock) 误伤
现象: 你明明 UPDATE 是一行不存在的数据,或者 UPDATE 不同的行,却依然被锁。
原因: 在 Repeatable Read 隔离级别下,MySQL 会使用间隙锁。
比如 DELETE FROM users WHERE id > 100,即使目前只有 id=101 的数据,MySQL 也会锁住 (100, +∞) 的范围。此时你插入 id=102 就会被阻塞。
排查: 通过视图可以看到 locked_mode 包含 Gap 字样。
场景三:大事务导致的批量阻塞
现象: 一个后台定时任务正在删除 100 万条过期日志(DELETE FROM logs ...),导致前台所有写入操作全部卡死。
排查: 视图中会显示同一个 blocking_pid 对应了几十个 waiting_pid。这意味着一个大事务阻塞了全世界。
解决: 杀掉大事务,将其拆分为 DELETE + LIMIT 的小事务执行。
4. 进阶:如果 sys 库也没装怎么办?
在某些极度精简的云数据库环境,可能连 sys 库都没有。此时你需要回归原始手段:
SHOW ENGINE INNODB STATUS;
在输出的大段文本中,找到 LATEST DETECTED DEADLOCK (如果是死锁) 或 TRANSACTIONS 章节。
你会看到类似 MySQL thread id 88, OS thread handle ... 以及 TRX HAS BEEN WAITING 10 SEC 的描述。虽然晦涩,但也能找到线索。
5. 总结
不要等到数据库报警了才去 Google 怎么查锁。
- 预防: 避免长事务,大批处理用 LIMIT 分批。
- 监控: 监控长事务(Long Transaction)。
- 急救: 记住
SELECT * FROM sys.innodb_lock_waits。找到blocking_pid,确认无误后,KILL之。
更多推荐



所有评论(0)