bigywater.tech
MySQL / InnoDB · 29

元数据锁:为什么一条 ALTER 会等住查询

用一个未提交的普通SELECT事务阻塞ALTER TABLE,再验证ALTER排队之后才发起的新SELECT查询是否会被一起卡住。

1.5099s
ALTER TABLE被未提交读事务阻塞的时长
1.0074s
排在ALTER之后的普通SELECT也被卡住的时长
FIFO队列
MDL按公平排队,不允许新读请求插队

做的事:一个未提交的普通SELECT,也占着元数据锁

哪怕只是读,一个未提交的事务也不会轻易放手。

代码A: START TRANSACTION A: SELECT * FROM t12 WHERE id=1 -- 只是普通读,不加FOR UPDATE (A不提交,事务保持开启)

验证:B的ALTER TABLE,被这次"只是读"的事务卡住了

A没有锁任何行(没用FOR UPDATE),但依然挡住了DDL。

结果B: ALTER TABLE t12 ADD COLUMN extra INT 1.5秒后Acommit,B才完成 B耗时: 1.5099 s

原因是:只要一个事务对某张表执行过任何语句(哪怕只是普通SELECT),这个事务就持有这张表的一个元数据锁(MDL),一直到事务提交或回滚才释放。ALTER TABLE 需要获取这张表的排他型MDL才能真正开始改结构,只要还有事务攥着共享型的读MDL不放,ALTER就只能排队等。

意外发现:ALTER排队之后,新来的普通SELECT也被一起卡住了

这是最容易被忽视、也最容易造成生产事故的一点。

时间线t=0s A开启事务,SELECT一行(占着共享MDL) t=0.5s B发起ALTER TABLE,开始排队等待 t=1.5s C才发起一条普通SELECT t=1.5s Acommit 结果: B(ALTER TABLE)耗时: 1.5099 s C(普通SELECT,在B排队之后才发起)耗时: 1.0074 s ← 也被卡住了!

C的这条SELECT是在B的ALTER已经开始排队等待之后才发起的,理论上C只是要读,跟B的ALTER互不冲突——但结果是C也被卡住了整整1.0074秒(从C发起到A commit的时间差),直到A提交释放锁,B和C才一起被放行。

这就是MDL队列的公平性设计:一旦有一个排他型的请求(比如ALTER TABLE)在排队等待,后面新来的、哪怕是兼容的共享型请求(普通SELECT),也不允许insert队、越过前面等待的排他请求先执行——这是为了防止"源源不断的新读请求"让一个合法的DDL永远排不上号(饥饿问题)。代价是:线上一旦有人对一张热点表发起了一次长时间等待的ALTER,哪怕这条ALTER本身还没真正执行,它也会像一颗定时炸弹一样,让后续所有对这张表的查询都跟着排队卡住,表现得像整个服务突然对这张表失去了响应。

分步演示:一条ALTER是怎么连累后续查询的

每一步都是本文上面用三个MySQL连接跑出来的原始结果。

ALTER一旦排队,后面的普通查询也要跟着排队
这一步新验证的连接

    本文用到的工具

    • 本机 MySQL 26.7.0(InnoDB)
    • pymysql + threading:构造未提交事务+ALTER排队+后续SELECT三方场景
    京ICP备2026067502号-1京公网安备11010502063487号