MySQL 报出 Lock wait timeout exceeded; try restarting transaction 这个错误时,说明当前事务在 innodb_lock_wait_timeout 时间内(默认 50 秒)一直没有拿到想要的锁,最终放弃执行。这类问题的麻烦之处在于,报错只是结果,真正的病灶是那个一直持有锁不释放的事务或者 SQL。要彻底解决,必须把锁的持有者、等待者、以及背后的业务逻辑一起挖出来。

一、先搞清楚锁从哪里来:InnoDB 的加锁规则
排查锁问题之前,得先明白 MySQL 在什么情况下会加锁。InnoDB 默认使用行级锁,但加锁的对象是索引记录,而不是数据行本身。如果 UPDATE 或 DELETE 语句的 WHERE 条件没有命中合适的索引,InnoDB 就会扫描全表,把扫描到的每一行都加上锁,效果上接近锁全表。很多所谓的锁等待超时,根源其实就是一条没走索引的更新语句。
第二个容易被忽视的是间隙锁(Gap Lock)。在可重复读(REPEATABLE READ)隔离级别下,InnoDB 为了防止幻读,对范围查询命中的区间会加间隙锁。间隙锁之间互相不冲突,但间隙锁会阻止其他事务在区间内插入数据。典型的触发场景是:事务 A 执行了 SELECT * FROM orders WHERE status = 1 FOR UPDATE,即使 status=1 的记录不存在,也会锁住对应的间隙,此时事务 B 想插入一条 status=1 的新记录就会被阻塞,等到超时后报错。
此外还有唯一键冲突时的锁、外键检查加的锁、以及 INSERT ... SELECT 对源表的加锁。理解这些规则后,排查时就能大致判断出哪些 SQL 组合可能产生冲突,缩小排查范围。
二、快速定位:找到谁在持有锁,谁在等锁
线上出现锁等待超时时,第一件事是抓住现场。MySQL 提供了两套视图来观察锁信息。第一套是 information_schema.INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS(MySQL 8.0 改为 performance_schema 下的 data_locks 和 data_lock_waits)。通过三表联查,可以直接拿到正在等锁的事务 ID、阻塞它的事务 ID,以及双方争抢的具体锁资源。
-- MySQL 5.7 查看锁等待关系
SELECT
r.trx_id AS waiting_trx_id,
r.trx_mysql_thread_id AS waiting_thread,
r.trx_query AS waiting_query,
b.trx_id AS blocking_trx_id,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_query AS blocking_query,
b.trx_state,
b.trx_started
FROM information_schema.INNODB_LOCK_WAITS w
JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_id
JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id;
查询结果中重点看 blocking_query,它就是罪魁祸首正在执行的 SQL。但有一种情况要特别注意:blocking_query 可能是 NULL。这是因为持有锁的事务已经执行完了 SQL,但事务还没提交或回滚(比如程序里开了事务后去做 RPC 调用、发消息,迟迟没有 commit)。这时要看 trx_started 字段确认事务的存活时长,再通过 performance_schema.threads 和 events_statements_current 查出该连接最近执行过的语句,还原它到底干了什么。
拿到 blocking_thread 之后,如果情况紧急需要快速止血,可以直接执行 KILL <thread_id> 杀掉阻塞源头,被阻塞的事务会继续执行。但kill只是应急手段,事后必须分析根因,否则问题会反复出现。建议同时开启慢查询日志和 general log 采样一段时间,把事发时段的完整 SQL 流水留下来用于离线分析。
三、常见根因分析与对应解法
第一种根因是索引缺失导致锁范围扩大。用 EXPLAIN 检查更新类语句的执行计划,如果 type 是 ALL,说明全表扫描,锁的粒度会覆盖全部记录。解决办法很直接:给 WHERE 条件中的过滤列建索引,让语句只锁真正需要修改的行。比如订单表按 status 加工单,如果 status 上没索引,一条 UPDATE 会锁住全表几十万行,其他事务碰这张表必然排队。
第二种是大事务长事务。有些业务代码在一个事务里处理上千条数据,或者事务中夹杂了外部 HTTP 调用,导致锁持有时间从毫级膨胀到几十秒。处理思路是把大事务拆小:批量操作改成分批提交,每批几百条;外部调用移到事务外面,用本地消息表或事务回查的方案保证最终一致。同时可以配置 SET GLOBAL innodb_lock_wait_timeout = 10 适当缩短等待时间,让冲突快速失败而不是长期堆积。
-- 大事务分批处理的示例 -- 每次只处理 500 条,缩短单次锁持有时间 UPDATE orders SET status = 2 WHERE status = 1 LIMIT 500; -- 结合程序循环判断影响行数,直到没有可处理的数据为止
第三种是并发插入被间隙锁挡住。如果业务上不存在幻读敏感的逻辑,可以考虑把隔离级别降到 READ COMMITTED,此时 InnoDB 不加间隙锁,只锁已经存在的记录,插入冲突会明显减少。另外唯一键冲突也会带来锁:两个事务同时插入相同的唯一键,先到的拿到锁,后到的等待,如果先到的事务迟迟不提交,就会连锁引发超时。对于高并发插入场景,可以用 INSERT ... ON DUPLICATE KEY UPDATE 或先查后插加唯一索引兜底的幂等方案来规避。
四、建立长效机制:监控与预防
解决完眼前的问题,还应该建立监控避免下次被动救火。可以写一个定时任务,每隔几秒查询 INNODB_TRX,找出 trx_started 超过一定阈值(比如 10 秒)的长事务并告警;或者直接用 performance_schema 的 data_lock_waits 做锁等待次数的采集,接入 Prometheus + Grafana 做趋势图。锁等待的突增往往先于业务报错出现,提前告警能争取处理时间。
在规范层面,代码评审时重点检查:事务内是否有远程调用、更新语句是否都走索引、是否有人写了 SELECT FOR UPDATE 却忘了限制条件范围、连接池的超时配置和 MySQL 的 innodb_lock_wait_timeout 是否匹配。把这些点固化成团队规约,配合监控数据持续观察,锁等待超时的发生率可以压到极低的水平。
MySQL锁等待innoDB锁Lock wait timeout修改时间:2026-09-08 05:32:27