
我处理过好几起线上数据库假死最后排查定位到的原因都是这两个字在作怪metadata lock。如果你在MySQL里执行任何DDL加字段、加索引、改表结构时卡住顺手看一眼show processlist大概率会看到一堆会话停在Waiting for table metadata lock这个状态。这个状态很反直觉——表面上只是等一把元数据锁实际上背后往往是某个看起来人畜无害的睡着的事务在作祟。这篇文章我就来讲讲这个锁的成因、排查链路和解决方法都是我用实际场景一点点验证过的希望能帮你少走弯路。这个内容适合谁看日常维护MySQL、需要做表结构变更的DBA或后端开发特别是那些遇到过明明只是执行了一条ALTER TABLE结果整个表的读写全被堵死的同学。我先从机制讲起再还原一次典型的故障现场接着给出一套可以照着抄的排查和处置流程最后聊一聊如何从根上避免这类问题。1. 别再被名字骗了到底是哪把锁卡住了所有查询1.1 元数据锁不是表锁但比表锁更霸道很多人第一次见到Waiting for table metadata lock时会疑惑我没锁表啊为什么连SELECT都被堵住了这里必须先把概念掰开揉碎metadata lockMDL锁的是表的元数据也就是表结构本身而不是表里的数据行。MySQL在5.5版本引入了MDL机制本意是防止一种尴尬场景一个事务已经用旧的表结构执行了SQL另一个会话突然ALTER TABLE改了表结构导致前一个事务后续的SQL语义错乱。为了规避这种风险MySQL规定任何会话在执行SQL前都必须先获取对应表的MDL读锁任何DDL操作都必须获取MDL写锁。读锁和读锁之间是兼容的所以平时所有DML都能并发执行读锁和写锁之间互斥所以只要有事务还在使用这张表的结构DDL就进不去。关键点在于事务持有MDL读锁的时间不只是一条SQL的瞬间而是从第一条语句开始直到COMMIT或ROLLBACK为止。换句话说一个会话如果执行了INSERT然后一直不提交哪怕它当前处于Sleep状态它的MDL读锁也不会释放。这时候你执行ALTER TABLE去抢写锁只能干等。更麻烦的是DDL一旦排进等待队列后续所有需要对该表加MDL读锁的SELECT、INSERT、UPDATE也会被堵住因为它们得排在DDL后面。这就是为什么一条DDL能把整张表的读写全部拖死。1.2 一旦出现排队受害者的范围会滚雪球我见过很多次这样的现象最开始只是某个ALTER TABLE在等待过了几分钟show processlist里全是Waiting for table metadata lock包括最简单的SELECT 1。原因就出在MDL锁的排队机制上新会话的读锁请求不会绕过那个等待中的写锁请求而是老老实实排在它后面。打个比方一个房间里大家都拿着允许查阅资料的通行证读锁突然有个人要改墙上的设计图写锁门口管理员说你得等里面的人走光才能进。结果门外的其他人想进去查资料也被管理员拦下里面正等着改图呢你们现在不能进。于是门口越堵越多。所以判断这个问题的严重程度不是看有多少会话在等待而是先找到那个持有MDL读锁、一直不提交的事务。只要它还活着后面的DDL和DML就都得陪它耗着。很多人的第一反应是杀掉这个ALTER TABLE这完全是被表象误导了——杀等待者不解决任何问题后面还会继续排队。2. 我实际遇到的一次全员卡死事件复盘2.1 现象一条加索引的SQL把读写全堵了有一次处理线上故障报障说一个核心表的读写延迟飙到几十秒部分接口超时。我看了一眼show processlist大概长这样IdUserHostdbCommandTimeStateInfo100app10.x.x.xmallQuery120Waiting for table metadata lockALTER TABLE orders ADD INDEX idx_user_id...101app10.x.x.xmallQuery118Waiting for table metadata lockSELECT * FROM orders WHERE user_id...102app10.x.x.xmallQuery118Waiting for table metadata lockUPDATE orders SET status... WHERE id...90app10.x.x.xmallSleep3600NULL前三个会话都在等唯一可疑的是Id90它显示Sleep已经挂了3600秒。当时我的第一反应就是**要么这个Sleep会话藏着一个未提交的事务要么它只是连接池里长期空闲的连接根本没事务。**这两者性质完全不同前者是元凶后者是无辜的。判断方法很简单——查information_schema.innodb_trx。2.2 真凶一个睡了一小时的未提交事务我执行了下面这条SQLSELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx\G结果里果然有一条trx_id: 482731904 trx_state: RUNNING trx_started: 2025-01-16 10:23:11 trx_mysql_thread_id: 90 trx_query: NULL事务从10:23开始一直处于RUNNING状态但trx_query是NULL说明它当前没有正在执行的语句处于空闲状态。这时候已经很清楚了会话90在10:23执行过某条DML然后一直没提交虽然它看起来只是一个睡着的连接但手里紧紧攥着orders表的MDL读锁导致后面所有ALTER和DML全部卡死。这种场景在代码里特别常见比如业务代码里手动BEGIN后做了几次查询/更新逻辑提前return没执行COMMIT或ROLLBACKORM框架开启了事务但忘了提交连接被归还到连接池后长期空闲定时任务里某个步骤执行了SELECT ... FOR UPDATE后面抛异常但异常处理没有回滚事务。会话90大约是从某个异常分支里漏出来的事务一直挂到第二天才被发现。2.3 处置确认业务影响后精准kill确认会话90对应的事务没有任何正在执行的关键任务之后我直接执行了KILL 90;会话90被终止事务被回滚MDL读锁随之释放。紧接着ALTER TABLE马上开始执行后面的所有查询和更新也陆续恢复正常。整个过程从发现问题到解决不到两分钟但前提是找对了要KILL的对象。这也是我想重点强调的不要看到一堆Waiting for table metadata lock就随便找个会话kill更不要kill正在执行ALTER TABLE的那个会话那样对恢复毫无帮助。3. 从受害者顺藤摸瓜定位阻塞源的完整排查思路3.1 先把等待链完整拉出来如果你遇到类似问题不要急着动手。先按顺序执行以下几步把现场完整记录下来第一步查看所有卡住的会话SHOW FULL PROCESSLIST;重点看State列是否包含Waiting for table metadata lock以及Time列是否异常大。被阻塞的会话会集中出现这个状态。第二步查看所有运行中的事务找谁最老SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx ORDER BY trx_started ASC;注意如果阻塞会话只是持有了MDL读锁但并未真正操作数据行比如只执行了一条SELECT且走的是MVCC快照它在innodb_trx里也可能查不到。但绝大多数真实场景下持有MDL读锁空闲的事务都会在这个列表中出现。事务启动时间越早嫌疑越大。第三步如果你用的是MySQL 5.7及以上版本直接用sys库这把手术刀定位阻塞关系SELECT * FROM sys.schema_table_lock_waits\G它会明确告诉你哪张表、哪个会话在等待哪个会话持有锁连SQL语句都给你列出来非常直观。我实测下来这个视图在多数6分以上的故障里都能直接给出答案。3.2 进阶手段直接翻performance_schema的MDL表如果上面的步骤还不够清晰比如在MySQL 5.7及以上、且performance_schema已开启的情况下你可以直接查询MDL锁的内部表SELECT OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, SOURCE, OWNER_THREAD_ID, OWNER_EVENT_ID FROM performance_schema.metadata_locks WHERE OBJECT_SCHEMA 你的库名 AND OBJECT_NAME 你的表名;这张表会列出该表上所有MDL锁LOCK_STATUS是PENDING的就是在等待的GRANTED的就是已经持锁的。再结合performance_schema.threads表把OWNER_THREAD_ID关联到PROCESSLIST_ID就能找到对应的会话ID。我在处理一次多个会话互相等待、根因不明显的复杂故障时就是用这张表直接定位到三个会话中的某一个才是真正的锁持有者。对比一下三种排查手段的侧重点手段适用版本定位能力备注SHOW FULL PROCESSLIST所有版本看表象找等待者无法直接看出锁持有者information_schema.innodb_trx所有版本找长事务和会话ID空闲事务也可能遗漏sys.schema_table_lock_waits5.7.9直接给出等待方和持有方最推荐performance_schema.metadata_locks5.7锁级别精确视图适合复杂场景3.3 别忘了查业务上下文这个事务到底是谁发起的定位到具体会话ID之后还需要回答一个问题这个事务是谁的它打算干什么这两条SQL能帮你补全上下文SHOW PROCESSLIST里的Host列能定位到是哪台应用服务器information_schema.innodb_trx.trx_query如果是NULL那说明事务当前是空闲状态但你可以查看MySQL的binlog或慢日志找到该会话在那个时间点执行过的最后一条SQL确认它在做什么。有一次我定位到一个持锁事务但trx_query是NULL怎么都看不出来是哪段代码留下的。后来排查应用日志才发现是某段代码在调用远程接口之前开了事务远程接口超时导致逻辑中断事务一直挂到连接被回收。这个教训说明MDL锁故障很多时候不是数据库本身的问题而是应用层的事务边界没控制好。4. 先灭火再除根应急kill到长期止血的落地做法4.1 KILL的讲究该杀谁不该杀谁应急处置的核心原则是**优先处理持锁的旧事务而不是等待中的DDL。**但有个前提你必须确认这个事务后续不会产生更严重的业务影响。实操中我一般按这个顺序决策如果持锁事务的trx_query为NULL且对应会话长时间Sleep基本可以判定为漏提交事务业务影响面最小直接kill。如果持锁事务正在执行某个查询或更新trx_query非NULL先观察这条SQL执行了多久如果已经跑了很久且还没结束这往往意味着它本身也是慢SQLkill掉反而是止损。如果持锁事务来自核心业务链路暂时不能kill那就只能回退DDL选择业务低峰期再做。这里有一个很多人会犯的错误**kill掉正在等待MDL锁的那个ALTER TABLE会话。**这样做等于告诉后面的DML你们继续等吧因为持有锁的旧事务还在新的DDL依然进不来。正确做法是找到那个旧事务kill它。kill掉旧事务后MDL写锁被释放原本排队的DDL会立刻执行后面的所有DML也会自动恢复整个阻塞链路自然解开了。4.2 大表变更别硬扛换一个思路做DDL如果是几百G的大表即便MDL锁风波解决了直接用ALTER TABLE做结构变更也可能因为重建表、拷贝数据导致长时间锁表和大量IO消耗。对于这类变更我的做法是优先选择基于触发器的在线表结构变更方案它可以在业务运行期间逐步完成数据回填只在最后切换表名时短暂获取MDL写锁把锁等待时间压缩到秒级其次如果MySQL版本支持执行DDL时显式指定ALGORITHMINPLACE, LOCKNONE但要注意翻看执行计划和warning确认它确实走了在线路径而不是由于表引擎、索引类型等原因悄悄退化成COPY模式无论用哪种方式都必须在变更前检查是否有长事务并保证变更期间没有新的大事务启动。这里要破一个常见误解**Online DDL不等于不拿MDL写锁。**哪怕是ALGORITHMINPLACEDDL开始和结束时依然需要拿MDL写锁只是持锁窗口很短。一旦碰上未提交的长事务在线DDL同样会卡在Waiting for table metadata lock。4.3 从根上止血应用层与连接层的整改解决一次故障只是开始。如果代码里事务边界混乱这个问题下次还会换个表再来一次。事后必须推动几件事第一代码review重点看事务边界。凡是手动开启了事务的逻辑必须保证在try-finally或等效结构中执行COMMIT或ROLLBACK。严禁把远程HTTP调用、外部接口请求、耗时的批量操作放在事务中间。第二连接池配置要合理。很多连接池默认会保留空闲连接不回收这会让漏提交的事务长期存活。建议设置合理的idleTimeout和maxLifetime让长期空闲的连接及时销毁重建。至少保证一个漏掉的事务不会一直活到天荒地老。第三关注只读查询是否开启事务。某些框架或代码习惯用Transactional(readOnly true)包一层只读操作它同样会开启事务并持有MDL读锁。如果这个只读操作时间很长比如做了跨服务调用同样会把DDL堵死。能不用事务的只读查询就不要开事务。5. 防患于未然让MDL锁从事故变成例行检查项5.1 主动告警不等业务告警先让数据库自己喊出来这类问题最大的特点是爆发前毫无征兆因为持锁事务可能已经闲挂了几小时甚至几天。我的建议是配置两类数据库监控长事务监控定时扫描information_schema.innodb_trx只要存在trx_started超过设定时间比如60秒的事务立即报警。这能第一时间提醒你有事务可能hold住了MDL锁。MDL等待监控定时扫描SHOW PROCESSLIST一旦出现Waiting for table metadata lock状态视为P0级告警立即触发人工介入。在监控脚本里用下面这条SQL就能把长事务轻松拉出来SELECT trx_mysql_thread_id, trx_started, NOW() - trx_started AS trx_age_seconds, trx_query FROM information_schema.innodb_trx WHERE trx_started NOW() - INTERVAL 60 SECOND ORDER BY trx_started ASC;配合告警系统的webhook或短信通知基本能做到故障发生后一分钟内知晓。5.2 变更前检查清单每次DDL前过一遍这几项我在自己的例行流程里养成了一个习惯任何DDL执行前都过一遍下面的检查清单确认目标表上没有超过30秒的缓慢SQL排除大查询确认information_schema.innodb_trx中没有针对该表的长事务确认业务低峰期执行比如凌晨2点到6点大表变更一律走在线变更方案不使用原生ALTER硬扛如果半天内刚做过代码发版留意是否有新引入的事务边界问题。这一套组合下来我后来踩进MDL锁深坑的次数明显少了很多。其实很多时候问题不是MySQL不行而是我们给了MySQL一个无法避免锁等待的前提条件。5.3 一个实用的定期巡检思路除了变更前检查日常巡检也建议加入一条每周随机挑一两个业务高峰时段手动跑一下SHOW FULL PROCESSLIST看看有没有Time特别大的Sleep会话以及information_schema.innodb_trx里的老事务。如果有测试环境或边缘业务直接模拟一次长事务DDL的场景让团队成员都感受一下故障表象长什么样真的遇到时才不会慌。6. 一些容易忽略的次生坑和我的习惯动作6.1 杀会话之后不只是释放锁那么简单KILL一个事务会话数据库端会自动回滚它之前的改动。但应用端的连接池不会立刻感知到连接已经被断开某些旧连接可能还会被复用一下导致业务短暂报Connection is closed之类错误。所以在排障窗口内最好提前通知应用团队盯着错误日志避免把数据库故障变成应用恐慌。6.2 小心多个等待者互相接力的怪现象还有一种比较隐蔽的情况你杀掉了当前持有锁的事务DDL执行完成后锁正常释放。但过一会儿新的DDL又卡住了。这时候很可能是应用层又启动了新的长事务或者连接池里还有其他旧的漏提交连接被复用。建议在KILL之后观察5到10分钟确认innodb_trx里没有再次堆积新事务再宣布故障解决。6.3 我的个人习惯把排障SQL存成模板我自己维护了一套排障SQL集合里面包括查长事务、查MDL等待、查锁持有者、查慢SQL等场景每次遇到同类问题时直接一键执行不用现场敲。这个习惯帮我省了很多时间也减少了现场操作失误的概率。你可以根据自己的环境把上面的SQL存成.sql文件或运维脚本关键时候能避免手忙脚乱。最后分享一个踩过几次坑之后形成的习惯每次做线上表结构变更我都会先看一眼information_schema.innodb_trx有没有当前表相关的长事务养成这个条件反射之后我再也没因为一条ALTER TABLE把自己折腾到大半夜。表结构变更本身不复杂复杂的往往是那些看不见的、默默持有锁的闲坐事务。希望这篇亲测经验能让你在遇到类似情况时少一点慌乱多一点从容。