lock wait timeout exceeded 是事务锁等待超时(默认50秒),非存储过程执行超时;根本原因是dml语句持锁未释放或等待他人释放锁,诱因包括无索引扫描、间隙锁、长事务、游标单行处理及隔离级别不匹配等。

Lock wait timeout exceeded 是锁等超时,不是存储过程本身超时
MySQL 报出 Lock wait timeout exceeded; try restarting transaction,本质是事务在等锁时超过了 innodb_lock_wait_timeout(默认 50 秒),而不是“存储过程执行太久被杀”。存储过程只是容器,真正卡住的是它内部某条 DML 语句持有的锁没释放,或等待别人释放锁。
- 常见诱因:存储过程中用了
SELECT ... FOR UPDATE或UPDATE扫描范围过大(如没走索引、用WHERE status = 'pending'在 RR 隔离级别下触发间隙锁) - 游标循环里逐行
UPDATE,每行都加 X 锁,锁持续到整个事务结束,其他并发请求全排队 - 事务边界没控制——把日志写入、HTTP 调用、复杂计算全包进
BEGIN/COMMIT,人为拉长锁持有时间
默认 REPEATABLE READ 隔离级别让间隙锁雪上加霜
MySQL 存储过程默认运行在 REPEATABLE READ 下,这个级别会自动加间隙锁(Gap Lock),尤其在范围条件 + FOR UPDATE 场景中。比如:SELECT id FROM orders WHERE created_at > '2026-06-01' FOR UPDATE,不仅锁住匹配的行,还锁住“2026-06-01 之后但尚未插入的空隙”,直接阻塞后续 INSERT,引发连锁等待。
- 若业务允许“不可重复读”(如定时统计、后台批处理),开头加
SET TRANSACTION ISOLATION LEVEL READ COMMITTED可关闭间隙锁 - 不要依赖“默认最安全”,RR 对高并发批量更新反而是最重的锁策略
-
READ COMMITTED下,InnoDB 只加行锁,不加间隙锁,锁粒度更细、释放更早
autocommit=1 不等于“无事务”,反而放大锁争用
很多人以为存储过程里没写 BEGIN 就是“每条语句独立提交、不会锁太久”,这是错觉。当 autocommit=1 时,每条 DML 确实自动提交,但每条语句仍是一个独立小事务:获取锁 → 执行 → 写 binlog → 释放锁。高频短事务在高并发下会造成锁频繁申请/释放,上下文切换开销大,且容易和其它长事务形成“锁链式等待”。
- 更稳的做法:人工划定最小必要事务边界,例如只把“查 ID 列表 + 批量更新”包进一个
BEGIN/COMMIT,而日志记录、消息通知挪到应用层异步做 - 避免在事务内调用
SLEEP()、GET_LOCK()或外部服务,这些操作会让锁白白挂着不动 - 批量操作必须分片:用
WHERE id BETWEEN ? AND ?每次处理 100–500 行,别用WHERE status = 'pending'全表扫
游标 + 单行处理是并发杀手,集合操作才是正解
MySQL 的 DECLARE CURSOR 是纯单线程迭代器,在 RR 隔离级别下,游标打开期间会持续持有快照,且每次 FETCH 后执行的 UPDATE 都可能触发新锁。它无法并行,也不适配 InnoDB 的 MVCC 设计逻辑。
- 能用一条 SQL 完成的,绝不要写循环:把
UPDATE t SET a = a+1 WHERE id IN (SELECT id FROM tmp)替代游标遍历 - 中间结果优先建临时表:
CREATE TEMPORARY TABLE tmp_ids (id BIGINT PRIMARY KEY) ENGINE=MEMORY,带主键索引,避免未索引临时表导致全表扫描锁 - 聚合类操作(如累加积分)直接用
INSERT INTO ... SELECT ... ON DUPLICATE KEY UPDATE,绕过“查→改”两阶段
SELECT ... FOR UPDATE,在 RR 下锁的可能不是数据行,而是你根本没意识到的“空隙”。**











