原创

MySQL ALTER 等待元数据锁:三会话还原阻塞链

给小表加一列,语句一直停在 Waiting for table metadata lock,CPU 和磁盘却很平静。这时增加排序缓冲区没有意义,先找谁还持有这张表的元数据锁。

MySQL 8.4 的元数据锁保护表定义在使用期间保持一致。一个已经执行完 SELECT、但事务尚未结束的连接,也可能继续持锁。会话当前显示 Sleep,不代表它对数据库没有影响。

把阻塞链做出来

使用 MySQL 8.4、InnoDB,打开三个固定连接 A、B、C。初始化由任意一个连接完成:

CREATE DATABASE IF NOT EXISTS mdl_lab;
USE mdl_lab;
CREATE TABLE document (
    id BIGINT NOT NULL PRIMARY KEY,
    title VARCHAR(100) NOT NULL
) ENGINE=InnoDB;
INSERT INTO document VALUES (1, 'metadata-lock-demo');

会话 A 读取表,但保留事务:

USE mdl_lab;
SELECT CONNECTION_ID() AS connection_a;
START TRANSACTION;
SELECT * FROM document WHERE id = 1;
-- 此处先不要 COMMIT,也不要退出窗口。

会话 B 尝试改表。即使这次加列具备轻量算法,也需要经过相应元数据锁阶段:

USE mdl_lab;
SELECT CONNECTION_ID() AS connection_b;
SET SESSION lock_wait_timeout = 120;
ALTER TABLE document ADD COLUMN note VARCHAR(100) NULL;

B 应停在等待状态,A 却已经没有正在执行的查询。把排查范围限制为“当前正在跑的慢 SQL”,很容易漏掉 A。

在第三个连接还原持有者与等待者

会话 C 检查采集是否开启:

SELECT NAME, ENABLED, TIMED
FROM performance_schema.setup_instruments
WHERE NAME = 'wait/lock/metadata/sql/mdl';

需要足够的诊断查询权限。若实例关闭了相应采集,空结果不等于没有锁,应由实例维护者按采集策略启用,不能把空表当无锁证明。

接着把元数据锁归属到实际连接:

SELECT m.OBJECT_SCHEMA, m.OBJECT_NAME,
       m.LOCK_TYPE, m.LOCK_DURATION, m.LOCK_STATUS,
       t.PROCESSLIST_ID AS connection_id,
       t.PROCESSLIST_COMMAND AS command_name,
       t.PROCESSLIST_STATE AS process_state,
       t.PROCESSLIST_INFO AS current_sql
FROM performance_schema.metadata_locks AS m
LEFT JOIN performance_schema.threads AS t
       ON t.THREAD_ID = m.OWNER_THREAD_ID
WHERE m.OBJECT_TYPE = 'TABLE'
  AND m.OBJECT_SCHEMA = 'mdl_lab'
  AND m.OBJECT_NAME = 'document'
ORDER BY m.LOCK_STATUS, connection_id;

重点看 A 已获得的锁和 B 的待获取锁,而不是只按锁行数量判断严重程度。OWNER_THREAD_ID 是 Performance Schema 线程编号,不能直接当成连接编号执行管理操作;关联 threads 后的 PROCESSLIST_ID 才对应客户端连接。

如果实例带有 sys 库,可进一步查询已组织好的等待关系:

SELECT *
FROM sys.schema_table_lock_waits
WHERE object_schema = 'mdl_lab'
  AND object_name = 'document';

这个视图能节省手工拼接时间,但输出仍需结合事务和业务归属解释。原始锁表里有已授予锁,不代表每一条都和等待锁不兼容;随意把同一表上的连接两两连接,会制造不存在的阻塞关系。

等待中的 DDL 可能把后来请求也排住

B 仍在等待时,在 C 做一次有限等待的读取实验:

SET SESSION lock_wait_timeout = 2;
SELECT * FROM mdl_lab.document WHERE id = 1;

根据等待队列与锁兼容关系,后来的请求可能被排在待执行 DDL 后面。此时业务看起来像“加列拖慢了所有查询”,实际链路可能是旧事务先挡住 DDL,DDL 的等待再影响后续请求。只杀掉最慢的一个 SELECT,并不一定解除根因。

这个实验使用短的元数据等待预算,避免演示连接无限排队。它与 innodb_lock_wait_timeout 不是同一个参数:后者主要对应 InnoDB 行锁等待,不能拿来替代元数据锁等待限制。

回到 A:

ROLLBACK;

A 结束事务后,观察 B 是否继续完成,以及后续读取是否恢复。最后核验实际结构:

SHOW CREATE TABLE mdl_lab.document;

不要仅凭客户端连接断开、超时或取消按钮的返回,就认定 DDL 没有执行。发生结果不确定时,查结构、查执行状态,再决定下一步;盲目重试可能变成“列已存在”的新错误。

处理阻塞者前,先判断应该结束哪一段工作

最轻的恢复往往是让持锁业务正常提交或回滚。若决定取消等待中的 DDL,先核对连接编号和当前语句,防止拿着过时编号误操作另一个连接。取消 DDL 能缓解队列,但旧事务仍未解决,之后同类变更还会再次被挡住。

终止阻塞连接则可能触发业务回滚,并让客户端看到失败。需要评估事务已经修改多少数据、是否还有外部副作用,以及应用会不会自动重试。看到 Sleep 就直接断连接,只是把隐蔽问题换成业务异常。

预防上,发布前检查长事务和空闲事务,给 DDL 设置有限的等待时间,记录开始时间、目标对象和回退方式。连接池归还连接以前应确认事务已经结束;只把自动提交设回去、却不理解具体提交行为,也容易把问题藏在连接复用之后。

对于自动迁移工具,拿不到元数据锁就退出并报告,通常比持续占着等待队列更容易控制影响。这个策略应由维护窗口和服务容忍度决定,而不是把等待参数全局调到极大。

完成实验,确认三个窗口都没有遗留事务,再清理:

DROP DATABASE mdl_lab;
正文到此结束
评论插件初始化中...
Loading...
本文目录