news 2026/10/19 14:28:54

MySQL元数据锁阻塞DDL?一次线上订单表加索引卡顿排查实录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL元数据锁阻塞DDL?一次线上订单表加索引卡顿排查实录

上周给线上订单表加索引,一条ALTER TABLE orders ADD INDEX idx_status_created(status, created_at);跑出去之后就一直卡着,SHOW PROCESSLIST里状态是Waiting for table metadata lock。我一开始觉得奇怪,这个表才几百万行,加个索引按说几十秒就该完成,怎么会卡到业务告警?最后排查了两个小时才明白:真正要处理的不是 DDL 本身,而是一笔来自应用连接池的“早就应该结束但没结束”的事务。

这类问题平时不容易出现,但一出现往往就是连接池被打满、接口超时一片告警。这篇文章把我这次亲测排查的过程和背后的原理一次讲清楚,也把能直接用的命令和方法整理出来。不管你是 DBA、后端开发还是刚接触 MySQL 的运维,照着这个思路走一遍,基本都能在十几分钟内定位到元凶。

1. 元数据锁到底锁的是什么:先搞清楚等待的本质

1.1 Metadata Lock 到底是什么

MySQL 在 5.5 版本引入了 Metadata Lock,简称 MDL,也就是元数据锁。它和 InnoDB 的行锁、表锁不太一样,InnoDB 锁管的是“表中具体的数据行”,而 MDL 管的是“表结构本身”。你可以把 MDL 理解成图书馆门口的图书登记员:任何人对一本书做操作,都得先跟登记员说一声,登记员记录完才能进去;有人要改书的分类编号时,其他人就必须等在门口,等当前正在看书的人全部出来。

具体来说,执行 SELECT、INSERT、UPDATE、DELETE 这些 DML 语句时,MySQL 会获取对应表的 MDL 共享锁(SHARED),多个会话可以同时持有多把共享锁,互不阻塞。而执行 ALTER TABLE、DROP TABLE、TRUNCATE TABLE 这类 DDL 语句时,需要获取 MDL 排他锁(EXCLUSIVE),排他锁和任何其他锁都不能共存。一旦排他锁申请不到,DDL 就会一直处于Waiting for table metadata lock。

这个设计的本意是防止 DML 在执行过程中表结构突然变了,导致记录格式错乱。但问题也出在这里:如果某个会话的一个事务始终不提交,即使它现在已经“不干活了”,它之前访问过的那张表的 MDL 共享锁也会一直攥在手里。这时候任何 DDL 想拿排他锁,都只能干等。

1.2 哪些场景最容易触发这个坑

根据我自己的经历和帮别人排查过的案例,下面几类情况占了绝大多数:

场景常见原因典型现象
应用代码开启事务后未提交autocommit=0 或者BEGIN之后忘记COMMIT连接状态是 Sleep,但innodb_trx里有旧事务
事务里执行了长查询或外部调用事务内发 HTTP 请求、等待第三方响应事务长时间不结束,锁一直持有
只读事务也开了事务@Transactional(readOnly=true)等框架行为一条 SELECT 也能持有 MDL 共享锁
DDL 排队后引发连锁反应DDL 获取排他锁排队,后续 DML 全被挡住Processlist 中多个连接同时显示 Waiting for table metadata lock

这里有个很容易忽略的点:SHOW PROCESSLIST里看到的 State 是Waiting for table metadata lock的会话,未必是“持锁的人”,很多时候它是“被锁连累的人”。真正持有锁的会话可能正老老实实显示成Sleep,甚至Command一栏是空的。所以排查一定不能只看第一眼,要顺着事务列表往下挖。

注意:MDL 的持有范围是整个事务,而不是单条语句。也就是说,哪怕某个事务里只有一条普通查询,只要这个事务还没提交,相关表的 MDL 共享锁就一直不会释放。这个特性是很多“查不出原因”的根子。

2. 亲测第一步:一套稳定的排查流程,快速找出是谁在占锁

我很反感遇到问题就到处乱杀连接,那样很容易把无辜请求也一起干掉。下面这套流程是我实际用了很多次的,每一步都有明确的检查目的。

2.1 先看 Processlist,区分“被锁的人”和“持锁的人”

第一件事就是执行:

SHOW FULL PROCESSLIST;

也可以直接从information_schema.processlist查:

SELECT id, user, host, db, command, time, state, info FROM information_schema.processlist ORDER BY time DESC;

重点看三样东西:State、Time、Info。

如果发现一个 ALTER 语句的 State 是Waiting for table metadata lock,而且已经跑了几十分钟,基本可以确定它是在等锁。同时你往往还会看到一批 SELECT 或 UPDATE 语句也挂着同样的 State,这通常是“排队效应”:排他锁请求在 MDL 队列里排在最前面,它后面的共享锁请求全部被堵住,结果就是一片假象。

这时候不要急着杀 ALTER。先把我下面第二步做完,搞清楚谁拿着锁再动手。

2.2 用 information_schema.innodb_trx 挖出隐藏事务

SHOW PROCESSLIST只能看到当前正在执行的语句,看不到那些事务还活着、但当前没有在执行语句的连接。这时候要用innodb_trx:

SELECT trx_id, trx_mysql_thread_id, trx_started, trx_state, trx_query, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_duration FROM information_schema.innodb_trx ORDER BY trx_started ASC;

trx_mysql_thread_id就是对应的 MySQL 连接 ID,用它可以和processlist.id关联上。trx_started是事务开始时间,trx_duration算出来就能快速知道哪些事务已经跑了很久。

我那次排查时,就是通过这个表发现某个连接 ID 的事务已经存在 2 小时 20 分,trx_query字段里写的是一个很久以前的 INSERT 语句,但当前processlist里这个连接的 Command 已经是 Sleep,状态看着很正常。如果不是专门查事务表,根本不会想到它才是占锁的元凶。

如果看到的都是短暂事务,还可以进一步加条件,只看超过某个时长的:

SELECT trx_mysql_thread_id, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_duration, trx_query FROM information_schema.innodb_trx WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60 ORDER BY trx_duration DESC;

2.3 用 performance_schema.metadata_locks 做精准定位

MySQL 5.7 开始,performance_schema提供了metadata_locks表,可以直接看到当前所有 MDL 锁的情况。这是最精确的定位方式,不需要靠猜。

先确认performance_schema开启:

SHOW VARIABLES LIKE 'performance_schema';

然后可以这样查目标表上的 MDL 锁:

SELECT mls.OBJECT_SCHEMA, mls.OBJECT_NAME, mls.LOCK_TYPE, mls.LOCK_STATUS, th.ID AS PROCESSLIST_ID, th.PROCESSLIST_COMMAND, th.PROCESSLIST_TIME, th.PROCESSLIST_INFO FROM performance_schema.metadata_locks mls JOIN performance_schema.threads th ON mls.OWNER_THREAD_ID = th.THREAD_ID WHERE mls.OBJECT_SCHEMA = 'your_db' AND mls.OBJECT_NAME = 'orders' ORDER BY th.PROCESSLIST_TIME DESC;

如果OWNER_THREAD_ID对应线程查不到,就需要再结合REQUESTING_THREAD_ID看等待锁的线程。实际操作中,我一般先看metadata_locks里OBJECT_NAME等于目标表的记录,再用threads表把THREAD_ID映射回PROCESSLIST_ID,因为processlist里直接展示的连接 ID 和线程内部 ID 不是同一个东西。

实操心得:如果你不想写这么长的 SQL,可以先跑SHOW PROCESSLIST加innodb_trx,九成问题已经能定位。只有遇到特别复杂的多连接互相等待场景,才需要把performance_schema.metadata_locks拿出来精确定位。我平时两种方式结合,速度快,也都验证过。

3. 亲测有效的解决方法和适用场景

3.1 等待并观察:什么情况下可以等

最简单粗暴的“解决方式”就是等。如果innodb_trx里查到持锁事务已经接近提交,或者应用那边明确知道某个批处理任务马上完成,等一两分钟也未尝不可。

但我个人不建议在业务高峰期抱着侥幸心理去等。MDL 一旦堆积,影响会像雪崩一样扩散:一个 DDL 挂起,后面所有访问这张表的请求全部排队,连接数急速上涨,最终连接池耗尽,整个应用不可用。我在一次模拟压测中见过,一个小表上误加索引,两个小时内几乎把整库的连接池拖垮。

如果要等,最好同时盯着两个指标:一是innodb_trx里事务是否消失,二是processlist里 Waiting 数量是否下降。超过 1 分钟仍然没有任何变化,就别等了。

3.2 精准 Kill 持锁会话:我最终用的方案

这是最直接有效的手段,也是我当时结束问题的方案。

定位到innodb_trx里最老的那个事务对应的trx_mysql_thread_id后,执行:

KILL <connection_id>;

比如查询结果显示最老事务的trx_mysql_thread_id是 123456,就执行:

KILL 123456;

KILL 掉持锁会话后,原来排队的 ALTER 会在很短时间内获取到 MDL 排他锁,继续执行。实际操作中,我 kill 完之后几乎立刻看到 DDL 的状态从Waiting for table metadata lock变成了altering table,再过了几十秒就完成了。

但这件事有两个重要前提:

  • 必须先确认这个会话对应的业务是否真的可以中断。KILL 会导致这个事务回滚,如果它正在写重要数据,可能带来部分数据不一致。不过 MDL 卡住的时候,这个事务往往已经是空闲状态很久了,回滚影响通常可控。
  • 不要一看到多个等待的 SELECT 就批量 KILL。这些只是排队者,杀掉它们并不能释放排他锁,反而会造成应用层一堆异常报错。真正的动作应该指向持锁者。

3.3 快速失败的应急参数

MySQL 里有一个参数叫lock_wait_timeout,它控制语句等待元数据锁的最长时间。默认值是 31536000 秒,也就是整整一年。虽然生产上不太可能一年都没人管,但一旦发生 MDL 堆积,这个默认值等于没有上限,所有请求都会傻等着。

你可以动态调整:

SET GLOBAL lock_wait_timeout = 5; SET SESSION lock_wait_timeout = 5;

这样超过 5 秒拿不到元数据锁的语句会直接报错:

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

注意:这个办法的本质是“快速失败”,不是“解决阻塞”。它可以让排队请求尽快失败,保护连接池和应用线程,但 DDL 本身也会失败,必须等持锁事务释放后再重试。我一般只把它当应急开关用,调完尽快恢复默认值,并且继续清理持锁事务。

MySQL 8.0 还支持在 DDL 语法上加WAIT n或NOWAIT:

ALTER TABLE orders ADD INDEX idx_status_created(status, created_at), WAIT 5;

如果 5 秒拿不到锁就立即失败。这个特性适合把“长期阻塞”变成“快速失败”,避免拖垮业务。

3.4 不要指望 Online DDL 能完全绕过元数据锁

MySQL 5.6 起支持 Online DDL,很多人以为有了它,ALTER TABLE 就不会堵业务了,这是误区。

Online DDL 确实能减少部分操作的锁持有时间,但它的执行过程分为准备、执行、提交几个阶段。准备和提交阶段依然需要获取 MDL 排他锁,哪怕只是一瞬间。如果有一个未提交事务一直攥着目标表的共享锁,DDL 连准备阶段都进不去,照样卡在Waiting for table metadata lock上。

也就是说,Online DDL 可以解决“执行过程中能不能继续读写”的问题,但解决不了“有没有长事务占着元数据锁”的问题。不管用ALGORITHM=INPLACE还是默认的 online 方式,只要持锁事务不结束,DDL 就永远别想开工。这也是为什么我在做任何 DDL 之前,都会先查一遍长事务。

4. 一次真实排查实录:从现象到恢复的完整记录

4.1 现象与第一反应

晚上 20 点左右,我接到告警通知:某订单库活跃连接数突然从 50 涨到 300,应用侧大量接口响应超时。登录数据库执行SHOW FULL PROCESSLIST,看到非常典型的一幕:

+--------+---------+-----------+-------+---------+------+--------------------------+-------------------------------------------+ | Id | User | db | State | Time | Info | Command | 备注 | +--------+---------+-----------+-------+---------+------+--------------------------+-------------------------------------------+ | 10001 | appuser | testdb | Query | 1234 | NULL | Waiting for table metadata lock | ALTER TABLE orders ADD INDEX ... | | 10002 | appuser | testdb | Query | 1234 | NULL | Waiting for table metadata lock | SELECT * FROM orders WHERE ... | | 10003 | appuser | testdb | Query | 1234 | NULL | Waiting for table metadata lock | UPDATE orders SET ... | | 10005 | appuser | testdb | Sleep | 8310 | NULL | NULL | 看不出异常,但嫌疑很大 | +--------+---------+-----------+-------+---------+------+--------------------------+-------------------------------------------+

大量语句都挂在orders表上,状态统一是Waiting for table metadata lock。第一反应自然认为是那条 ALTER 引起的,但我也知道不能只杀 ALTER,因为真正导致它拿不到锁的人还在暗中。

4.2 第一次失误:kill 错了对象

我当时第一轮操作其实犯了个典型错误。看到 ALTER TABLE 卡了这么久,直接执行了:

KILL 10001;

这个连接是 DDL 持有者吗?不是,它是“等待者”。杀完之后,连接池里的请求倒是短暂恢复了一部分,但新的问题是:那条 ALTER 再跑一次依然卡住,而且新的排队又出现了。

原因是持锁事务还活着,KILL 一个等锁的会话,既没有释放 MDL 共享锁,也没有解决排他锁的需求。后来我才把目光转向information_schema.innodb_trx,一下就看到了真相:

SELECT trx_id, trx_mysql_thread_id, trx_started, trx_state, trx_query FROM information_schema.innodb_trx ORDER BY trx_started ASC;

结果里第一条事务让我很意外:

trx_id: 1234567 trx_mysql_thread_id: 10005 trx_started: 2025-01-15 17:40:23 trx_state: RUNNING trx_query: NULL

这个 ID 10005 的会话,看SHOW PROCESSLIST只是 Sleep,怎么看都不像有事。但trx_started显示事务从 17:40 就开始了,到 20:03 已经持续了 2 小时 23 分钟。即便它现在没有执行任何语句,这个事务在开启后一定访问过orders表,所以一直握着orders表的 MDL 共享锁。

4.3 正确的处理步骤和恢复过程

确认就是这个连接后,我先和业务负责人确认了 ID 10005 对应的应用会话可以中断,随后执行:

KILL 10005;

执行完的瞬间,我再看SHOW PROCESSLIST,之前那批Waiting for table metadata lock的连接已经在陆续恢复执行,原本要加索引的 ALTER 也从等锁状态变成了真正的执行中。

这里有一个细节:MDL 等待队列是公平的,排在最前面的 ALTER 在持锁事务释放后,会优先拿到锁,并不需要把它 kill 掉再重新执行。我这次没有重新跑 ALTER,等了大概几十秒,观察它的状态变为altering table,然后正常完成。

亲测后的经验:遇到这种问题,正确的恢复顺序是先定位持锁者,再 KILL 持锁者,最后观察等锁队列自动消化。顺序反了,只会越搞越乱。

5. 事后复盘与预防清单:让这种问题不再重复

5.1 应用层的长事务是最大根源

这次持锁 2 小时的会话,追到应用代码后发现,是因为一个批处理任务里开启了事务,在事务里调用了外部接口,外部接口超时时间设得又很长,导致事务一直挂着。事务挂着不关,MySQL 层面的锁就不会释放。

类似的代码配置我也在很多项目里见过:

  • 使用@Transactional注解,但方法里存在远程调用、消息发送、文件上传;
  • 手动BEGIN之后忘记COMMIT;
  • 连接池的autocommit被设置成了false,且代码里没有每次请求都提交事务;
  • 监控脚本用某些客户端工具开会话时,默认开启了事务。

这些都是隐藏长事务的温床。实际推行落地的做法是:事务内不放任何非数据库操作;远程调用必须放在事务提交之后;连接池保持autocommit=true;框架层面尽量让只读查询不开启事务。

5.2 变更前的三查检查

现在我在执行任何涉及表结构的 DDL 之前,都会强制跑一遍三项检查:

-- 1. 查当前所有连接状态 SELECT id, user, host, db, command, time, state, info FROM information_schema.processlist WHERE time > 30; -- 2. 查超过 60 秒的未提交事务 SELECT trx_mysql_thread_id, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_duration, trx_query FROM information_schema.innodb_trx WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60; -- 3. 查目标表上的 MDL 锁情况 SELECT mls.OBJECT_NAME, mls.LOCK_TYPE, mls.LOCK_STATUS, th.ID AS PROCESSLIST_ID, th.PROCESSLIST_COMMAND FROM performance_schema.metadata_locks mls LEFT JOIN performance_schema.threads th ON mls.OWNER_THREAD_ID = th.THREAD_ID WHERE mls.OBJECT_SCHEMA = 'your_db' AND mls.OBJECT_NAME = 'orders';

如果有任何一条返回结果,先处理干净再动手改表。这套检查已经帮我避免了好几次线上变更事故,比事后补救省心得多。

5.3 变更工具的选择和我后续的运维策略

大表 DDL 在低峰期执行是底线,除此之外,我开始考虑引入pt-online-schema-change这类工具。它的思路是创建一个影子表,在影子表上完成耗时的结构变更,再通过触发器把增量数据同步过去,最后用一次短时间锁表完成切换。

很多人以为用了它可以彻底免掉 MDL 问题,其实不是。它在最后切换表名的RENAME TABLE阶段,还是要获取 MDL 排他锁。如果这时候正好有一个长事务还握着旧表的共享锁,这个切换同样会卡住。只是它把最耗时的 ALTER 阶段从原表剥离了,最终持锁的窗口短到秒级,冲突概率大幅降低。

所以就算用 pt-osc,我仍然会先检查长事务。可以这样理解:任何 DDL 最终都要过“读锁释放”这一关,长事务不结束,神仙工具也得排队。

除了按需引入工具,我还把长事务监控加到了数据库告警里,对运行时间超过 5 分钟的事务实时告警,对超过 30 分钟的事务直接通知到 DBA 群。线上运行的连接池也会配合设置合理的事务超时,降低单会话无限挂住的概率。

最后再分享一个小习惯:我现在查看类似 MySQL 锁问题,不会只看一条命令就下结论,而是把processlist、innodb_trx、metadata_locks三个信息一起看。少了任何一个,都可能被假象带偏。这次亲测的教训,说到底就是一句话:等锁的人往往只是表象,持锁的人才是一切的根源。先把源头找出来再动手,就不会白忙。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 14:28:34

Cline:面向人类认知的AI原生协作系统

1. 项目概述&#xff1a;这不是又一个Git克隆体&#xff0c;而是一次对协作本质的重新设计“7个人、5个月&#xff0c;Anthropic造出了Git替代品”——这个标题在技术圈刷屏时&#xff0c;我正带着某高校实验室的本科生做版本控制实训。学生刚用git commit -m "fix bug&qu…

作者头像 李华
网站建设 2026/10/19 14:28:04

文本转3D:从语义描述到可交互三维模型的技术解剖

1. 这不是“AI画图”&#xff0c;而是一次三维语义空间的具象化实验“Google 文本转 3D 生成蝴蝶博物馆”——这个标题刚出现时&#xff0c;我第一反应是点开看是不是又一个带滤镜的宣传动图。结果在某实验室的内部演示视频里&#xff0c;看到输入“iridescent blue morpho but…

作者头像 李华
网站建设 2026/10/19 14:26:42

轻量级嵌入式移动侦测核心:纯CPU三帧差分+自适应背景建模

简介&#xff1a;本资源是一个基于OpenCV的C移动侦测实现项目&#xff0c;面向计算机视觉初学者、安防系统开发人员及嵌入式图像处理学习者&#xff0c;解决视频流中运动目标实时检测的核心问题&#xff0c;适用于家庭监控、智能门铃、交通行为分析等典型场景。压缩包共16个文件…

作者头像 李华
网站建设 2026/10/19 14:26:02

基于YOLOv8的智能工厂危险区域电子围栏系统实战解析

简介&#xff1a;面向毕业设计或课程设计的智能工厂安全监控完整方案&#xff0c;基于YOLOv8目标检测算法构建危险区域电子围栏系统&#xff0c;可实时检测人员或物体是否闯入设定区域&#xff0c;并自动触发警报或联动安全设备干预&#xff0c;提升工厂安全管理水平。压缩包共…

作者头像 李华
网站建设 2026/10/19 14:25:35

6机30节点系统机组组合建模实战:混合整数规划全解析

最近我把一个经典的6机30节点电力系统算例重新拿出来&#xff0c;用混合整数规划的思路完整跑了一遍机组组合问题。这个小算例看似简单&#xff0c;六台发电机、三十个节点&#xff0c;放在电力系统研究里属于“入门级”的存在&#xff0c;但真把启停状态、爬坡约束、备用容量和…

作者头像 李华
网站建设 2026/10/19 14:22:49

Zsh粘贴出现^[[200~乱码?一文搞懂bracketed paste协议与修复方法

我最近在 Ubuntu 上折腾终端环境时&#xff0c;也撞上过这个很“经典”的问题&#xff1a;从浏览器复制一段多行命令&#xff0c;切回 Zsh 准备粘贴&#xff0c;结果屏幕没有出现预期的命令&#xff0c;反而在最前面冒出一行 ^[[200~ &#xff0c;末尾还跟着一个孤零零的 ~ …

作者头像 李华