MDL锁由事务持有而非嵌套查询本身触发,长事务未提交会导致S-MDL长期占用,阻塞后续DDL及DML操作;需联动INNODB_TRX、metadata_locks和PROCESSLIST定位源头,并严格控制事务边界。

嵌套查询本身不直接触发元数据锁,但长事务会拖住MDL
元数据锁(MDL)是在语句执行时加上的,不是在解析嵌套结构时加的。真正让嵌套查询“看起来导致MDL阻塞”的,是它被包裹在一个未提交的长事务里。比如 BEGIN; SELECT * FROM t1 WHERE id IN (SELECT id FROM t2); —— 这条语句执行完后,事务没 COMMIT,MDL 就一直持有着,后续任何对 t1 或 t2 的 ALTER TABLE 都会被卡住。
常见错误现象:
-
SHOW PROCESSLIST中看到 DDL 语句状态为Waiting for table metadata lock - 业务表突然无法加字段、改类型,但
SELECT/INSERT还能跑 - 阻塞源头往往不是嵌套查询本身,而是它前面那条
UPDATE或后面那个忘了COMMIT的begin
如何快速定位哪个长事务在持有MDL
不能只查 INNODB_TRX,得联动 performance_schema.metadata_locks 和 information_schema.PROCESSLIST 才能闭环。
实操建议:
- 先查活跃长事务:
SELECT TRX_ID, TRX_STARTED, TRX_STATE, TRX_QUERY FROM information_schema.INNODB_TRX WHERE TIME_TO_SEC(TIMEDIFF(NOW(), TRX_STARTED)) > 60; - 再查谁在等MDL:
SELECT OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, OWNER_THREAD_ID FROM performance_schema.metadata_locks WHERE LOCK_STATUS = 'PENDING'; - 最后关联线程:
SELECT ID, USER, HOST, COMMAND, TIME, STATE, INFO FROM information_schema.PROCESSLIST WHERE ID = ?;(把上一步的OWNER_THREAD_ID填进去)
注意:performance_schema.metadata_locks 默认开启,但某些低版本 MySQL(如 5.6)需手动启用 setup_consumers 表。
为什么REPEATABLE READ隔离级别会让MDL问题更隐蔽
这不是隔离级别直接加MDL,而是它默认启用间隙锁 + 一致性快照,导致事务更容易“无意中变长”。
典型场景:
- 一个
SELECT ... FOR UPDATE在REPEATABLE READ下可能锁住整个索引范围,而应用层以为只锁了几行,迟迟不敢提交 - 嵌套子查询里用了
BETWEEN或>=,InnoDB 自动加 gap lock,事务持续时间越长,MDL 持有就越久 -
READ COMMITTED虽不加 gap lock,但若子查询被物化成临时表,仍可能延长事务生命周期——关键不在读,而在“没及时结束”
所以降级隔离级别只是辅助手段,治标不治本;核心还是控制事务边界。
嵌套查询+长事务组合下,DDL失败的真实链条
你以为是“ALTER TABLE 失败了”,其实背后是三段式阻塞:
- 会话A:开启事务 → 执行带子查询的
SELECT(哪怕只读)→ 卡住没提交 → 持有t1和t2的 S-MDL - 会话B:发起
ALTER TABLE t1 ADD COLUMN x INT→ 尝试获取 X-MDL → 等待会话A释放 S-MDL → 卡住 - 会话C:后续所有对
t1的 DML(哪怕只是INSERT)→ 因为 X-MDL 未拿到,DML 也排队等 B → 整个表“假死”
最容易被忽略的一点:即使嵌套查询是只读的,只要它在事务里且未提交,就足以阻塞 DDL。别迷信“我没改数据就没事”。










