适用场景
这篇文章适用于 MySQL 5.7、MySQL 8.0 或兼容 MySQL 协议的数据库中,执行 ALTER TABLE、CREATE INDEX、DROP INDEX、TRUNCATE TABLE 等 DDL 时长时间不返回,同时业务侧出现接口变慢、连接数升高、写入卡住等问题的场景。
元数据锁的英文是 Metadata Lock,通常简称 MDL。它不是表里的行锁,也不是 InnoDB 事务锁,而是 MySQL 为了保护表结构一致性引入的一类锁。只要 SQL 访问了某张表,MySQL 就会在语句或事务期间持有对应的元数据锁;DDL 要修改表结构时,必须等已有访问释放锁,自己拿到更高等级的元数据锁后才能继续执行。
线上最容易出问题的地方在于:一个看似普通的长事务或长查询,可能阻塞 DDL;而被阻塞的 DDL 又可能挡住后续新进入的查询和写入,最终把数据库连接池、应用线程池一起拖满。
现象描述
一次典型故障可能有这些表现:
- 运维或开发执行
ALTER TABLE orders ADD INDEX idx_user_created(user_id, created_at);后命令行一直卡住。 - 应用接口开始间歇性超时,慢 SQL 数量上升。
- 数据库连接数快速升高,
Threads_running或Threads_connected明显超过平时。 SHOW PROCESSLIST里出现大量Waiting for table metadata lock。- 回滚发布或重启应用不一定立即恢复,因为阻塞源可能还在数据库内部。
需要注意,DDL 卡住并不代表 MySQL 正在慢慢建索引。很多时候它还没有真正进入拷贝数据、重建索引阶段,而是在等待拿表结构锁。
可能原因
常见原因可以按优先级这样排:
- 有未提交事务访问过目标表,例如手工开启事务后执行过
SELECT * FROM orders WHERE id = 1,但忘记COMMIT。 - 有长查询正在扫描目标表,DDL 等待它结束。
- 有备份、数据导出、报表任务长时间访问目标表。
- 应用连接池中存在异常连接,事务没有及时关闭。
- DDL 已经在等待 MDL,后续普通 SQL 又排在 DDL 后面,形成请求堆积。
- 在业务高峰直接执行大表 DDL,没有设置超时、没有灰度、没有预检查。
其中最容易误判的是第一种:事务里即使只是查询,只要事务没有结束,也可能继续持有目标表的元数据锁。开发环境不明显,线上连接池复用和事务传播一复杂,就会变成隐蔽故障。
快速判断是不是 MDL 问题
先看进程列表:
SHOW FULL PROCESSLIST;
重点看这些字段:
Id:连接线程 ID,后续KILL需要用它。User:连接用户,帮助判断来自应用、运维账号还是定时任务。Host:来源地址,帮助定位具体机器或服务。db:当前数据库。Command:常见有Query、Sleep、Binlog Dump等。Time:当前状态持续秒数。State:如果是Waiting for table metadata lock,基本可以确认有 MDL 等待。Info:正在执行的 SQL。
示例:
Id User Host db Command Time State Info
12031 app 10.0.1.21:53012 shop Sleep 1840 NULL NULL
12088 dba 10.0.2.10:42110 shop Query 620 Waiting for table metadata lock ALTER TABLE orders ADD INDEX idx_user_created(user_id, created_at)
12102 app 10.0.1.22:53144 shop Query 580 Waiting for table metadata lock SELECT * FROM orders WHERE id = 10001
12119 app 10.0.1.23:53208 shop Query 570 Waiting for table metadata lock UPDATE orders SET status = 2 WHERE id = 10002
这个例子里,真正要找的不是等待的 ALTER TABLE,而是更早访问过 orders 且没有释放锁的连接。很多时候它可能显示为 Sleep,因为 SQL 已经执行完了,但事务没有提交。
使用 performance_schema 定位阻塞源
如果启用了 performance_schema,优先从 metadata_locks 查。MySQL 8.0 通常更好用,MySQL 5.7 需要确认相关 instrument 是否开启。
查看目标表上的元数据锁:
SELECT
ml.OBJECT_SCHEMA,
ml.OBJECT_NAME,
ml.LOCK_TYPE,
ml.LOCK_DURATION,
ml.LOCK_STATUS,
ml.OWNER_THREAD_ID,
t.PROCESSLIST_ID,
t.PROCESSLIST_USER,
t.PROCESSLIST_HOST,
t.PROCESSLIST_DB,
t.PROCESSLIST_COMMAND,
t.PROCESSLIST_TIME,
t.PROCESSLIST_STATE,
t.PROCESSLIST_INFO
FROM performance_schema.metadata_locks ml
JOIN performance_schema.threads t
ON ml.OWNER_THREAD_ID = t.THREAD_ID
WHERE ml.OBJECT_SCHEMA = 'shop'
AND ml.OBJECT_NAME = 'orders'
ORDER BY
FIELD(ml.LOCK_STATUS, 'PENDING', 'GRANTED'),
t.PROCESSLIST_TIME DESC;
字段解释:
LOCK_STATUS='PENDING':正在等待锁,通常能看到 DDL。LOCK_STATUS='GRANTED':已经拿到锁,阻塞源大概率在这里面。LOCK_TYPE:锁类型,DDL 常见等待的是更强的排他类元数据锁。PROCESSLIST_ID:可以和SHOW PROCESSLIST里的Id对上。PROCESSLIST_TIME:状态持续时间,越大越值得关注。PROCESSLIST_INFO:当前 SQL,Sleep 连接可能为空,需要继续查事务。
如果发现 PENDING 的 DDL 和多个 GRANTED 的会话同时存在,就要进一步找哪个会话有未提交事务。
定位未提交事务
查看 InnoDB 事务:
SELECT
trx.trx_id,
trx.trx_mysql_thread_id,
trx.trx_started,
TIMESTAMPDIFF(SECOND, trx.trx_started, NOW()) AS trx_seconds,
trx.trx_state,
trx.trx_tables_locked,
trx.trx_rows_locked,
trx.trx_query
FROM information_schema.innodb_trx trx
ORDER BY trx.trx_started;
重点看:
trx_mysql_thread_id:对应SHOW PROCESSLIST的Id。trx_started:事务开始时间。trx_seconds:事务已持续秒数,可以自己计算,也可以在 SQL 里算。trx_query:事务当前正在执行的 SQL;如果为空,不代表没问题,可能是事务空闲未提交。
把事务和连接信息关联起来:
SELECT
trx.trx_mysql_thread_id AS process_id,
TIMESTAMPDIFF(SECOND, trx.trx_started, NOW()) AS trx_seconds,
trx.trx_started,
p.USER,
p.HOST,
p.DB,
p.COMMAND,
p.TIME,
p.STATE,
LEFT(p.INFO, 200) AS current_sql,
LEFT(trx.trx_query, 200) AS trx_sql
FROM information_schema.innodb_trx trx
JOIN information_schema.PROCESSLIST p
ON trx.trx_mysql_thread_id = p.ID
ORDER BY trx.trx_started;
如果看到某个应用账号连接已经 Sleep 很久,但事务持续时间也很久,它就是高优先级怀疑对象。
MySQL 5.7 开启 metadata_locks 观察能力
部分 MySQL 5.7 环境默认没有开启 metadata lock instrument,查询 performance_schema.metadata_locks 可能看不到关键数据。可以先检查:
SELECT *
FROM performance_schema.setup_instruments
WHERE NAME = 'wait/lock/metadata/sql/mdl';
如果 ENABLED 或 TIMED 不是 YES,可以临时开启:
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME = 'wait/lock/metadata/sql/mdl';
这类开关只影响观察能力,不会自动解决锁等待。生产环境长期策略要结合版本、性能基线和公司规范决定。
一个完整定位示例
假设发布时要给 shop.orders 加索引:
ALTER TABLE orders ADD INDEX idx_user_created(user_id, created_at);
执行 10 分钟后还没返回,应用开始告警。第一步看进程:
SHOW FULL PROCESSLIST;
发现:
22011 app 10.0.1.15:48122 shop Sleep 2260 NULL NULL
22105 dba 10.0.2.10:53920 shop Query 612 Waiting for table metadata lock ALTER TABLE orders ADD INDEX idx_user_created(user_id, created_at)
22130 app 10.0.1.16:49001 shop Query 590 Waiting for table metadata lock SELECT * FROM orders WHERE order_no = 'A20260711001'
第二步查事务:
SELECT
trx_mysql_thread_id,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_seconds,
trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;
发现:
trx_mysql_thread_id trx_started trx_seconds trx_query
22011 2026-07-11 09:10:12 2280 NULL
第三步联系业务日志和连接来源。22011 来自应用机器 10.0.1.15,事务空闲但未提交,说明可能是某段代码开启事务后异常分支没有提交或回滚。
第四步确认是否可以处理。先和业务确认该连接是否可以中断,必要时保留现场信息:
SHOW ENGINE INNODB STATUS\G
再解除阻塞:
KILL 22011;
如果 DDL 本身已经引发大量业务 SQL 排队,也要评估是否先杀 DDL:
KILL 22105;
这里没有固定答案。若 DDL 还没拿到锁,杀掉 DDL 通常能让后续业务 SQL 不再排在它后面;若业务允许短暂维护,并且已经确认阻塞源可以结束,也可以先释放阻塞源让 DDL 继续。线上优先级一般是先恢复业务,再安排变更。
修复方案
立即止血
按下面顺序处理比较稳妥:
- 确认等待 DDL 的表名和 SQL。
- 找到最早持有目标表 MDL 的会话或长事务。
- 判断该会话来自应用、脚本、报表还是人工操作。
- 如果业务影响已经扩大,优先停止继续排队的 DDL。
- 如果确认阻塞源可以中断,执行
KILL process_id。 - 观察
SHOW PROCESSLIST中Waiting for table metadata lock是否消失。 - 观察应用错误率、接口耗时和数据库连接数是否回落。
不要一上来批量 KILL 所有等待连接。等待连接多数是受害者,真正要处理的是阻塞源和排队在前面的 DDL。
应用侧修复
如果阻塞源来自应用,需要重点检查:
- 是否所有事务分支都有
commit或rollback。 - 异常捕获后是否吞掉异常但没有回滚。
- 是否在事务里调用外部 HTTP、RPC、消息队列或慢文件操作。
- 是否把只读查询包进了不必要的大事务。
- 连接池是否开启了连接归还前的事务状态清理。
以 Python 伪代码为例,危险写法是:
conn.begin()
order = query_order(conn, order_id)
call_remote_service(order)
update_order(conn, order_id)
conn.commit()
问题在于远程调用放在事务里,一旦远程服务慢,事务就会变长。更稳妥的方式是缩短事务边界:
order = query_order_without_transaction(order_id)
remote_result = call_remote_service(order)
with transaction() as conn:
update_order(conn, order_id, remote_result)
核心原则是:事务只包住必须保持一致性的数据库读写,不要把网络等待、人工确认、文件处理放进事务。
DDL 侧修复
执行 DDL 前建议先设置等待超时,避免无限卡住:
SET SESSION lock_wait_timeout = 10;
ALTER TABLE orders ADD INDEX idx_user_created(user_id, created_at);
说明:
lock_wait_timeout对 metadata lock 等待有效。- 设置为 10 秒不是通用标准,要按业务窗口调整。
- 超时失败比无限等待更容易被自动化发布系统感知,也更容易回滚流程。
对于大表 DDL,还要评估在线变更工具,例如 pt-online-schema-change 或 gh-ost。即使用在线工具,也不能完全忽略 MDL:它们在创建触发器、切换表名等阶段仍然需要短暂元数据锁。只是在设计上把长时间阻塞窗口压缩到更小。
预防措施
发布前检查长事务
DDL 前先查长事务:
SELECT
trx_mysql_thread_id,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_seconds,
trx_state,
LEFT(trx_query, 200) AS trx_query
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60
ORDER BY trx_started;
如果目标库已经有持续几分钟的事务,先不要执行 DDL。尤其是高峰期,有长事务时强行变更很容易触发连锁等待。
发布前检查目标表活跃 SQL
SELECT
ID,
USER,
HOST,
DB,
COMMAND,
TIME,
STATE,
LEFT(INFO, 200) AS SQL_TEXT
FROM information_schema.PROCESSLIST
WHERE DB = 'shop'
AND INFO LIKE '%orders%'
ORDER BY TIME DESC;
这个查询不一定完整,因为 SQL 文本可能不含库名或表名,也可能被截断,但它能快速发现显眼的报表、导出和慢查询。
给变更系统加保护
如果 DDL 由发布平台或自动化脚本执行,建议内置这些保护:
- 执行前检查目标库长事务。
- 会话级设置
lock_wait_timeout。 - DDL 超时失败后自动停止,不继续重试轰炸数据库。
- 失败后输出
SHOW PROCESSLIST和长事务摘要。 - 大表默认要求低峰窗口或在线变更工具。
监控告警
可以关注这些指标:
Threads_connected:连接数是否异常升高。Threads_running:正在运行线程是否异常升高。- 慢查询数量和 P95、P99 延迟。
information_schema.innodb_trx中长事务数量。performance_schema.metadata_locks中PENDING状态数量。- 应用连接池等待时间和获取连接超时次数。
如果监控系统支持 SQL exporter,可以采集长事务数量:
SELECT COUNT(*) AS long_trx_count
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 300;
也可以采集 MDL 等待数量:
SELECT COUNT(*) AS pending_mdl_count
FROM performance_schema.metadata_locks
WHERE LOCK_STATUS = 'PENDING';
这两个指标不能替代业务告警,但能在 DDL 风险扩大前提供早期信号。
常见误区
误区一:Sleep 连接一定无害
Sleep 只表示当前没有正在执行 SQL,不代表没有事务。一个空闲未提交事务可能比正在运行的查询更危险,因为它很容易被忽略。
误区二:只有写操作才会阻塞 DDL
普通查询也会持有元数据锁。事务未结束时,查询访问过的表可能继续影响 DDL。
误区三:Online DDL 就不会阻塞
MySQL 的 online DDL 降低了对读写的影响,但不等于完全无锁。开始和结束阶段仍可能需要元数据锁,遇到长事务一样会等待。
误区四:杀掉所有等待 SQL 就能恢复
排队等待的 SQL 多数不是根因。根因通常是更早的长事务、长查询或排队在前面的 DDL。批量杀等待 SQL 可能短暂降低连接数,但阻塞链还会继续产生。
总结
MySQL 元数据锁问题的排查关键不是盯着卡住的 DDL 本身,而是找到谁让 DDL 拿不到锁,以及 DDL 是否又挡住了后续业务 SQL。
实战中可以按这个顺序处理:
- 用
SHOW FULL PROCESSLIST确认是否存在Waiting for table metadata lock。 - 用
performance_schema.metadata_locks查看目标表上的PENDING和GRANTED锁。 - 用
information_schema.innodb_trx找长事务和空闲未提交事务。 - 先恢复业务,再决定继续 DDL、取消 DDL 或改到低峰执行。
- 从应用事务边界、DDL 超时、发布前检查和监控告警四个方向做预防。
对线上数据库来说,DDL 不是简单的一条 SQL,而是一次变更动作。只要把长事务检查和锁等待超时纳入发布流程,大部分 MDL 引发的连锁故障都可以提前拦住。
Discussion
评论