应立即定位并终止持锁超60秒的事务,因其常卡在http调用等非db操作;将耗时操作移出事务、拆分大dml为500–1000行小批次并及时commit;调低innodb_lock_wait_timeout至10~30秒以快速暴露问题。

查出正在持锁不放的“钉子户”事务
长事务不是慢,是锁一直挂着不动——TRX_STATE = 'RUNNING'但trx_started早于 60 秒的记录,大概率在干 HTTP 调用、PDF 生成或卡在应用层没提交。别只看 SHOW PROCESSLIST,它对“已执行完 SQL 却没 COMMIT”的事务毫无反应。
用这两条语句快速定位:
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.INNODB_TRX WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60 ORDER BY trx_started;
SELECT * FROM sys.innodb_lock_waits\G
重点关注 BLOCKING_TRX_ID 和 WAITING_TRX_ID 对应关系,再反查 INNODB_TRX 找到阻塞源头的线程 ID 和原始 SQL。
把非数据库操作彻底移出事务边界
事务只该做三件事:读、写、判断要不要回滚。其余所有耗时动作,都是锁等待的加速器。
-
CALL pay_service()、file_put_contents()、curl_exec()必须挪到COMMIT之后 - 反例:
BEGIN; UPDATE order SET status=2 WHERE id=123; CALL pay_service(); COMMIT;→ 支付调用失败前,订单行一直被锁 - 正例:
BEGIN; UPDATE order SET status=2 WHERE id=123; COMMIT; CALL pay_service();→ 锁在第 2 行就释放 - 支付失败需要补偿?用异步任务或状态机兜底,别在事务里重试
拆分大 DML 为带 COMMIT 的小批次
一次更新 10 万行 ≠ 10 万个并发机会,而是 10 万个行锁持续绑定到同一个事务上,后续任何碰这 10 万行的请求全得排队等它结束。
- 别只加
LIMIT 5000,关键是在每次执行后立刻COMMIT - 推荐批大小:500–1000 行/事务;主键区间分片(
WHERE id BETWEEN ? AND ?)比LIMIT offset, size更稳,避免偏移扫描放大 - 确保
WHERE条件字段有索引,否则type = ALL意味着全表扫描+全表加锁,哪怕只改 100 行也锁住 80 万行 - 应用层逐条
UPDATE时,别攒着,每批执行完必须COMMIT,否则锁不会真释放
调低超时参数暴露问题,而不是掩盖它
innodb_lock_wait_timeout 是诊断开关,不是救命稻草。设成 300 秒只会让业务卡得更久、更难定位——Nginx 默认 60 秒就断连了,数据库还在傻等。
- 生产建议设为
10~30(单位秒),用SET SESSION innodb_lock_wait_timeout = 15在事务前动态设置 -
lock_wait_timeout(管 DDL 元数据锁)默认是 1 年,必须改成30或60,否则ALTER TABLE阻塞其他查询时没人知道 - 临时调试可用
SET SESSION innodb_lock_wait_timeout = 5快速暴露锁争用点,但上线禁用
最常被忽略的是:事务里混了非 DB 操作,或者误以为 autocommit=1 就等于“无事务”。其实每条 DML 仍是独立小事务,高频下照样形成锁链式等待——真正该控制的,是事务的**必要边界**,不是它的超时时间。











