ALTER TABLE 卡在 Waiting for table metadata lock:改表前的三处预查与执行参数
在线 DDL 最容易坑人的地方,是名字里有"在线"两个字。看到 ALGORITHM=INSTANT、LOCK=NONE,很容易以为生产表加字段可以随手敲。真到业务高峰,一个长事务挂着,ALTER TABLE 等 MDL,后面的查询跟着排队,才发现事故不是出在改表本身,而是出在改表前没做判断。
以下按线上变更的视角走一遍:一张订单大表加字段,怎么判断能不能走 INSTANT,怎么提前发现 MDL 风险,怎么执行,怎么观察复制延迟,最后怎么复查。适用范围以 MySQL 8.x / InnoDB 为主,具体操作仍要按线上小版本和表结构再确认。
MDL 等待与队列效应
很多事故不是 DDL 执行慢,而是 DDL 在等 metadata lock。MySQL 为了保护表定义一致性,DDL 在开始和提交表定义时需要元数据锁。在线 DDL 的排他锁时间通常很短,但如果前面有长事务一直占着表的元数据锁,这个"很短"就会变成"等不到"。
更麻烦的是,等待中的 DDL 往往会形成队列效应。比如一个报表连接开了事务读 orders 没提交,ALTER TABLE 开始等排他 MDL,后面新的订单查询又被这个 pending 的 DDL 卡住。线上表现可能是:连接数升高、SQL 变慢、接口超时,但慢查询里你只看到一堆普通 SELECT。
第一步:先看有没有长事务
尤其是报表、后台导出、手工查询、定时任务,它们经常是 MDL 事故的源头。
SELECT trx_id, trx_state, trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_sec,
trx_mysql_thread_id, trx_query, trx_tables_locked, trx_rows_locked
FROM information_schema.innodb_trx
ORDER BY trx_started ASC;
第二步:看 metadata lock
生产上在变更前就准备好这条 SQL,出现 pending 能第一时间定位是哪张表、哪个线程:
SELECT OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, PROCESSLIST_ID
FROM performance_schema.metadata_locks ml
JOIN performance_schema.threads t ON t.THREAD_ID = ml.OWNER_THREAD_ID
WHERE OBJECT_SCHEMA = 'appdb' AND OBJECT_NAME = 'orders';
第三步:看 instant row version
MySQL 8.4 里,INSTANT 加列/删列会产生 row version,INFORMATION_SCHEMA.INNODB_TABLES.TOTAL_ROW_VERSIONS 可以看到累计值;MySQL 8.4 的上限是 64。这个值太高时,继续 INSTANT 可能直接报错,别等发布窗口里才发现。
SELECT NAME, TOTAL_ROW_VERSIONS FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME = 'appdb/orders';
执行:让 DDL 快速失败而不是无限等锁
真正上线时,不要让 DDL 无限等锁。可以在执行会话里设置一个比较短的 lock_wait_timeout,拿不到 MDL 就快速失败,先处理阻塞源,而不是把业务流量拖进来一起等。
SET SESSION lock_wait_timeout = 60;
-- 显式声明算法与并发,避免线上悄悄走重路径
ALTER TABLE appdb.orders
ADD COLUMN remark VARCHAR(255) NOT NULL DEFAULT '',
ALGORITHM=INSTANT, LOCK=NONE;
MDL 超时和行锁超时不是一套参数
注意 MDL 的等待超时和行锁不是一套参数:
ALTER等 MDL 看lock_wait_timeout,默认 31536000 秒(一年);UPDATE/DELETE改同一行看innodb_lock_wait_timeout,默认 50 秒。
行锁等 50 秒会自己报错退出,MDL 默认却是一年,所以一条 ALTER 可能安安静静站队很久,既不超时也不报错。
从库也要盯
很多团队只盯主库执行成功,忽略从库。DDL 会写入 binlog 并在复制链路上执行,如果从库机器规格差、SQL 线程被别的任务拖住,读流量可能先在从库上慢下来。变更期间至少观察 Seconds_Behind_Source、复制错误、从库 CPU/I/O,以及业务读延迟。
变更前清单
- 确认 MySQL 版本、表引擎、行格式、字段位置和目标 DDL 是否支持预期算法。
- 确认
ALGORITHM、LOCK显式写在 SQL 里,避免线上悄悄走重路径。 - 检查长事务、metadata locks、业务定时任务和手工查询窗口。
- 确认备份可用,并准备回滚方案。新增字段通常让旧代码忽略即可,删字段则要更谨慎。
- 设置短
lock_wait_timeout,拿不到锁先失败,不把业务拖进等待队列。 - 执行期间观察连接数、QPS、错误率、主从延迟、慢查询和 MDL pending。
- 执行后验证表结构、row version、索引生效情况和关键接口链路。
一个典型事故
一个后台导出开事务读大表,没人注意;DBA 执行了一个看起来很轻的加字段;DDL 在等 MDL,后续业务 SELECT 又排在 DDL 后面。最后大家盯着接口超时找代码问题,其实根因是一条没提交的查询。
小结
DDL 不要只问"这个操作是不是在线的",要问"它会等谁"。MySQL 8.x 的在线 DDL 确实比早年好用很多,尤其是 INSTANT 让不少表结构变更变得非常轻。但轻不代表无风险,ALTER TABLE 仍然是一次生产发布。建议:小版本核实、算法显式、MDL 预查、短等待失败、复制观察、事后复查。