一条UPDATE让订单表卡死40分钟,根因是Sleep了72分钟的那个连接

简介: 一条中午就该提交的UPDATE,在事务里挂了70多分钟,整张订单表堵死40分钟。文章从processlist里不起眼的Sleep连接讲起,拆解MDL排队机制、lock_wait_timeout默认一年的坑、行锁与元数据锁两套超时的差异,以及RR隔离级别下长事务如何钉住purge水位线、让undo只涨不缩。最后给出commit与kill的判断标准,以及长事务监控与DDL变更的治理方法。

大家好,我是数据库小学妹👋我踩过的坑,你别再踩。

周四下午两点多,开发小刘在群里喊我:订单表卡死了,所有查询都在转圈。我第一反应是有慢查询,登上去翻了一遍才发现不是。没有任何一条SQL特别慢,是整张表的新请求全被堵住了,谁都不动。顺着连接和锁一路挖下去,根子是一条中午12点多就该提交的UPDATE。小刘跑完订正就去吃饭了,事务一直没提交,在后台空挂了七十多分钟。等我定位到源头,离出事已经快一个小时。

今天想把这条链完整拆一遍,从processlist里那行Sleep说起,到ALTER怎么把全表读写一起堵死,再到后台悄悄涨起来的undo,一整套都捋清楚。

一、现象:全表冻结,先看processlist

遇到卡死,我习惯先拉一把线程。SHOW FULL PROCESSLIST一敲,问题基本就现形了。

+-----+---------+--------+--------+------+-------------------------------+------------------------------------+
| Id  | User    | db     | Command| Time | State                         | Info                               |
+-----+---------+--------+--------+------+-------------------------------+------------------------------------+
| 213 | dba_dev | appdb  | Sleep  | 4320 |                               | NULL                               |
| 187 | admin   | appdb  | Query  | 300  | Waiting for table metadata lock| ALTER TABLE orders ADD INDEX ...    |
| 156 | api_rw  | appdb  | Query  | 120  | Waiting for table metadata lock| SELECT * FROM orders WHERE ...      |
| 155 | api_rw  | appdb  | Query  | 118  | Waiting for table metadata lock| UPDATE orders SET ...               |
+-----+---------+---------+--------+------+-------------------------------+------------------------------------+

看State那一列,一大半会话都是Waiting for table metadata lock,堵点全指向orders。真正被卡住的不是某一条SQL,是所有要碰这张表的操作。

最扎眼的反而是Id 213:Command是Sleep,Time已经4320秒,Info空着。没经验的人会把它当成普通空闲连接直接跳过,可它才是整条链的源头。最坑的也在这:事务还开着,命令状态却是Sleep,光看processlist根本发现不了。

二、定位元凶:一条命令看排队,一条命令看事务

MDL的排队关系,直接查performance_schema.metadata_locks这张表就能看明白。

SELECT OBJECT_SCHEMA, OBJECT_NAME,
       LOCK_TYPE, LOCK_STATUS, PROCESSLIST_ID
FROM performance_schema.metadata_locks ml
JOIN performance_schema.threads t
  ON t.THREAD_ID = ml.OWNER_THREAD_ID
WHERE OBJECT_SCHEMA = 'appdb' AND OBJECT_NAME = 'orders';
+---------------+-------------+---------------+-------------+----------------+
| OBJECT_SCHEMA | OBJECT_NAME | LOCK_TYPE     | LOCK_STATUS | PROCESSLIST_ID |
+---------------+-------------+---------------+-------------+----------------+
| appdb         | orders      | SHARED_WRITE  | GRANTED     | 213            |
| appdb         | orders      | EXCLUSIVE     | PENDING     | 187            |
| appdb         | orders      | SHARED_READ   | PENDING     | 156            |
| appdb         | orders      | SHARED_WRITE  | PENDING     | 155            |
+---------------+-------------+---------------+-------------+----------------+

看到这张表,问题就清楚了:213攥着SHARED_WRITE不放,187的ALTER要EXCLUSIVE,在排队。156、155这些SELECT、UPDATE又排在ALTER后头。一个等一个,全堵在orders上。

接着得搞清213到底开了个什么事务。查information_schema.innodb_trx

SELECT trx_id, trx_state, trx_started,
       TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS run_seconds,
       trx_mysql_thread_id, trx_query, trx_rows_modified
FROM information_schema.innodb_trx
ORDER BY trx_started;
+----------+-----------+---------------------+-------------+---------------------+-----------+-----------------+
| trx_id   | trx_state | trx_started         | run_seconds | trx_mysql_thread_id | trx_query | trx_rows_modified|
+----------+-----------+---------------------+-------------+---------------------+-----------+-----------------+
| 231472   | RUNNING   | 2026-09-04 13:36:12 | 4320        | 213                 | NULL      | 3125643         |
+----------+-----------+---------------------+-------------+---------------------+-----------+-----------------+

trx_state是RUNNING,trx_query却是NULL,trx_rows_modified三百多万行。这三个值凑一起,事情基本就清楚了:SQL早就跑完,当前没有语句在跑,事务却一直没提交,还改了一票大的。这种状态圈里叫idle in transaction。活干完了,事务不结束,锁全攥在手里。小刘中午跑订正,UPDATE执行完没提交,窗口一挂就是一下午,人早去吃饭了。

三、为什么一条UPDATE能堵住整张表:MDL的排队机制

先说个矛盾:213改的是订单表里不同的行,按说行锁不该堵住全表。真正把整张表卡住的不是行锁,是MDL(元数据锁)。这个点很多人搞混,值得拆开讲。

DML执行时会在表上加一把SHARED_WRITE元数据锁,而且要到事务提交才释放。ALTER这类DDL要的是EXCLUSIVE锁,和SHARED_WRITE互斥,只能等213先交。如果只到这一步,顶多ALTER自己等一等,全表不至于冻住。

真正的坑在排队规则。MySQL的MDL有个防止写者饿死的机制:一旦有EXCLUSIVE在排队,后面新到的SHARED_READ、SHARED_WRITE就算彼此能共存,也得老实排在EXCLUSIVE后面。于是187的ALTER一排队,156的SELECT、155的UPDATE全被挡在门外。ALTER不是自己慢,它是把整张表的口子堵死了。

还有个更阴的坑,MDL的等待超时跟行锁不是一套参数。

锁类型 谁在等 超时参数 默认值
MDL ALTER等DDL lock_wait_timeout 31536000秒(一年)
行锁 UPDATE/DELETE改同一行 innodb_lock_wait_timeout 50秒

行锁等50秒会自己报错退出,MDL默认却是一年。所以那条ALTER安安静静站队,既不超时也不报错,整张表就这么被拖了40分钟,中间没有任何环节会主动喊停。

那UPDATE自己改过的三百多万行呢?它们身上的记录锁也要等事务结束才释放,后面想改这些行的DML会先卡在行锁上。但这次行锁不是主角,MDL才是。两套锁分不清,排查就会跑偏。

四、后台还在胀:长事务把purge钉死了

锁堵在明面上,一眼能看见。长事务留下的不止这笔,还有一层看不见的,叫undo膨胀。这笔一样得算。

小刘那个事务不是一上来就UPDATE的。他先跑SELECT确认要改哪些数据,看完才动手。在MySQL默认的RR(可重复读)隔离级别下,这条SELECT会创建一个一致性快照ReadView,快照要活到事务结束才算完。

问题出在这:InnoDB的purge线程清理历史版本时,只清得掉“比所有活跃ReadView都旧”的版本。213把ReadView钉在下午一点多,等于把purge的水位线锁死。之后别的事务提交产生的历史版本全清不掉,History list length只能一路涨。

SHOW ENGINE INNODB STATUS\G
------------
TRANSACTIONS
------------
History list length 26895

UPDATE那三百多万条undo(也就是改动前的旧值,回滚时全靠它)也得一直留着,事务不结束就释放不了。undo表空间的truncate一样被活跃事务挡住,只涨不缩。真拖到晚上,磁盘告警就会来敲门。长事务的可怕不在它自己慢,在它把一堆后台机制全卡住了。

五、抉择:commit还是kill

锁和undo都看清了,剩下的就是动手。小刘能叫回来最好办:UPDATE早执行完,只差提交,让他补个COMMIT,锁立刻释放,订正也保住,几乎零成本。

联系不上人的时候才考虑kill,而且姿势要对。现在没有语句在跑,KILL QUERY 213没有任何东西可杀,必须KILL 213断掉整个会话,让InnoDB回滚整个事务。这一下是三百多万行的回滚,InnoDB要拿着undo把数据一条条翻回原样,期间IO和负载都不小。回滚进度可以用SHOW ENGINE INNODB STATUS盯着,看那个事务什么时候消失。

我的标准很粗暴:改动本来就做错了、要撤,直接KILL,别犹豫。SQL跑完了、只差提交,优先找人commit。为一次没必要的回滚把生产IO打满,不划算。

小刘最后两分钟补了COMMIT,阻塞解除,ALTER走完,查询恢复。业务侧攒了一堆超时告警,整张表堵了大概40分钟。

六、复盘:三个环节叠出来的事故

回头看,这不是一条SQL的锅,是三个环节叠在了一块:长事务没人看见,ALTER恰好撞上来,中间没人兜底。

最冤的是那条ALTER吗?我不这么看。它只是正常执行,结果成了压垮业务的最后一根稻草。真正的根子在没人管的长事务,还有“想加索引就加索引”的变更习惯。锁只是表象,根上是事务纪律。

七、治理:让长事务可见,让DDL别裸奔

治理也就照着这三个环节补。

先让长事务可见。MySQL没有内置的idle-in-transaction超时,只能自己盯。innodb_trx里trx_query为空、又跑得久的,就是重点怀疑对象。我们后来加了个定时任务:事务超过60秒就报群,超过300秒直接拉清单。

SELECT trx_mysql_thread_id, trx_started,
       trx_query IS NULL AS idle_in_trx,
       trx_rows_modified
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60;

再用pt-kill这类工具兜底,超过N秒的idle in transaction自动断掉,别等它酿成事故。

再立变更纪律。数据订正必须分批、走短事务:几条一批,一批一提交。应用层别把autocommit关了又忘开,事务里也别夹“人肉确认”这种等待。用Spring这类事务框架,就显式配超时,别让一个@Transactional把事务无限挂下去。

最后给DDL上保险。动ALTER前,先跑一遍sys.schema_table_lock_waits,看看有没有长事务蹲着。大表加索引、加字段,能上gh-ost就上gh-ost,在线变更不吃MDL排队这一套。实在要用原生ALTER,就把lock_wait_timeout从默认的一年调成能接受的值,比如60秒,让DDL等不起就自己放弃,而不是拖着全表陪葬。

SET SESSION lock_wait_timeout = 60;

避坑清单

长事务最坑的就是看不见。它不像死锁会主动报错,processlist里只是个Sleep连接,安安静静攥着锁,等DDL撞上来,整张表才一起炸。别等出事再查,把innodb_trx盯起来,60秒以上就告警,比什么都管用。

表被堵的时候,先分清是MDL还是行锁。State是Waiting for table metadata lock,就走metadata_locks查DDL排队;报Lock wait timeout exceeded,才去查行锁。方向错了,会在processlist里绕半天。MDL默认等一年,很多人不知道,它才是“一堵堵全表”的元凶。

kill之前一定先看trx_query和trx_rows_modified。SQL跑完、只差提交的,找人commit比kill划算太多。真到要kill那步,记住idle in transaction要断会话用KILL,不是KILL QUERY。真回滚三百多万行,够你喝一壶,别手滑。

写在最后

这次事故里,ALTER最冤,它只是按规矩办事。真正该收拾的是那条没人管的长事务,还有随手就加索引的习惯。锁的账算到最后,算的都是事务纪律。长事务盯住了,死锁、MDL阻塞、从库延迟会跟着少一大截。干DBA这些年我最大的体会:一半时间在救火,另一半得用来想怎么让火别烧起来。

你遇到过这种整表突然全堵的事吗?当时查了多久才定位到源头?评论区聊聊,我猜不少人的第一反应跟我一样,也是先去看慢查询。

我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋

相关文章
|
1天前
|
存储 缓存 运维
数据库慢了就堆硬件?三维选型框架+4条避坑告诉你高性价比数据库一体机怎么选
业务增长、数据库扛不住,传统“加硬件”方案为何屡屡失效?数据库一体机的“软硬协同”到底解决了什么问题?如何用一套方法论选出高性价比方案?本文从问题根源、技术原理、市场产品到选型框架,一次性把数据库一体机这件事讲透。
|
7天前
|
SQL 运维 关系型数据库
2026年分布式数据库有哪些主流选择?先评估这5个维度再决定
分布式数据库选型,99%的文章在列表格比参数——但真正的决策关键不在厂商PPT里,在上线后的真实运维里。本文从网络延迟容忍度、SQL兼容性验证、在线扩容能力、全局索引代价、运维工具链成熟度5个维度出发,给出可落地的评估方法和决策建议,帮助你在选型阶段避开那些“只有上线后才会发现”的坑。
|
7天前
|
SQL 关系型数据库 MySQL
死锁报错看了三遍没看懂?我拆给你看(附定位SQL)
从一次真实死锁现场切入,讲清行锁、间隙锁、插入意向锁的加锁机制与死锁形成原理,手把手教你怎么用show engine innodb status和information_schema定位死锁,并给出加锁顺序设计等避坑清单。
|
8天前
|
存储 人工智能 Java
1TB库克隆从小时级到秒级,开发环境不再靠手搓
从AI编程时代开发环境不够用的痛点出发,讲清数据库秒级克隆的底层原理(copy-on-write与写重定向两条路线、引用计数与垃圾回收的工程差异)、三种实现层次(逻辑复制/存储快照/数据库原生COW),结合Neon、TDSQL-C及金仓KES的布局,给出三种落地模式(按开发、按PR、给Agent)、配额回收权限三个管理要点,以及一次配额被打爆的真实复盘与避坑清单。
|
4天前
|
SQL 关系型数据库 MySQL
加索引写入TPS掉到1/3?订单表慢查询的五个反直觉真相
以五个真实排障案例拆解MySQL慢查询的反直觉真相:type=index不等于快、索引会拖垮写入、LIMIT救不了深分页、大JOIN未必优于小查询、慢SQL常常是受害者不是凶手。主张收到慢查询先证明瓶颈,动索引前先做减法。
|
5天前
|
SQL 人工智能 运维
3个月AI Agent运维实测:慢SQL它管,根因还得我上
以三个月实测的视角,划清AI Agent自治运维的真实能力边界:巡检、慢SQL发现等重复活已可替代,复杂根因、变更审批、数据兜底仍需人把关,探讨DBA角色从救火队员向定规则、把关人的转型。
|
6天前
|
存储 关系型数据库 MySQL
面试总问的B+树,我把磁盘IO到底怎么算的讲清楚了
从磁盘IO的底层约束讲起,逐层对比哈希、二叉、红黑树、B树与B+树,讲清MySQL为什么选B+树(矮胖树、顺序IO、范围查询、查询稳定),并用这套底层理解反过来指导覆盖索引、前缀索引、最左前缀等日常索引设计。
|
网络协议 Linux 数据库
|
22天前
|
安全 关系型数据库 MySQL
切换从32秒缩到10秒,MHA到InnoDB Cluster升级复盘
从MHA停维护近十年、份额跌至12%的现实切入,完整记录从MHA一主两从升级到InnoDB Cluster的路径,含MySQL Shell建集群、Router切换、数据迁移与验证下线
|
29天前
|
存储 搜索推荐 关系型数据库
纯向量库架构上线两周出事故,我帮他们重构后发现了3个选型误区
从一次生产事故出发,拆解向量数据库爆火的真实原因,深入底层索引机制和架构取舍,分析融合趋势。给从业者一个清醒的判断框架。