ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MySQL 1205 锁等待超时排查手册:从锁机制到根治方案

2026/10/7 17:13:51 拓冰建站 浏览量
MySQL 1205 锁等待超时排查手册:从锁机制到根治方案 如果某天你负责的应用突然告警日志里反复出现1213 - Deadlock found或者1205 - Lock wait timeout exceeded; try restarting transaction大部分人的第一反应是重启数据库或者重启应用。说实话我见过太多同行为这个报错折腾到半夜最后发现根本不是 MySQL 挂了而是事务锁互相等太久被 InnoDB 直接掐断了。1205 这个错误在 MySQL 和 MariaDB 里非常常见它不是一个致命故障而是一个业务并发和事务设计有问题的信号。这篇文章我结合自己排查线上问题的经验从锁机制讲起把触发场景、定位方法、根治方案和日常预防整理成一套可以直接拿去用的排查手册。不管是刚接触数据库的新手还是已经带团队的后端同学按这个思路走大部分 1205 都能快速解决。1. 先搞清楚 1205 到底在说什么1.1 报错拆解Lock wait、timeout、restarting transaction1205 (HY000): Lock wait timeout exceeded; try restarting transaction是三段信息的组合。Lock wait timeout说的是 InnoDB 存储引擎在等待某个锁的时候超时了try restarting transaction是给客户端的一个建议让你把这个事务重新跑一遍。注意这里的 restart 不是让你重启 MySQL 服务而是应用端要把这个事务回滚后再重新执行。这个区别很关键我在排查工单时经常看到有人因为理解错含义直接把实例重启了结果锁没释放业务中断时间反而更长数据一致性也没法保证。InnoDB 是行级锁机制事务 A 更新了某一行但还没提交事务 B 想更新同一行就必须等 A 释放锁。这个等待不是无限的由参数innodb_lock_wait_timeout控制默认 50 秒。如果 50 秒内 A 还没提交或回滚B 就会收到 1205 错误。它的本质是我等得太久不等了属于一种自我保护避免事务无限期挂起拖着整个系统。1.2 行锁、间隙锁和 MVCC 的基本关系要真正理解 1205得先拎清楚 InnoDB 的锁都有哪几种。按粒度分有表级锁LOCK TABLES显式加的或者 DDL 时的元数据锁和行级锁Record Lock。按模式分有共享锁S LockSELECT ... LOCK IN SHARE MODE或SELECT ... FOR SHARE和排他锁X LockSELECT ... FOR UPDATE、UPDATE、DELETE。行锁只在事务隔离级别和索引条件下发挥作用在默认的REPEATABLE READ隔离级别下InnoDB 还会加间隙锁Gap Lock和临键锁Next-Key Lock防止幻读。MVCC多版本并发控制是 InnoDB 实现高并发读的核心机制普通SELECT走的是快照读不加锁所以跟写操作不冲突。但一旦你用了FOR UPDATE、FOR SHARE或者执行UPDATE、DELETE就变成了当前读必须拿最新的锁信息也就不可避免地进入了锁等待队列。很多 1205 就是从这里冒出来的——明明业务里大部分都是普通查询结果一两个写操作和一个长事务撞在一起队列瞬间堵死。1.3 1205 和 1213 死锁的区别别混为一谈1205 和 1213 是两种完全不同的错误。1213 是 InnoDB 在等待关系中发现循环等待检测机制认为死锁必然发生主动回滚了其中一个事务让另一个继续。1205 则只是单方面等待超时并不一定构成环可能是对方事务持有锁时间过长也可能是排队的事务太多当前事务排在后面等着时间耗尽。简单概括1213 是环路属于 InnoDB 主动干预1205 是饿肚子属于被动放弃。线上有个常见现象一条慢查询把一个表里几万行都锁住了后面所有写请求排着队最前面的线程等到 50 秒超时后报 1205这个场景根本不存在死锁环纯粹是前车不走、后车全堵。2. 常见的触发场景基本都是这几类2.1 事务开着不提交锁占着不放这是 1205 出现频率最高的一类原因而且往往不是故意为之。典型情况是应用代码里关了autocommit或者用Transactional包了一个包含外部接口调用的大方法。事务开启后执行了UPDATE然后去调外部 HTTP 接口等对方响应等了十几秒甚至几十秒数据库这边锁一直握着。另一个并发的请求恰好要改同一行就只能干等着。更隐蔽的是程序抛异常后没有正确回滚连接池里的连接还带着未提交的事务下次被其他线程复用锁依然存在。我在排查时习惯先查INNODB_TRX里有没有长时间RUNNING的事务这类问题通常一眼就能看到。2.2 慢 SQL 缺索引把行锁放大成变相表锁第二个高频原因是 SQL 本身写得不行。UPDATE或DELETE的WHERE条件没有走索引InnoDB 扫描全表时会对扫描过的每一行加锁这个现象常被大家称作锁全表——实际上它仍然是行锁但等于把全表每一行都锁上了效果和表锁没什么区别。比如订单表有 500 万行你执行UPDATE orders SET status 1 WHERE customer_id 20240001而customer_id字段上压根没有索引这条 SQL 就会扫全表把碰过的行全部加锁其他任何订单的更新都被挡住。等到扫描结束准备提交时后面排队的事务已经超时了。2.3 批量任务和人工操作 DML 给线上添乱还有一类很经典运营同学在测试环境跑了一个UPDATE忘了加WHERE条件或者研发直接连生产库执行大范围更新。这种手动事务如果执行时间超过 50 秒会连累正常业务查询后面的写操作全部超时。很多 DBA 都接过类似的电话排查半天发现没有任何应用异常就是有人用命令行开了一个事务执行完忘了COMMIT。这种事情在团队里很难完全杜绝只能靠规范和巡检来兜底。平时在测试环境操作可以随意生产环境的大批量 DML 一定要拆批并且安排到业务低峰期。2.4 大事务批量更新/删除锁覆盖范围远超预期批量处理业务里常见的写法是循环执行UPDATE一个事务里更新几万行甚至几十万行。这种大事务持有锁的数量和时长都很大一旦两个批处理任务同时跑互相等待的概率就很高。比如一个任务更新订单 A~M 的区间另一个任务更新订单 K~Z 的区间在间隙锁和临键锁的作用下两者可能产生重叠区域的等待后启动的那个事务就会逐渐逼近 1205 的阈值。批量任务不是不能做而是必须控制每个事务的规模分批提交给其他事务留出能插队的机会。3. 现场断案定位阻塞源的完整路径3.1 第一板斧SHOW ENGINE INNODB STATUS 看锁战士收到 1205 告警后第一步不是杀连接而是先看 InnoDB 的现场快照。执行SHOW ENGINE INNODB STATUS\G重点看TRANSACTIONS段里面有当前活跃事务列表、每个事务持锁和等待的情况。官方对这条命令的描述是只能反映最近一次死锁的信息但实际上它对锁等待也有很强的参考价值。如果能看到类似LOCK WAIT的线程状态配合下面的 trx id 就能顺藤摸瓜找到谁在阻塞谁。mysql SHOW ENGINE INNODB STATUS\G输出里的TRANSACTIONS段会列出一堆事务的十六进制 ID比如TRANSACTION 422138904, ACTIVE 120 sec这就是事务已经活了 120 秒还没提交。后面跟着的LOCK WAIT信息里会指明它在等待哪个 lock以及holding trx id是谁。把这些 ID 记住再去查information_schema就能确认具体连接和 SQL。3.2 第二板斧information_schema 三张表交叉验证performance_schema和information_schema里有一组专门记录锁状态的表。在 MySQL 5.7 及之前是INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITSMySQL 8.0 把后两张表换成了performance_schema.data_locks和performance_schema.data_lock_waits。无论哪个版本INNODB_TRX都是必查的它直接告诉你当前有哪些正在运行的事务、开始时间、状态、执行的 SQL 是什么。SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query, trx_rows_locked, trx_rows_modified FROM information_schema.INNODB_TRX\G重点看trx_state是不是LOCK WAIT以及trx_started距离现在多久。一个事务如果trx_started超过几百秒那它大概率就是元凶。在 5.7 里还能用INNODB_LOCK_WAITS直接查到谁在等谁的锁SELECT * FROM information_schema.INNODB_LOCK_WAITS\G它会给出requested_lock_id、requesting_trx_id、blocking_lock_id和blocking_trx_id把阻塞关系列得明明白白。3.3 第三板斧sys 库一键定位省时省力如果你用的是 MySQL 5.7 以上版本sys库里有一个封装好的视图sys.innodb_lock_waits本质上就是把前面几张表JOIN了一次把它该输出的关键字段直接给你等待锁的事务 ID、对应的进程 ID、执行的 SQL以及阻塞来源的进程 ID 和 SQL。用好这个视图定位时间能缩短到一分钟以内。SELECT waiting_trx_id, waiting_pid, waiting_query, blocking_trx_id, blocking_pid, blocking_query FROM sys.innodb_lock_waits\GMySQL 8.0 下这个视图底层走了performance_schema.data_lock_waits可用性依然很稳定。我一般到这一步就能确定要不要KILL了。3.4 现场处置怎么安全地释放锁找到阻塞源后处置手段分两种情况。如果阻塞事务明显是异常事务比如一直RUNNING了几百秒且 SQL 是一个大范围更新那就直接KILL它对应的线程 ID。注意要KILL的是trx_mysql_thread_id对应的连接不是trx_id。执行前最好跟业务方确认一下避免误杀正在进行的合理操作。KILL 123456;如果这个事务卡在外部调用上KILL之后应用会收到异常事务回滚锁立刻释放。还有一种情况是阻塞方是正常业务、只是执行很慢这时候强行KILL意义不大应该从慢 SQL 优化入手或者等待它执行完。判断标准很简单看它的执行时间和业务性质而不是一味地杀。4. 根治方案从参数到业务的逐层优化4.1 innodb_lock_wait_timeout 怎么调才不算乱调把innodb_lock_wait_timeout默认的 50 秒调大是很多团队第一时间想到的办法。但调参要讲逻辑这个参数的本意是让事务等待更久一点给慢事务更多时间完成从而减少 1205 报错。但调得过大有一个隐患就是应用线程会长时间占据连接池中的连接后续排队请求越积越多最终可能导致连接池耗尽报Cant connect to MySQL server之类的错误。从我的经验看如果没有特殊的长事务场景不建议把innodb_lock_wait_timeout调超过 120 秒。反而可以把线上值调小一点比如 20~30 秒让失败的请求快速失败给应用重试留出余量而不是让每个请求都卡在数据库连接上。生产环境修改方式SET GLOBAL innodb_lock_wait_timeout 30; SET SESSION innodb_lock_wait_timeout 30;同时记得把配置写进my.cnf的[mysqld]段否则实例重启后又会回到 50 秒的默认值。4.2 索引优化让锁精确命中该锁的行大部分业务表的锁竞争都源于更新条件没有走索引。我给团队做培训时经常打一个比方行锁就像小区里的车位锁你要锁的是你自己的车位结果因为没有明确标识你挨个把整个停车场所有车位都摸了一遍锁了一遍别人根本没法停车。索引就是那个车位标识。给WHERE条件上的列建合适的索引能让 InnoDB 精准锁定目标行锁的规模和持有时间都会大幅下降。ALTER TABLE orders ADD INDEX idx_customer_id (customer_id);但索引也不是建得越多越好联合索引的顺序、覆盖索引的使用都会影响效果。最直接的办法是先用EXPLAIN验证UPDATE或DELETE的type是不是ref或const如果还是ALL全表扫描说明索引没有生效继续优化。4.3 事务粒度与提交频率是业务侧的关键动作就算索引到位如果业务逻辑里一个事务要更新一万行才提交1205 仍然会找上门。事务是越小越好越快提交越好。常见的改造方案有三种一是把大事务拆成小事务比如处理一万条数据就分成十个批次每批一千条提交一次二是把外部调用挪出事务范围先查数据、调接口、拿到结果后再进事务做最终更新三是在一个方法里尽量只更新必要的数据项不要把无关表的数据放进同一个事务。批量操作参考模板-- 把一次更新 100 万行拆成 1000 次循环每次 1000 行 UPDATE orders SET status 1 WHERE status 0 AND id BETWEEN 1 AND 1000; COMMIT; -- 下一批, id 范围推进 UPDATE orders SET status 1 WHERE status 0 AND id BETWEEN 1001 AND 2000; COMMIT;这样每条 SQL 持锁时间极短其他事务几乎感觉不到阻塞。注意每批之间最好加一个sleep(0.1)之类的小间隔给数据库一点喘息空间也避免主从复制跟不上产生延迟。4.4 架构层面减少锁竞争的可选手段如果业务并发确实很高同一行数据被大量并发更新光靠 SQL 优化可能还不够。可以考虑的方案包括异步队列削峰、把同一行的更新合并到批处理任务里、或者引入消息队列让写操作排队执行。也有团队为了让事务不阻塞把SELECT ... FOR UPDATE改成乐观锁方案也就是在表里加一个version字段UPDATE时带上WHERE version 旧值如果影响行数为 0 就重试。这个方案能大幅度降低锁等待但要求业务能接受偶尔的重试。方案没有绝对的好坏取决于你的业务对一致性和延迟的容忍度我建议先把第 4.2 和 4.3 节做完再考虑架构调整。5. 实战复盘两个真实案例带你走完整流程5.1 案例一报表查询怎么堵住了一大片写请求当时的情况是晚上 9 点线上突然出现大量 1205 告警集中的错误全指向一张交易表。按第 3 节的流程查下去INNODB_TRX里发现一个事务已经RUNNING了 280 秒trx_query是一条按时间和用户维度统计的报表 SQL。问题就出在它为了统计先执行了一个SELECT ... FOR UPDATE把当天的大部分交易记录加了锁然后还在做聚合计算。后面的支付回调请求都在等这些记录上的锁排队超过 50 秒就全部报 1205。处置动作分两步先跟业务确认这个查询属于非实时报表然后执行KILL释放锁让支付请求恢复随后把报表 SQL 改成普通快照读SELECT去掉FOR UPDATE因为报表根本不需要加锁用 MVCC 就能保证一致性。从那之后这个错误就再没出现。这里的教训是很多人以为FOR UPDATE能让报表更准确实际上在默认隔离级别下快照读已经足够盲目加锁不仅没必要还会连累业务。5.2 案例二批量脚本忘记 COMMIT半夜排队全堵死另一个案例是运营团队在凌晨跑一个会员积分迁移脚本脚本逻辑是先读取一批会员 ID然后逐个UPDATE积分表。因为忘了在循环里COMMIT几千个会员的更新全部累加在一个事务里这个事务持有了大量行锁。凌晨流量不高没立刻暴露问题但早上 7 点业务高峰一上来所有涉及积分表的写操作全部开始报 1205。查日志时发现这个事务竟然已经RUNNING了 5 小时十分夸张。当时也不需要分析太多直接把对应线程KILL然后把脚本改造为每处理 500 个会员提交一次的批次模式同时加了日志打点每个批次记录处理进度。脚本重跑后就再没出过问题。这个案例告诉我一个道理所有批量脚本只要碰生产库就必须强制要求分批提交并打印进度这应该写进团队的发布规范里。6. 日常预防让 1205 在发生前就被发现6.1 关注锁相关的状态指标监控系统里应该盯几个关键指标Innodb_row_lock_waits累计等待次数、Innodb_row_lock_time累计等待时间、Innodb_row_lock_time_avg平均等待时间。这些状态变量会持续累积观察它们的变化趋势能判断锁竞争是否在恶化。SHOW GLOBAL STATUS LIKE Innodb_row_lock%;另外Threads_running、Threads_connected也要一起看锁等待增多时线程数通常会同步上升。告警阈值不需要设得太敏感我一般建议当Innodb_row_lock_waits在 5 分钟内增长超过 500 次或者Innodb_row_lock_time_avg持续大于 1000 毫秒时触发提醒具体数值要根据业务体量调整。6.2 建立日常巡检脚本巡检脚本的核心是定时检查长事务。一个简单可靠的做法是每分钟跑一次查询找出执行时间超过 60 秒的事务把结果写入日志并推送到告警平台。SELECT trx_mysql_thread_id AS pid, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS running_seconds, trx_query AS query FROM information_schema.INNODB_TRX WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) 60 ORDER BY running_seconds DESC;发现长事务后可以先通知业务方确认是否正常如果不正常立刻KILL。这套巡检机制帮我们提前发现过很多起人为操作失误和慢 SQL 拖垮事务的问题比事后看告警舒服太多。巡检不需要很复杂的工具一个 Crontab 加一个 Shell/Python 脚本足够。6.3 规范落地的关键点最后简单聊聊团队规范。1205 大量出现的团队往往缺少几条基本共识事务里不放外部调用、批量操作必须分批提交、生产环境 DML 需要走审批并附带影响行数、SELECT ... FOR UPDATE必须经过代码评审才能使用。这些规则不复杂但能坚持执行下来数据库层面会省掉非常多麻烦。我建议团队把第 5 章里的两个案例整理成文档发给每个后端开发比口头喊一百遍注意锁有效得多。我在实际排查 1205 的过程中还有一个体会报错本身不可怕可怕的是把它当成偶然错误去重启了事问题第二天必定再来。你只要顺着谁在持锁、谁在等锁、锁的跨度多大、事务多久才提交这四个问题追下去九成以上的 1205 都能在十分钟内找到根因。下次再看到try restarting transaction时先平静地打开INNODB_TRX查一查那个躲在后半夜的锁霸就跑不掉了。