普通select会阻塞ddl,因其隐式开启事务并持有mdl shared_read锁直至提交或断连;真正持锁者需通过performance_schema.metadata_locks定位,而非直接kill等待线程。

Waiting for table metadata lock 不是表被“锁住”了,而是你的 ALTER TABLE、DROP INDEX 或 TRUNCATE TABLE 正在等一个元数据锁(MDL)的释放时机——它卡在获取写锁(X)的路上,而读锁(S 或 SHARED_READ)还被别人攥着。
真正要查的,从来不是那个显示 Waiting for table metadata lock 的线程,而是谁在安静地持锁不放。
为什么普通 SELECT 也会阻塞 DDL?
MySQL 的 MDL 是 Server 层自动加的,跟存储引擎无关(哪怕表是MyISAM 也一样生效)。只要连接没设 autocommit=1,执行第一条 SELECT 就会隐式开启事务,并立即持有该表的 SHARED_READ 锁,直到:
- 显式执行 COMMIT 或 ROLLBACK
- 连接断开(比如脚本退出但没调 conn.close())
- 超时断连(依赖 wait_timeout,通常默认 28800 秒,远不够用)
常见错误现象包括:
- Python 脚本用
pymysql或aiomysql执行完SELECT * FROM users LIMIT 1就直接退出,没commit()也没close() - 应用连接池配置了
autocommit=False,但业务逻辑里漏了事务收尾 - 开发在 MySQL 客户端手动
BEGIN后忘了COMMIT,窗口关了但连接还在
如何准确定位持锁线程(MySQL 5.7+)?
SHOW PROCESSLIST 和 INNODB_TRX 都不可靠——前者只反映当前动作,后者只管 InnoDB 事务。MDL 是 Server 层锁,必须查 performance_schema.metadata_locks:
- 先确认开关已启用:
SELECT * FROM performance_schema.setup_instruments WHERE NAME = 'wait/lock/metadata/sql/mdl';,确保ENABLED和TIMED都是YES - 再查锁归属(把
your_db和your_table换成实际值):SELECT OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, PROCESSLIST_ID, PROCESSLIST_USER, PROCESSLIST_HOST FROM performance_schema.metadata_locks m JOIN performance_schema.threads t ON m.OWNER_THREAD_ID = t.THREAD_ID WHERE OBJECT_SCHEMA = 'your_db' AND OBJECT_NAME = 'your_table';
重点关注LOCK_STATUS = 'GRANTED'且LOCK_TYPE为SHARED_READ或SHARED_WRITE的行——这些PROCESSLIST_ID就是真正在 hold 锁的连接
为什么 KILL 错线程会让问题更糟?
- 别 KILL 那个状态是Waiting for table metadata lock 的线程:它只是受害者,重试后立刻又卡住
- 真正该 KILL 的,往往是 Command = 'Sleep'、Time > 300、INFO 为空、且在 metadata_locks 中显示 GRANTED 的线程
- 如果这个线程背后是个长事务,KILL 会导致回滚(尤其是大表 UPDATE),期间仍持续持有 MDL 锁,其他操作继续挂起
- 更稳妥的做法是先用 SELECT * FROM information_schema.PROCESSLIST WHERE ID = ? 确认它是否真无业务活动,再决定是 KILL 还是通知业务方主动 COMMIT最常被忽略的一点:MDL 锁等待没有超时参数可调,innodb_lock_wait_timeout 对它完全无效。它卡多久,只取决于持锁者何时放手——而这往往藏在某个没人关注的空闲连接或异常退出的脚本里。











