
我不会起名字322· 后端 / 算法 / 数据库 技术栈 力扣 Hot100 Go 项目 Redis MySQL文章目录一条 UPDATE 等了 30 秒InnoDB 锁等待的完整排查链路一、先把错误信息看全二、第一步确定谁在等谁三、第二步找到那个不提交的事务3.1 应用代码里忘了提交3.2 长事务查询3.3 连接泄漏 / 连接池未回收四、第三步看懂死锁日志五、第四步确认是不是没走索引六、第五步区分锁的类型七、第六步改法7.1 缩短事务收益最大7.2 让所有事务按同一顺序访问7.3 加索引或改 SQL7.4 拆分大事务7.5 调整超时时间治标7.6 允许的话用读已提交八、一份可以贴到运维手册的排查脚本小结一条 UPDATE 等了 30 秒InnoDB 锁等待的完整排查链路线上告警Lock wait timeout exceeded; try restarting transaction。你正准备去查发现它自己好了过了两小时又来一次。想复现手动跑那条 SQL 又快得很。这类偶发、不可复现、重启就好的锁等待排查难点不在于 SQL 本身而在于你到现场的时候现场已经没了。这篇给出一条完整的排查链路从谁在等谁到谁在持有锁不释放再到下次怎么不让它发生。一、先把错误信息看全Lock wait timeout exceeded的完整信息里有两个关键数字ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction但更详细的信息在 InnoDB 层要在报错时立刻抓SHOWENGINEINNODBSTATUS\G在输出里找LATEST DETECTED DEADLOCK和TRANSACTIONS两段。注意一个坑SHOW ENGINE INNODB STATUS只保留最近一次死锁的信息而且它显示的是当前快照锁等待结束后就查不到了。所以告警触发时要立刻抓或者提前让它落盘-- 开启后InnoDB 会把死锁信息写进错误日志事后可查SETGLOBALinnodb_print_all_deadlocksON;这个参数建议生产环境常开。它只在发生死锁时写日志开销可以忽略但能让你事后看到完整的死锁现场。二、第一步确定谁在等谁MySQL 5.7 及以上information_schema里有三张表是主力-- 当前正在等待的锁SELECT*FROMinformation_schema.INNODB_LOCK_WAITS;-- 当前持有的锁8.0 里改名叫 performance_schema.data_locksSELECT*FROMinformation_schema.INNODB_LOCKS;-- 当前活跃事务SELECT*FROMinformation_schema.INNODB_TRX;一条能直接用的联合查询MySQL 5.7SELECTr.trx_idASwaiting_trx_id,r.trx_mysql_thread_idASwaiting_thread,r.trx_queryASwaiting_query,b.trx_idASblocking_trx_id,b.trx_mysql_thread_idASblocking_thread,b.trx_queryASblocking_query,b.trx_startedASblocking_started,TIMESTAMPDIFF(SECOND,b.trx_started,NOW())ASblocking_age_secFROMinformation_schema.INNODB_LOCK_WAITS wJOINinformation_schema.INNODB_TRX bONb.trx_idw.blocking_trx_idJOINinformation_schema.INNODB_TRX rONr.trx_idw.requesting_trx_idORDERBYblocking_age_secDESC;MySQL 8.0 的写法不一样INNODB_LOCKS被移除了SELECTwaiting_pid,waiting_query,blocking_pid,blocking_query,wait_age,sql_kill_blocking_queryFROMsys.innodb_lock_waits;8.0 的sys.innodb_lock_waits是官方封装好的视图还贴心地给了sql_kill_blocking_query字段 —— 直接复制那一列执行就能 kill 掉阻塞者。看到结果后重点看blocking_age_sec阻塞事务已运行时长说明几秒正常的短事务竞争多半是热点行几十秒到几分钟事务里有慢查询、或者事务没提交几小时几乎可以肯定是有连接忘了提交僵尸事务拿到blocking_thread之后就可以决定是 kill 还是继续查KILL12345;-- 填 blocking_thread 的值生产上不要无脑 KILL。先看一下blocking_query是什么如果这个事务已经跑了很久且持有很多锁kill 之后它的回滚可能要花更长时间。判断标准是kill 一个小事务很快kill 一个改了几十万行的大事务回滚可能要几分钟期间锁还在。三、第二步找到那个不提交的事务绝大多数偶发锁等待根因都是某个连接开了事务却长时间不提交。常见来源有三个。3.1 应用代码里忘了提交典型的错误模式Transactionalpublicvoidprocess(ListLongids){for(Longid:ids){// 中间调用了外部接口可能几秒才返回remoteClient.notify(id);orderMapper.updateStatus(id,1);}}// 事务在整个循环结束才提交期间所有 update 的行锁一直持有在事务里做远程调用是最常见的反模式。事务应该只包含数据库操作RPC、发消息、读写文件都要挪到事务外面。3.2 长事务查询-- 找出运行超过 60 秒的事务SELECTtrx_id,trx_mysql_thread_id,trx_started,TIMESTAMPDIFF(SECOND,trx_started,NOW())ASage_sec,trx_rows_locked,trx_rows_modified,LEFT(trx_query,100)ASqueryFROMinformation_schema.INNODB_TRXWHERETIMESTAMPDIFF(SECOND,trx_started,NOW())60ORDERBYage_secDESC;关注trx_rows_locked和trx_rows_modified如果一个事务锁了几万行但只改了几行说明它的扫描范围太大多半是没走索引。3.3 连接泄漏 / 连接池未回收-- 看当前所有连接的状态和时长SELECTid,user,host,db,command,time,state,LEFT(info,80)ASinfoFROMinformation_schema.PROCESSLISTWHEREcommand!SleepORtime300ORDERBYtimeDESC;如果看到大量Sleep状态但time很大的连接说明应用拿了连接没还或者连接上的事务没提交。这类连接持有的锁要等连接被回收才释放。四、第三步看懂死锁日志死锁和锁等待超时是两回事锁等待是我等你死锁是我等你、你等我InnoDB 会主动回滚其中一个。死锁日志长这样------------------------ LATEST DETECTED DEADLOCK ------------------------ 2026-10-09 10:23:45 0x7f8e4c0b9700 *** (1) TRANSACTION: TRANSACTION 123456, ACTIVE 5 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s) MySQL thread id 100, OS thread handle 12345, query id 999 updating UPDATE orders SET status 2 WHERE user_id 1001 *** (1) HOLDS THE LOCK(S): RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY of table shop.orders trx id 123456 lock_mode X locks rec but not gap *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 58 page no 4 n bits 72 index idx_user of table shop.orders trx id 123456 lock_mode X locks rec but not gap waiting *** (2) TRANSACTION: TRANSACTION 123457, ACTIVE 4 sec starting index read ... *** WE ROLL BACK TRANSACTION (1)读这段日志的顺序看两个事务各自的 SQLupdating后面那句—— 这是最重要的信息看HOLDS THE LOCK(S)已经拿到了什么锁在哪个索引上index PRIMARY还是index idx_user看WAITING FOR THIS LOCK在等什么锁、等哪个索引上的锁看WE ROLL BACK TRANSACTION (n)InnoDB 回滚了哪个通常是影响行数少的那个。一个高频结论如果日志里两个事务分别持有主键索引和二级索引的锁那基本可以断定是先用二级索引定位、再回表更新主键的顺序问题。解决思路是让所有事务按同一顺序访问。五、第四步确认是不是没走索引这是最容易被忽略、却最常见的原因。InnoDB 的行锁是加在索引上的如果 UPDATE 的 WHERE 条件没走索引就会升级成锁全表更准确地说锁住扫描过的所有行。-- 一定要看执行计划EXPLAINUPDATEordersSETstatus2WHEREphone13800138000;重点看三列列健康的值危险信号typeref/range/constALL全表扫key有索引名NULLrows接近实际影响行数远大于实际影响行数特别注意两个隐性陷阱陷阱一类型不匹配导致索引失效。-- phone 是 varchar但传了数字 → 索引失效全表扫UPDATEordersSETstatus2WHEREphone13800138000;陷阱二隐式字符集转换。-- 关联的两张表字符集/排序规则不一致索引也会失效SELECT*FROMaJOINbONa.codeb.code;六、第五步区分锁的类型看到lock_mode后面的关键字能判断锁的范围关键字含义出现条件locks rec but not gap记录锁只锁这一行唯一索引等值命中locks gap before rec间隙锁锁的是区间范围条件、或等值未命中next-key lock记录锁 间隙锁默认的 RR 隔离级别下的行锁形式insert intention插入意向锁INSERT 时一个常见误解行锁不是只锁一行。在 RR可重复读MySQL 默认隔离级别下InnoDB 用的是 next-key lock锁的是这一行 前面的间隙。所以WHERE id 100这种范围条件会把 id 大于 100 的所有现有记录和间隙都锁住包括还不存在的行 —— 这就是为什么并发插入会互相阻塞。判断隔离级别SELECTtransaction_isolation;-- 8.0SELECTtx_isolation;-- 5.7七、第六步改法按优先级排序从最有效开始。7.1 缩短事务收益最大// 错误RPC 在事务里Transactionalpublicvoidhandle(Ordero){remoteClient.push(o);// 几百毫秒到几秒mapper.update(o);// 锁在这个时刻才真正加上但要等 RPC 完才释放}// 正确事务只包住数据库操作publicvoidhandle(Ordero){remoteClient.push(o);// 挪到事务外doUpdate(o);// 只有这个方法是事务的}TransactionalpublicvoiddoUpdate(Ordero){mapper.update(o);}7.2 让所有事务按同一顺序访问如果死锁是因为两个业务按相反顺序更新同一批行那就统一顺序-- 统一按主键升序更新UPDATEordersSETstatus2WHEREidIN(5,3,9)ORDERBYid;批量更新时显式加ORDER BY id可以显著降低死锁概率。7.3 加索引或改 SQL确保 WHERE 条件走索引把锁的范围从扫描过的所有行缩小到真正要改的那几行。7.4 拆分大事务// 一次更新 10 万行 → 拆成每批 500 行for(inti0;iids.size();i500){ListLongbatchids.subList(i,Math.min(i500,ids.size()));updateBatch(batch);// 每批独立事务}大事务的问题不只是锁范围大还有回滚慢、主从延迟、undo 膨胀。7.5 调整超时时间治标-- 默认 50 秒可以调小让失败快速暴露SETGLOBALinnodb_lock_wait_timeout10;调小意味着更快报错但也可能把本来能等到的请求也拒掉。适合做兜底保护不要当成解决方案。7.6 允许的话用读已提交SETGLOBALtransaction_isolationREAD-COMMITTED;RC 级别下间隙锁会大幅减少大部分场景下没有间隙锁死锁概率明显下降。代价是失去了可重复读的保证而且binlog 格式必须是 ROW否则主从数据会不一致。八、一份可以贴到运维手册的排查脚本-- 1. 谁在等谁5.7SELECTr.trx_mysql_thread_idASwaiting_thread,r.trx_queryASwaiting_sql,b.trx_mysql_thread_idASblocking_thread,b.trx_queryASblocking_sql,TIMESTAMPDIFF(SECOND,b.trx_started,NOW())ASblocking_age_secFROMinformation_schema.INNODB_LOCK_WAITS wJOINinformation_schema.INNODB_TRX bONb.trx_idw.blocking_trx_idJOINinformation_schema.INNODB_TRX rONr.trx_idw.requesting_trx_id;-- 2. 长事务SELECTtrx_mysql_thread_id,trx_started,trx_rows_locked,trx_rows_modified,TIMESTAMPDIFF(SECOND,trx_started,NOW())ASage_sec,LEFT(trx_query,80)FROMinformation_schema.INNODB_TRXWHERETIMESTAMPDIFF(SECOND,trx_started,NOW())30ORDERBYage_secDESC;-- 3. 长时间 Sleep 的连接可能是没提交的事务SELECTid,user,host,db,command,time,LEFT(info,80)FROMinformation_schema.PROCESSLISTWHEREcommandSleepANDtime300ORDERBYtimeDESC;-- 4. 死锁现场只在刚发生时有效SHOWENGINEINNODBSTATUS\G配套的预防措施-- 死锁信息落盘事后可查SETGLOBALinnodb_print_all_deadlocksON;-- 锁等待超时调小快速失败SETGLOBALinnodb_lock_wait_timeout10;小结排查顺序是谁在等谁 → 谁不提交 → 看死锁日志 → 确认有没有走索引 → 判断锁类型 → 改。8.0 用sys.innodb_lock_waits5.7 用INNODB_LOCK_WAITS联合INNODB_TRX两者写法不同。innodb_print_all_deadlocks ON建议常开死锁现场转瞬即逝不落盘就永远查不到。绝大多数偶发锁等待的根因是事务里做了非数据库操作RPC、IO导致锁持有时间被拉长。行锁加在索引上WHERE 不走索引会让锁范围从几行变成全表。改法优先级缩短事务 统一访问顺序 加索引 拆大事务调innodb_lock_wait_timeout只是兜底。