MySQL元数据锁(Metadata Lock,简称MDL)是数据库server层提供的一种表级锁机制,其核心作用是保护表的元数据信息在并发访问时的一致性。这里的元数据包括表结构定义、列属性、索引信息等,并不涉及表里的具体数据行。只要会话对表发起访问,MySQL就会在后台默默加上对应的MDL,开发者通常感知不到它的存在,直到遇到结构变更被阻塞时才注意到。

从实现角度看,MDL并不是由存储引擎(如InnoDB)管理的,而是位于MySQL的server层。它随着SQL语句的开始而加锁,并一直持有到事务结束才释放。这一点和普通行锁不同,行锁可以在事务中随时加随时释放,而MDL只要在一个事务里开启了,就必须等事务提交或回滚后才能解开。也正因如此,一个长事务如果开了MDL读锁,就会挡住后续想要加MDL写锁的DDL语句。
MDL的锁类型与兼容性
MySQL中的MDL主要分为读锁和写锁两类。读锁(SHARED_READ、SHARED_WRITE等)在对表进行SELECT、INSERT、UPDATE、DELETE等普通数据操作时获取;写锁(EXCLUSIVE)则在执行ALTER TABLE、DROP TABLE、TRUNCATE等修改表结构的语句时获取。读锁之间互不冲突,多个会话可以同时持有,但读锁与写锁、写锁与写锁之间互斥。
这种互斥关系意味着,当有一个会话正在通过事务读取表数据而未提交时,另一个会话若想修改该表结构,就必须等待前面的MDL读锁释放。如果此时还有更多读事务进来,写锁申请会排在其后,形成锁等待队列。我们可以通过下表直观了解常见操作的MDL兼容性:
| 已有锁类型 | 请求SHARED_READ | 请求EXCLUSIVE |
|---|---|---|
| 无锁 | 允许 | 允许 |
| SHARED_READ | 允许 | 阻塞 |
| EXCLUSIVE | 阻塞 | 阻塞 |
需要特别注意的是,在MySQL 5.5之后引入MDL的初衷,就是解决早期版本中由于未锁住元数据,导致DDL和DML并发时可能出现表结构崩溃或查询结果错乱的问题。比如一个线程正在全表扫描,另一个线程删除了列,如果没有MDL保护,查询就可能读到不存在的字段而报错。
加锁与释放的实际表现
下面用一段简单的会话示例说明MDL的阻塞过程。假设我们在会话A中开启一个事务并查询表:
-- 会话A BEGIN; SELECT * FROM user LIMIT 1; -- 此时会话A持有user表的MDL读锁,事务未提交
接着在会话B中尝试修改表结构:
-- 会话B ALTER TABLE user ADD COLUMN age INT; -- 该语句会阻塞,因为需要获取user表的MDL写锁
这时如果会话C再发起普通查询,也会看似卡住。原因是MySQL的MDL锁队列中写锁优先于后续读锁,会话B的写锁在等待会话A,而会话C的新读锁又要排队在写锁之后,于是整体表现为业务查询全部停滞。只有会话A执行COMMIT或ROLLBACK,锁释放后,会话B和C才能继续。
从这段代码可以看出,MDL的释放严格绑定事务生命周期。很多线上故障都是因为某个程序漏掉了事务提交,或者使用了autocommit=0却长时间未处理,导致DDL无法推进。因此在做结构变更前,应当先检查是否有长事务,利用information_schema.innodb_trx结合performance_schema.metadata_locks视图定位阻塞源。
如何降低MDL带来的负面影响
面对MDL写锁阻塞的问题,最直接的办法是在低峰期执行DDL,并尽量缩短事务时间。对于必须使用的大表变更,可以借助支持在线DDL的工具,例如pt-online-schema-change,它通过新建影子表、触发器同步数据的方式来避免长期持写锁。不过即便使用这类工具,原表上的MDL读锁依然会被正常业务持有,只是写锁占用时间被极大压缩。
另外,从MySQL 5.7开始,metadata_locks表被纳入performance_schema,我们可以主动监控锁等待。例如执行如下语句找出被阻塞的MDL请求:
SELECT OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, PROCESSLIST_ID FROM performance_schema.metadata_locks WHERE LOCK_STATUS = 'PENDING';
这段查询会列出当前正在等待的MDL锁信息,结合PROCESSLIST_ID就能知道是哪个连接的事务没提交。养成变更前查锁、变更中监控的习惯,基本可以规避绝大多数元数据锁引发的线上事故。理解MDL概念不仅是面试考点,更是日常稳定运维MySQL的重要基础。