DDL卡了40分钟,真凶是一条没提交的SELECT

简介: ALTER TABLE 卡四十分钟,换算法重试都没用。拆清 MDL 的持有与等待队列,讲明未提交的 SELECT 怎么堵住整张表,给出三层排查思路,说明 KILL QUERY 为何放不掉 MDL。

大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。

有回发版加索引,一条 ALTER TABLE 卡了四十分钟。我先以为是表大、DDL 慢,换了算法,又重试了两次。结果不是慢,是根本拿不到锁。SHOW PROCESSLIST 里那一行写着 Waiting for table metadata lock,后面还排着一串查询。业务不是被 DDL 拖慢的,是被它堵住的。

一、卡住的不是 DDL,是 MDL

ALTER TABLE 执行前,要先拿到这张表的元数据锁,也就是 MDL。它保护的是表结构,不是行数据。DDL 要拿排他锁,普通查询拿共享锁。只要有人还持有共享锁,排他锁就得等。

MDL 不是只有一种。SELECT 拿 MDL_SHARED_READ,DML 拿 MDL_SHARED_WRITE。在线 DDL 还会先拿 MDL_SHARED_UPGRADABLE。真正改表定义时,要升级成 MDL_EXCLUSIVE。升级这一步,就得等前面所有共享锁退场。

等的不是几毫秒。MDL 跟着事务走,事务不提交,锁就不放。一条普通的 SELECT,只要在事务里,也会把 MDL 一直攥着。这就是 DDL 卡住的起点。

二、一条 SELECT 为什么能挡住 DDL

很多人以为只有写操作才持锁。其实读也会。事务里执行一条 SELECT,MySQL 会在表上挂一个 MDL_SHARED_READ。这条语句跑完了,锁也不会马上放,要等事务提交或回滚。

所以真正危险的不是长 SQL,是长事务。查询早就返回了,连接还在 Sleep,事务还开着。更常见的是连接池把连接还回去了,事务却没提交。应用以为这次请求结束了,MDL 还挂在那条会话上。DDL 只能在队列里等,等到 lock_wait_timeout 超时,默认是一年。

-- 会话 A:开事务,读一下,不提交
SET autocommit = 0;
SELECT COUNT(*) FROM t_order WHERE id = 1;
-- 不 commit,连接挂着

-- 会话 B:加索引,卡在等待 MDL
ALTER TABLE t_order ADD INDEX idx_user_id (user_id);
-- State: Waiting for table metadata lock

三、最怕的不是等,是后面跟着排队

MDL 有一套等待队列。一个排他锁在等,后面的共享锁请求也会往后排。这是为了避免 DDL 被源源不断的新查询饿死。代价是,一条卡住的 DDL,会把整张表变成单行道。

怎么确认是在排队?SHOW PROCESSLIST 里会同时出现两种状态。DDL 那一行是 Waiting for table metadata lock,新来的查询也可能变成同一句。看到两行以上,基本就能判断是 MDL 队列,不是单条 SQL 慢。

那天就是这样。DDL 卡住之后,新的查询开始变慢,接着超时,最后连接池也满了。我一开始还在查 SQL,其实 SQL 都没问题。问题在表级队列上,一条没提交的 SELECT 把整条路堵死了。

四、把等待链一层层剥开

先看谁在等。MDL 在 Server 层,不在 InnoDB 层,SHOW ENGINE INNODB STATUS 里看不到它。performance_schema.metadata_locks 会列出当前所有 MDL,已授予和等待中的都在里面。LOCK_STATUS 是 PENDING 的那一行,就是卡住的请求。OWNER_THREAD_ID 指向持锁或等锁的线程。再看谁堵了谁,sys.schema_table_lock_waits 把等待方和阻塞方拼在一起,还直接给出 kill 语句。阻塞方如果是空闲连接,blocking_query 可能是空的。这时去 information_schema.innodb_trx 看事务,trx_started 越早越可疑,trx_state 是 RUNNING、trx_query 是空,基本就是开了事务没提交。

还有个前提。metadata_locks 要有数据,得先开 wait/lock/metadata/sql/mdl 这个 instrument。8.0 默认是开的。表查出来是空的,先确认它的状态。查不到不等于没有锁,可能是没采集。

-- 0. 确认 MDL 采集已开启(8.0 默认 ON)
SELECT NAME, ENABLED, TIMED
FROM performance_schema.setup_instruments
WHERE NAME = 'wait/lock/metadata/sql/mdl';

-- 1. 看 MDL:谁在等,谁已拿到
SELECT OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE,
       LOCK_DURATION, LOCK_STATUS, OWNER_THREAD_ID
FROM performance_schema.metadata_locks
WHERE OBJECT_SCHEMA = 'shop'
ORDER BY OBJECT_NAME;

-- 2. 看等待链:谁堵了谁,附带 kill 语句
SELECT * FROM sys.schema_table_lock_waits\G

-- 3. 看事务:谁开得最久,谁一直没提交
SELECT trx_id, trx_state, trx_started,
       trx_mysql_thread_id, trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;

-- 4. 把 PFS 线程 ID 换成 processlist ID
SELECT THREAD_ID, PROCESSLIST_ID, PROCESSLIST_USER,
       PROCESSLIST_HOST, PROCESSLIST_STATE, PROCESSLIST_TIME
FROM performance_schema.threads
WHERE THREAD_ID = ?;

还有一个细节,OWNER_THREAD_ID 是 PFS 的线程 ID,不是 SHOW PROCESSLIST 里的 ID。不 join performance_schema.threads,你拿到的号可能对不上人。查等待链时,先把这两张表对上,再决定杀谁。这是很多人查 MDL 查一半就断线的原因。

还有一层更容易被忽略。阻塞方未必是 MDL 的持有者,它可能也在等行锁。一个事务先锁了一行,另一个事务在等这行。第三个事务的 DDL,又在等第二个事务的 MDL。三条链套在一起,只看 MDL 表会以为是第二个事务的锅。往下再查 sys.innodb_lock_waits,才能找到最底下那个真正不放锁的事务。

五、在线 DDL 也绕不过 MDL

很多人一听 DDL 卡住,就换 ALGORITHM=INPLACE, LOCK=NONE。这两个参数能避免拷贝整张表,也能减少数据锁。但它们管不了 MDL。DDL 开始时仍要拿一次排他 MDL,结束时还要再拿一次,用来切换表定义。

ALGORITHM=INSTANT 也一样。它只改元数据,看起来最快,照样要拿 MDL。还有些变更只能走 COPY,LOCK=NONE 根本用不上。先看操作类型,再谈算法,别把参数当护身符。

gh-ost 和 pt-osc 也一样。它们把拷贝放到影子表上,最后一步 cutover 仍要短暂拿住原表的 MDL。如果这时有一个长事务挂着,cutover 就会反复重试。工具不会失败,它会一直等。你以为它在跑,其实它卡在门口。

这套逻辑在金仓 KES 上也是通的。sys_locks 能看到锁的持有和等待关系。sys_stat_activity 能查到会话状态和阻塞链。排查思路和 MySQL 的 metadata_locks 加 schema_table_lock_waits 是一脉相承的。换库不换理。DDL 卡住,先找持锁的事务,不是换算法。

六、先止血,再改习惯

止血的第一步不是杀 DDL,是找根阻塞者。杀 DDL 只是把等待的请求撤掉。持锁的事务还在,下一次发版还会卡。找到那个事务之后,先确认它在干什么。再决定是让它提交、回滚,还是直接断连接。

这里有个容易踩的坑。KILL QUERY 只停当前语句,事务还在,MDL 也还在。要让 MDL 释放,通常得 KILL CONNECTION,或者让应用自己提交、回滚。杀之前看清楚线程 ID,别把业务连接误杀了。

wait_timeout 也会兜底。空闲连接超时后会被断开,事务跟着回滚,MDL 自然释放。但这是被动兜底,回滚的代价可能比卡住更大。别把它当常规手段。

lock_wait_timeout 也值得单独说。它管的是 MDL 等待,默认 31536000 秒,也就是一年。innodb_lock_wait_timeout 管的是行锁,默认 50 秒,两个不是一个东西。给 DDL 会话设一个短超时,比如 5 到 10 秒。卡住就快速失败,别让它把整张表拖进队列。

根治还是靠事务习惯。事务里只放必要的 SQL。别把 RPC 调用、文件上传、用户思考时间包进去。报表查询单独走从库或只读实例。给它设 max_execution_time,跑太久自己退出。发版前先查一遍长事务,超过 60 秒的先找业务确认,再动 DDL。这几步做完,MDL 卡住的概率会低很多。

发版前先查一遍长事务。下面这条 SQL 会按开始时间列出超过 60 秒的事务。看到它们,先找业务确认。处理完再动 DDL。

SELECT trx_id, trx_started, trx_mysql_thread_id,
       TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS seconds
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60
ORDER BY trx_started;

监控也要补上。innodb_trx 里超过 60 秒的事务,应该有一条告警。metadata_locks 里 PENDING 超过 5 秒,也应该有一条。别等业务超时了,才去看等待链。告警早五分钟,处置就从容很多。

七、避坑清单

DDL 卡住,先看 SHOW PROCESSLIST 里的 State。出现 Waiting for table metadata lock,就别再怀疑 DDL 算法了,去查持有 MDL 的事务。算法只决定怎么拷贝,决定不了能不能拿到锁。锁不在算法手里,在人手里。

lock_wait_timeout 和 innodb_lock_wait_timeout 别混。前者管 MDL,默认一年;后者管行锁,默认 50 秒。DDL 会话把前者调短,卡住就失败,别把整张表挂在队列里。超时不是故障,是保护。

最后一条是我自己踩的。有次 DDL 卡了四十分钟,我先 kill 了 DDL,又重试了两次。结果新查询继续排队,业务反而更慢。后来才知道,真正持锁的是一条没提交的 SELECT。现在发版前,我先跑一条长事务检查。超过 60 秒的先找业务确认,再动 DDL。这个顺序反了,越救越乱。

八、写在最后

MDL 最难的地方,是它不报错。DDL 只是在等,查询也只是变慢。没有异常栈,没有红色告警,只有一张表慢慢变成单行道。等你看到连接池满了,往往已经堵了一阵子。

DDL 的算法决定它怎么改表,MDL 决定它能不能开始。把这两件事分开看,排查就不会跑偏。下次再遇到 DDL 卡住,先别动参数。去看那条等待链,找到最底下那个没提交的事务。锁是它拿的,也得它放。

我现在把 MDL 当成发版前的一道检查。先看长事务,再看 DDL 的算法,最后才排窗口。顺序对了,DDL 只是一次变更。顺序反了,它就会变成一次事故。

你们发版做 DDL,会先查长事务吗?有没有被 MDL 卡过?当时是怎么找到阻塞会话的?评论区聊聊。

我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋

相关文章
|
4月前
|
存储 关系型数据库 MySQL
数据库备份恢复最佳实践:RTO评估、PITR时间点恢复与演练SOP
本文从真实故障案例出发,分析RTO评估方法、全量增量恢复策略、PITR时间点恢复实践,附备份恢复演练SOP。
|
2月前
|
存储 关系型数据库 MySQL
对账差了三毛钱,查完我把全部金额字段从DOUBLE改成了DECIMAL
一次财务对账差三毛钱的排查,牵出金额字段用浮点数的老坑。从IEEE 754为什么存不准0.1讲起,用同一批金额把FLOAT、DOUBLE、DECIMAL三种类型实测对比,再给出金额字段的选型、聚合与改表做法,附避坑清单。
|
2月前
|
SQL Java 数据库连接
1万行插入13秒到0.9秒:ORM批量插入只差一个参数
从一次列表接口慢的排障讲起,发现2000多条一模一样的N+1查询。文章拆开ORM生成慢SQL的三类典型病:N+1懒加载、逐条批量插入(只差一个rewriteBatchedStatements参数)、隐式转换让索引白建。给出JOIN/批量IN/@BatchSize的取舍、MyBatis与JPA各自的修法,以及用performance_schema按SQL指纹抓N+1、测试环境打印真实SQL的协作办法。
|
2月前
|
SQL 运维 算法
订单表上亿行,我按用户ID拆成128片之后怎么样了
从单表几千万行慢查询的痛点出发,讲清垂直拆分与水平拆分的区别、分片键怎么选、分片算法(hash取模/range/一致性hash)怎么权衡,以及分库分表带来的分布式ID、跨片查询、分布式事务等问题,给出避免过度拆分的避坑清单。
|
2月前
|
SQL 关系型数据库 MySQL
死锁报错看了三遍没看懂?我拆给你看(附定位SQL)
从一次真实死锁现场切入,讲清行锁、间隙锁、插入意向锁的加锁机制与死锁形成原理,手把手教你怎么用show engine innodb status和information_schema定位死锁,并给出加锁顺序设计等避坑清单。
|
2月前
|
SQL 人工智能 数据库
AI写的SQL语法对、性能炸?上线前五道关卡能救命
从AI生成SQL的三大翻车模式(字段幻觉、性能灾难、语义错误)出发,给出上线前五道审核关卡:结构预检、执行计划校验、高危操作拦截、灰度上线、审计追踪,附SQL示例与避坑清单。
|
2月前
|
安全 关系型数据库 MySQL
高并发下1档只慢一点?innodb_flush_log_at_trx_commit的0/1/2实测
生成图片:不要沿用上面的图片风格,重新生成 4 张文章封面图供我选择,16:9 主标题:MySQL持久性最佳实践 副标题:redo刷盘参数三档取舍与故障分析 文章概述:实测innodb_flush_log_at_trx_commit的0、1、2三档性能,讲清进程崩溃与断电下的丢数据边界,以及redo、doublewrite、双1的关系,给出选型建议。
|
2月前
|
SQL 人工智能 运维
3个月AI Agent运维实测:慢SQL它管,根因还得我上
以三个月实测的视角,划清AI Agent自治运维的真实能力边界:巡检、慢SQL发现等重复活已可替代,复杂根因、变更审批、数据兜底仍需人把关,探讨DBA角色从救火队员向定规则、把关人的转型。
|
2月前
|
存储 关系型数据库 MySQL
面试总问的B+树,我把磁盘IO到底怎么算的讲清楚了
从磁盘IO的底层约束讲起,逐层对比哈希、二叉、红黑树、B树与B+树,讲清MySQL为什么选B+树(矮胖树、顺序IO、范围查询、查询稳定),并用这套底层理解反过来指导覆盖索引、前缀索引、最左前缀等日常索引设计。
|
3月前
|
SQL 人工智能 自然语言处理
上线第一周就拦下1条危险SQL:Agent连库四道防线
从团队试点AI Agent做数据问答差点出事的真实经历出发,梳理Agent连数据库与传统用户连库的本质区别,拆解提示注入、误操作、查询风暴、敏感泄露四类风险,给出最小权限只读账号、高危SQL拦截、全链路审计、速率控制四道防线的实操方案与避坑清单。

热门文章

最新文章