查清持锁线程再杀:受害者是等待MDL的线程,真正该杀的是持有SHARED_READ/SHARED_WRITE锁且trx_state='RUNNING'的悬挂事务;加字段须显式指定ALGORITHM=INPLACE、LOCK=NONE及DEFAULT NULL;lock_wait_timeout建议设为30秒;8.0.12+的INSTANT仅支持末尾加列且不可回滚。

查清谁在持锁,别杀错线程
DDL 卡在 Waiting for table metadata lock 时,显示“等待中”的线程只是受害者。真正该杀的是持有 SHARED_READ 或 SHARED_WRITE 锁的连接——它往往是个 Command = 'Sleep'、Time 超几百秒却仍 trx_state = 'RUNNING' 的悬挂事务。
必须查 performance_schema.metadata_locks:
SELECT OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, PROCESSLIST_ID 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' AND LOCK_STATUS = 'GRANTED';
- 重点看
LOCK_DURATION = 'TRANSACTION'的行,这种锁随事务生命周期存在 - 拿到
PROCESSLIST_ID后,用SELECT * FROM information_schema.INNODB_TRX WHERE TRX_MYSQL_THREAD_ID = ?确认是否真在跑长事务 - 确认后执行
KILL <pid>,不是KILL QUERY——后者不释放 MDL
加字段前必须显式声明 ALGORITHM 和 LOCK
MySQL 不会因为你写了 ADD COLUMN 就自动走 Online DDL。默认行为受版本、列位置、类型、默认值影响极大,稍不注意就降级为 COPY 模式,全表重建+长时间锁表。
安全加字段(尤其 MySQL 5.6–5.7)要强制组合:
-
ALGORITHM=INPLACE:明确要求原地修改,避免隐式拷贝 -
LOCK=NONE:尽可能不阻塞 DML;但若字段不允许为 NULL 且没设默认值,会静默失败,此时改用LOCK=SHARED -
DEFAULT NULL:显式声明,防止全表回填触发锁升级
示例:ALTER TABLE orders ADD COLUMN remark VARCHAR(200) DEFAULT NULL, ALGORITHM=INPLACE, LOCK=NONE;
控制等待时间,让问题暴露得更快
lock_wait_timeout 不是万能解药,但它能防止一个卡住的 DDL 把整条链拖死。它的作用是:新申请锁的线程最多等多久,超时直接报错,而不是无限排队。
- 线上建议设为
30秒,而非默认的 31536000(一年) - 临时生效:
SET lock_wait_timeout = 30; - 全局生效:
SET GLOBAL lock_wait_timeout = 30;,并写入my.cnf永久化 - 注意:它只对“新等待者”生效,不影响已持锁的长事务——所以必须配合前面的排查一起用
8.0.12+ 的 INSTANT 加字段也有限制
MySQL 8.0.12 引入 INSTANT,理论上加末尾字段毫秒级完成,但实际极易踩坑:
- 只支持
AFTER last_column,加到中间或开头会直接退化为INPLACE甚至COPY - 不支持
TEXT/BLOB/JSON类型,也不支持NOT NULL无默认值 - 表之前若用过
COPY模式做 DDL,内部 flag 被置位,后续INSTANT失效 - 执行后务必验证:
SELECT ORDINAL_POSITION FROM information_schema.COLUMNS WHERE TABLE_NAME = 't' ORDER BY ORDINAL_POSITION DESC LIMIT 1;应等于原列数 + 1
INSTANT 不记录 undo log,一旦执行不可回滚——加字段前先备份元数据,比期待“秒回”更实在。


















