查清谁在堵路:快速定位锁等待源头
出现
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction,第一反应不该是调参数,而是看“谁卡住了谁”。InnoDB 的锁等待现场稍纵即逝,必须用系统视图抓实时快照:
SELECT * FROM information_schema.INNODB_TRX—— 查当前所有活跃事务,重点关注
trx_state = 'LOCK WAIT'和
trx_started时间过长的
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS—— 直接给出等待关系:
blocking_trx_id是肇事者,
requested_lock_id是受害者
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS—— 查具体锁在哪行(
lock_trx_id、
lock_rec_id),配合
INNODB_TRX中的
trx_mysql_thread_id就能定位到线程
别依赖
SHOW PROCESSLIST,它只显示连接状态,不反映行级锁关系;也别等错误日志里出现 SQL 再查——那时锁可能早已释放。
临时止血:调整 innodb_lock_wait_timeout 参数
这个参数不是“治本”,但能避免业务雪崩。默认 50 秒太长,高并发接口常等几十秒才报错,用户体验极差;设太短(如 1 秒)又容易误杀正常慢事务。建议按场景分级设置:
线上核心接口:设为5或
10秒,让失败更快暴露,前端可降级或重试 后台批处理作业:可设为
120,但必须确保事务本身不超时(比如避免在事务里调外部 HTTP) 修改方式分两种:
SET innodb_lock_wait_timeout = 10只对当前会话生效;
SET GLOBAL innodb_lock_wait_timeout = 10对新连接生效,**当前已连的连接不会变**,这点极易踩坑 永久生效要改配置文件(如
/etc/my.cnf),加一行
innodb_lock_wait_timeout = 10,然后重启 MySQL —— 但生产环境未必允许重启
根因排查:为什么锁等这么久?
调参只是缓冲,真正要盯的是锁为啥不释放。常见硬伤有三个:
没走索引的 UPDATE/DELETE:比如UPDATE users SET status=1 WHERE phone='138...',而
phone没索引 → 全表扫描 → 锁住成千上万行 → 别的事务一碰就等。用
EXPLAIN看执行计划,确认
type是
ref或
range,不是
ALL长事务没提交:事务里混了 RPC 调用、文件读写、sleep(1)……导致锁持有时长远超预期。查
INNODB_TRX表里的
trx_started和
trx_state,凡超过 5 秒未提交的都该预警 批量插入撞上自增锁:MySQL 5.6/5.7 中
innodb_autoinc_lock_mode = 1(默认)时,
INSERT INTO t VALUES (),(),...会锁住整个自增计数器,直到语句结束。改成
2可缓解,但需重启;更稳妥的是拆成小批量或用显式主键
代码与事务设计避坑要点
很多锁等待问题,根源不在数据库,而在应用层怎么用它:
Spring @Transactional 默认传播行为是REQUIRED,如果一个方法里先查再更新,且没加
readOnly=true,那整个方法都在事务里——哪怕查操作根本不需要锁 避免“先 SELECT FOR UPDATE 再 UPDATE”的惯性写法,除非真需要校验中间状态;多数情况直接
UPDATE ... WHERE ...更轻量 多表更新务必固定顺序:比如总是先更新
orders再更新
order_items,否则 A 事务按此顺序、B 事务反着来,死锁就来了 ORM 自动生成 SQL 时,where 条件顺序可能不一致(如 MyBatis 动态 SQL 字段顺序随机),也会引发隐式锁序混乱,建议关键更新用固定字段顺序的 XML 显式写
锁等待超时从来不是孤立错误,它是并发逻辑、SQL 质量、事务边界三者共同暴露的切口。盯着
INNODB_TRX和慢查询日志交叉比对,比盲目调参管用十倍。
