发布脚本执行到 ALTER TABLE orders ADD COLUMN note ... 就停住了。等一会儿,订单列表的普通 SELECT 也开始超时。碰到这两个现象一起出现,先看看 MySQL 进程列表里有没有 Waiting for table metadata lock。一条早就查完数据的连接,只要事务还开着,就可能挡住改表。
下面以 MySQL 8.0、8.4 的 InnoDB 表为例。查现场时用能读取 sys、performance_schema 和相关会话的诊断账号,在执行 DDL 的同一个 MySQL 实例上取数据。
先确定改表在等什么
从另一个连接执行这条命令。对照迁移日志里的时间和库表,记下 ALTER TABLE 的 Id、State、Time 和 Info。查询其他账号的会话需要相应权限。
SHOW FULL PROCESSLIST;
Waiting for table metadata lock 表示语句正在等表的元数据锁(MDL)。普通 SELECT 也会在访问表时取得这类锁。如果 SELECT 跑在显式事务里,结果返回后锁还会留到事务结束。在线 DDL 更新表定义时需要独占元数据锁,排队的独占请求还可能让后续访问这张表的语句一起等待。MySQL 元数据锁文档 · 在线 DDL 并发说明
只看到应用报超时的时候,先对照数据库的等待状态。查 InnoDB 行锁可以看 sys.innodb_lock_waits,查表的元数据锁可以看下文的视图。如果取样时 DDL 已经结束或超时,当前列表里的等待记录也可能已经消失。MySQL 查询突然变慢怎么查
顺着等待会话找到还持锁的连接
把库名、表名换成实际值,在 DDL 仍等待时查询。先找 waiting_pid 与改表会话 Id 一致的行,再看 blocking_pid 对应谁。
SELECT waiting_pid, waiting_query, waiting_query_secs, waiting_lock_type,
blocking_pid, blocking_account, blocking_lock_type
FROM sys.schema_table_lock_waits
WHERE object_schema = 'app_db' AND object_name = 'orders'
ORDER BY waiting_pid, blocking_pid;
同一个等待会话可能出现多行。这个视图把同一张表上已获准的锁和正在等待的锁配对,因此要结合锁类型和会话状态判断,尤其是 waiting_pid 与 blocking_pid 相同时。两个字段都是 MySQL 连接 ID,可以和进程列表对上。MySQL sys 视图文档 · 视图定义
想看这张表上有哪些锁,或者进程列表出现了 Sleep,可以把持锁线程关联到 performance_schema.threads。这里的 PROCESSLIST_ID 仍是连接 ID。
SELECT t.PROCESSLIST_ID AS connection_id,
t.PROCESSLIST_COMMAND AS command,
t.PROCESSLIST_STATE AS state,
l.LOCK_TYPE, l.LOCK_DURATION, l.LOCK_STATUS
FROM performance_schema.metadata_locks AS l
LEFT JOIN performance_schema.threads AS t
ON t.THREAD_ID = l.OWNER_THREAD_ID
WHERE l.OBJECT_TYPE = 'TABLE'
AND l.OBJECT_SCHEMA = 'app_db'
AND l.OBJECT_NAME = 'orders'
AND l.LOCK_STATUS IN ('GRANTED', 'PENDING')
ORDER BY l.LOCK_STATUS, connection_id;
例如,一条 Sleep 连接仍持有 GRANTED 的 SHARED_READ,而改表会话在等 PENDING 的 EXCLUSIVE。接下来找这条 Sleep 连接的归属,核对是不是脚本开启事务后忘了结束,或者 Java 方法在查完表后还在事务里等远程调用。视图为空时,确认实例、库表、采样时机、账号权限,以及 wait/lock/metadata/sql/mdl 的采集是否启用。MySQL metadata_locks 字段与采集说明
在空库里看一次排队过程
下面只在本地或隔离的测试实例上做。先建一个空库 mdl_demo,用三个独立的 MySQL 客户端连接,连接 A 应保持在同一个会话里,并确认客户端在 SELECT 后没有自动结束事务。
连接 A 建表、插一条数据,再开始事务读这张表。保留这个连接,让事务先开着。
CREATE TABLE mdl_demo.orders (id BIGINT PRIMARY KEY) ENGINE=InnoDB;
INSERT INTO mdl_demo.orders (id) VALUES (1);
START TRANSACTION;
SELECT id FROM mdl_demo.orders WHERE id = 1;
连接 B 给等待设置一个上限,然后执行改表语句。示例表可以使用 INPLACE 和 LOCK=NONE,改表仍需要在更新定义时取得独占元数据锁。
SET SESSION lock_wait_timeout = 60;
ALTER TABLE mdl_demo.orders
ADD COLUMN note VARCHAR(40) NULL,
ALGORITHM=INPLACE, LOCK=NONE;
B 还在等待时,连接 C 先用前面的查询观察 mdl_demo.orders,再执行普通查询。SELECT 如果也等起来,可以回到 A 结束事务,观察 B 和 C 的结果。
SET SESSION lock_wait_timeout = 60;
SELECT id FROM mdl_demo.orders WHERE id = 1;
回到 A 执行 ROLLBACK,结束这个只读事务。若 B 还在等,等它返回后用 SHOW COLUMNS FROM mdl_demo.orders LIKE 'note' 核对列是否已加上。若 B 已经超时,先看原语句的结果和实际表结构。退出三个测试连接,清理这次专用的示例库。MySQL 8.0 在线 DDL 示例
恢复访问前,先决定这条 DDL 还要不要继续
高峰期发现订单查询已经排队,先和迁移执行方确认这次改表是否还能继续。若决定暂停,先由执行方撤销等待中的迁移并核对表结构和迁移记录,再处理持锁事务。持锁会话一结束,队列里的 DDL 就可能接着执行。需要结束业务事务时,由会话归属方确认应提交还是回滚。KILL QUERY 结束的是当前语句,睡眠会话中的事务仍需单独处理。MySQL KILL 语义
改表重试时,可以在专用迁移连接里设置较短的 SESSION lock_wait_timeout,具体值按变更窗口与回退方式定。这个值限制的是单次元数据锁获取,语句可能需要获取多把锁。innodb_lock_wait_timeout 管的是 InnoDB 行锁。Java 侧如果是事务范围拖得太长,先保住业务上的原子更新,再把可以移出的远程调用或文件处理放到事务外。MySQL lock_wait_timeout 文档 · MySQL innodb_lock_wait_timeout 文档
恢复后在同一个实例上核对三件事。订单表的元数据锁等待是否消失,新增列是否符合本次变更的决定,应用侧原来超时的读写请求是否恢复。三处结果对得上,再关闭这次排查。
MySQL 改表卡住了,怎么查元数据锁
https://java.li/archives/mysql-alter-table-metadata-lock
评论