最近在维护一个线上MySQL数据库时,遇到了一个典型的"Waiting for table metadata lock"问题。当时需要紧急清理一张大表的数据,执行TRUNCATE操作却一直卡住,导致后续业务查询全部阻塞。通过这次实战排查,我系统梳理了MySQL元数据锁的产生机制和解决方案,分享给同样可能遇到这个问题的同行们。
元数据锁(Metadata Lock,简称MDL)是MySQL 5.5版本引入的重要机制,主要作用是保证表结构变更期间的数据一致性。当出现"Waiting for table metadata lock"时,意味着当前会话正在等待获取某个表的元数据锁,而该锁被其他会话持有且未释放。这种情况在数据库维护和开发过程中相当常见,特别是在执行DDL操作(如ALTER TABLE、TRUNCATE等)时。
MySQL的元数据锁系统采用层级结构,按照锁的粒度可以分为:
按照锁的类型又可分为:
重要提示:MDL锁的获取遵循"先到先得"原则,且存在锁升级机制。例如,一个持有SHARED_WRITE锁的会话可以升级为EXCLUSIVE锁,但必须等待其他共享锁释放。
当以下条件同时满足时,就会出现"Waiting for table metadata lock":
常见的锁兼容矩阵如下:
| 当前持有锁 \ 请求锁 | SHARED_READ | SHARED_WRITE | EXCLUSIVE |
|---|---|---|---|
| SHARED_READ | 兼容 | 兼容 | 不兼容 |
| SHARED_WRITE | 兼容 | 不兼容 | 不兼容 |
| EXCLUSIVE | 不兼容 | 不兼容 | 不兼容 |
现象描述:
执行ALTER TABLE或TRUNCATE等DDL操作时长时间等待,通过SHOW PROCESSLIST可以看到状态为"Waiting for table metadata lock",同时存在运行时间很长的查询事务。
复现步骤:
sql复制-- 会话1
BEGIN;
SELECT * FROM orders WHERE user_id = 100 FOR UPDATE;
-- 不提交事务
-- 会话2
TRUNCATE TABLE orders;
-- 此时会卡住
解决方案:
sql复制SELECT * FROM information_schema.innodb_trx
ORDER BY trx_started ASC LIMIT 10;
sql复制SELECT * FROM performance_schema.events_statements_history
WHERE thread_id IN (SELECT thread_id FROM performance_schema.threads
WHERE processlist_id = [阻塞事务的ID]);
实战技巧:
sql复制-- 在配置文件中添加
[mysqld]
long_query_time = 60
log_queries_not_using_indexes = 1
现象描述:
即使没有明显的长查询,DDL操作仍然被阻塞,information_schema.innodb_trx中存在未提交的事务。
复现步骤:
sql复制-- 会话1
BEGIN;
INSERT INTO orders VALUES(...);
-- 忘记提交或ROLLBACK
-- 会话2
ALTER TABLE orders ADD COLUMN remark VARCHAR(200);
-- 阻塞
解决方案:
sql复制SELECT * FROM information_schema.innodb_trx
WHERE trx_state = 'RUNNING';
sql复制SELECT p.ID, p.USER, p.HOST, p.DB, p.COMMAND,
p.TIME, p.STATE, p.INFO, t.trx_started
FROM information_schema.PROCESSLIST p
LEFT JOIN information_schema.innodb_trx t
ON p.ID = t.trx_mysql_thread_id
WHERE t.trx_id IS NOT NULL;
sql复制SHOW ENGINE INNODB STATUS;
-- 查看TRANSACTION部分
避坑指南:
sql复制SET GLOBAL interactive_timeout = 180;
SET GLOBAL wait_timeout = 180;
现象描述:
最隐蔽的一种情况,SHOW PROCESSLIST和innodb_trx中都看不到明显阻塞源,但DDL操作仍然被阻塞。
复现步骤:
sql复制-- 会话1
BEGIN;
SELECT non_existent_column FROM orders;
-- 报错但未结束事务
-- 会话2
DROP TABLE orders;
-- 阻塞
解决方案:
sql复制SELECT * FROM performance_schema.events_statements_current
WHERE SQL_TEXT LIKE '%orders%' AND MESSAGE_TEXT LIKE '%error%';
sql复制SELECT THREAD_ID, SQL_TEXT, MESSAGE_TEXT
FROM performance_schema.events_statements_history
WHERE MESSAGE_TEXT LIKE '%error%'
ORDER BY EVENT_ID DESC LIMIT 5;
sql复制-- 先获取连接ID
SELECT PROCESSLIST_ID
FROM performance_schema.threads
WHERE THREAD_ID = [问题THREAD_ID];
-- 然后终止
KILL [连接ID];
最佳实践:
sql复制SET GLOBAL innodb_rollback_on_timeout = ON;
MySQL 5.7及以上版本提供了更强大的监控能力:
sql复制UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME = 'wait/lock/metadata/sql/mdl';
sql复制SELECT * FROM performance_schema.metadata_locks
WHERE LOCK_STATUS = 'PENDING';
sql复制SELECT * FROM sys.schema_table_lock_waits;
sql复制SET SESSION lock_wait_timeout = 60; -- 单位秒
bash复制#!/bin/bash
while true; do
mysql -e "SELECT NOW(), p.ID, p.USER, p.HOST, p.COMMAND, p.TIME, p.STATE, p.INFO
FROM information_schema.PROCESSLIST p
WHERE p.COMMAND NOT IN ('Sleep','Binlog Dump')
AND p.TIME > 60"
sleep 5
done
在线DDL阻塞:
对于MySQL 8.0的原子DDL特性,仍然可能出现阻塞:
sql复制SELECT * FROM performance_schema.events_statements_current
WHERE SQL_TEXT LIKE 'ALTER%' OR SQL_TEXT LIKE 'CREATE%';
复制环境中的锁:
在主从复制环境中,从库应用binlog时也可能产生MDL锁:
sql复制SHOW SLAVE STATUS\G
最近处理的一个生产案例:某电商平台在促销活动前需要紧急增加一个用户表字段,但ALTER TABLE执行了2小时仍未完成。通过以下步骤解决:
这个案例给我的经验是:重要的DDL操作必须在低峰期执行,且需要有完善的监控机制。对于核心业务表,建议维护时采用以下流程:
MySQL的元数据锁机制虽然保证了数据一致性,但也带来了潜在的阻塞风险。经过多次实战,我总结出几个关键点:首先,任何DDL操作前必须检查当前活动事务;其次,建立完善的长事务监控体系;最后,重要的结构变更一定要有回滚预案。