真正要杀的不是显示“waiting for table metadata lock”的线程,而是持有lock_status='granted'且command='sleep'、time>300、info为空的悬挂事务,它长期占用mdl锁导致全站雪崩。

不是ALTER TABLE慢,是它被别的连接死死攥着MDL读锁拦住了。
为什么Waiting for table metadata lock会雪崩式堆积
这个状态本身不表示DDL卡住,而是它排队等锁失败的“结果”。真正的问题源头往往安静地躺在SHOW PROCESSLIST里:一个Command = 'Sleep'、Time > 300、Info = NULL的连接。它可能只是执行过SELECT * FROM t后忘了COMMIT或ROLLBACK,却一直持有MDL_SHARED_READ锁。只要它不释放,后续所有对这张表的SELECT、INSERT、ALTER TABLE全得排队等——形成雪崩。
- ORM框架(如Django、MyBatis)隐式开启事务后未关闭,常见于“读已提交”预热查询
- 监控脚本执行了没加
LIMIT的全表SELECT,然后挂起 - 连接池配置不当,应用取完连接没
close(),游标长期空闲但事务未结束 - 存储过程或事件调度器里有未提交语句,静默持锁
ALGORITHM=INPLACE和LOCK=NONE为什么还是阻塞
这两个参数只管“数据重排阶段”是否锁表,不管“元数据变更阶段”。ALTER TABLE开始前和提交时,仍需短暂获取MDL_EXCLUSIVE锁来更新数据字典、写binlog。哪怕只持续几毫秒,只要此时有未释放的MDL_SHARED_READ,它就得等。
-
LOCK=NONE ≠ 零等待:高并发下DML会抢MDL,DDL仍要排队 -
ALGORITHM=INPLACE ≠ 全程无锁:页分裂、索引重建等操作仍需短时行锁或表级锁 - 加
NOT NULL字段却不带DEFAULT,MySQL会直接降级为ALGORITHM=COPY,全程锁表 - MySQL 8.0.12+ 的
ALGORITHM=INSTANT仅支持末尾加列、无全文索引、无虚拟列、row_format = DYNAMIC等严苛条件
怎么快速定位谁在持锁不放
别看Waiting for table metadata lock那行——那是受害者。要查LOCK_STATUS = 'GRANTED'且LOCK_DURATION = 'TRANSACTION'的记录,这才是真正在持锁的线程。
- 先确认开关已开:
SELECT * FROM performance_schema.setup_instruments WHERE NAME = 'wait/lock/metadata/sql/mdl';,确保ENABLED和TIMED都是YES - 查持锁者:
SELECT OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, PROCESSLIST_ID FROM performance_schema.metadata_locks m JOIN performance_schema.threads t ON m.OWNER_THREAD_ID = t.THREAD_ID WHERE OBJECT_NAME = 'your_table' AND LOCK_STATUS = 'GRANTED'; - 验证事务状态:
SELECT * FROM information_schema.INNODB_TRX WHERE trx_mysql_thread_id = [id];,重点看trx_started时间是否远超业务预期 - KILL前先试
KILL QUERY [id];若无效,再用KILL [id]——只有完整KILL才能回滚事务、释放MDL
生产环境大表DDL该怎么做
对千万级以上表,别信ALGORITHM=INPLACE能扛住。真正的安全路径是绕开MDL锁本身。
- 优先用
pt-online-schema-change:它建影子表 + 触发器捕获变更 + 分块拷贝 + 原子切换,原表读写基本不受影响 - 要求原表必须有主键或唯一非空索引,否则无法做增量同步
- 已有触发器的表不能用,会冲突;执行前务必加
--dry-run --print核对生成SQL - 禁止在从库单独运行再切主从——
pt-osc不复制DDL,会导致主从结构不一致
最常被忽略的一点:MDL锁生命周期与事务绑定,而事务可能来自自动提交语句或空闲连接。光查INNODB_TRX远远不够,必须直击performance_schema.metadata_locks。











