一天下午,在给线上一个小表加个字段,发现老是加不上去,一直卡死。运维同学突然跑过来跟我说,线上数据库这半个小时一直在重启,问我是否有做什么操作。我当时虎躯一震,总共 100 多行的小表加个字段都加出问题了?我立马停止尝试加字段,果然数据库恢复正常了。后面查到原因,也顺利加上字段,现在来复盘总结一下。
先讲下原因,表数据量虽然小,却是一个热点表,访问频率特别高,而且该表的访问是在一个大事务中。加字段的时候一直在等待获取 MDL 写锁。这个等待也影响了后续表访问对 MDL 读锁的获取,导致后面的查询也都被堵塞了。更惨的是,客户端有重试机制,查询堵塞超过超时时间会再起一个 session 进行请求,导致数据库的线程池很快就爆满了,直接挂掉。
MDL 锁属于表级别的元数据锁。表级别锁分为数据锁和元数据锁,通常我们说的加锁一般指的是加的数据锁。跟数据锁一样,元数据锁也分读锁和读写锁。
MDL 不需要显示使用,在进行表操作时会自动加上。当对表进行增删改查时,会自动加上 MDL 读锁;当要对表进行加减字段的结构修改时,会自动加上 MDL 写锁。
MDL 锁的存在,其实是为了保证数据的一致性。想象一下,假如没有 MDL 锁,一个查询在遍历表数据的过程中,另外一个线程执行了 ALTER TABLE t DELETE COLUMN 'col_1' 把 col_1 这一列删掉了,那查询结果就乱了,结果中是否应该有这一列数据?
介绍完 MDL 锁,我们再来复现下事故。我们通过下面的操作序列来模拟线上情况。

时刻 1,事务 1 对表 t_mdl_test 进行查询,注意此时事务 1 并未提交,所以获取的 MDL 读锁也不会释放。时刻 2 另外一个线程想要添加字段 c, 由于事务 1 正持着 MDL 读锁,所以事务 2 会陷入阻塞,等待事务 1 释放读锁后获取 MDL 写锁。
申请 MDL 锁的操作会形成一个队列,队列中写锁获取优先级高于读锁。所以事务 2 不仅阻塞了加字段的操作,也会阻塞后续对该表的所有操作。比如后面的事务 3 和事务 4 查询由于获取不到 MDL 读锁都被阻塞了。
这时,如果客户端有重试机制,查询超时后会重新进行请求,容易把数据库的连接池给挤爆了。
表 t_mdl_test 建表:
CREATE TABLE `t_mdl_test` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增id',
`a` varchar(64) NOT NULL,
`b` varchar(64) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
了解了原因,事情就比较好办了,数据库奔溃原因是由于加字段等待时间太长导致影响后续请求,但 mysql 又无法在 alter table 语句里面设定等待时间.
所以当时做法是继续尝试加字段语句,语句卡住 30 秒就手动 cancel 掉。避免对后续请求的影响。重试了几次发现一直没能加上。。。,最后是通过查看接口调用监控,在请求频率较低的时间点给加上了。
在查阅资料的时候,发现另外一个情况。

这种情况事务 2 会阻塞吗?大家可以在自己的 huiwan_write_x 库中自行实验。