线上搞分区表DBA和运维最爱干的事之一就是按月无缝加分区ALTER TABLE orders ADD PARTITION ( PARTITION p202603 VALUES LESS THAN (20260401) );看官方文档描述让人非常安心RANGE/LIST的ADD PARTITION是Inplace操作几乎不搬数据业务还能继续写入。结果半夜脚本一跑监控突然冒出一堆Deadlock found when trying to get lock受害者偏偏是白天跑得好好的普通INSERT语句。很多人排查第一反应是“是不是新分区被锁了”别猜了并不是。MySQL根本没有“每个分区单独一把大锁”这种设计。ADD PARTITION和INSERT真正死磕的是整张表的元数据锁(MDL)。死锁往往就发生在这样一个微妙瞬间DDL收尾必须独占整张表但某个业务事务死活还没松手。下面用一张表、两个会话分析底层的逻辑。1. 一条常规的ALTER是怎么逼死正常INSERT的先建一张典型的按月分区订单表CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT, order_day INT NOT NULL, -- 例如20260315 amount DECIMAL(12,2), PRIMARY KEY (id, order_day) ) PARTITION BY RANGE (order_day) ( PARTITION p202601 VALUES LESS THAN (20260201), PARTITION p202602 VALUES LESS THAN (20260301) );会话A(运维DDL)ALTER TABLE orders ADD PARTITION ( PARTITION p202603 VALUES LESS THAN (20260401) );会话B(业务DML模拟包含耗时逻辑的长事务)BEGIN; INSERT INTO orders (order_day, amount) VALUES (20260215, 99.00); -- 故意不COMMIT模拟长事务或多语句事务中间态白天业务高峰期会话B这种“开了事务、写了几条、还在等下游RPC响应”的场景到处都是。 一旦会话A的ADD PARTITION撞上这种事务就会直接卡住甚至跟其他正在写的会话绕成死锁。2. 所谓的Online其实是“中间松两头紧”对于RANGE分区的ADD PARTITIONInnoDB大致分三步走准备阶段(Prepare)短暂要个独占(X锁)。干活阶段(Inplace)把锁降下来允许业务继续INSERT/UPDATE。(这就是所谓的Online阶段)。收尾阶段(Commit前)再次强制升成独占(X锁)改数据字典、换分区定义。坑就埋在第3步。中间放行是为了少挡业务但收尾必须独占。因为底层数据字典和分区边界变了总不能一边有人按旧地图写一边你在换新地图吧。源码里InnoDB对“要不要搬数据”分得很清(位于handler0alter.cc的alter_parts::need_copy)RANGE/LIST的ADD PARTITION一般不用拷数据。所以ha_innopart::check_if_supported_inplace_alter直接返回HA_ALTER_INPLACE_NO_LOCK_AFTER_PREPARE。说白了就是“准备阶段排他干活时放开写提交前再排他一次。” SQL层真正收口的地方在mysql_inplace_alter_table()(sql_table.cc)1. Prepare完成 → 锁降级到SU(允许并发写) 2. Inplace主阶段干活 3. Commit前 → 调wait_while_table_is_used() → 锁强制升级到MDL_EXCLUSIVE(X锁)wait_while_table_is_used()干的活极其霸道把当前表上的共享锁升成X锁并把别人手里打开的表实例全清掉。结论很清楚ADD PARTITION不是全程无锁是“中间松两头紧”。3. 别光盯行锁了MDL才是幕后黑手3.1业务INSERT在拿什么锁INSERT打开表时拿的是MDL_SHARED_WRITE(SW锁)。 潜台词很明确“我要改这张表的数据谁也别动表结构。”只要事务不提交这把SW锁就死死挂在当前会话上。行锁、自增锁那是InnoDB引擎层的事对ADD PARTITION来说真正挡提交的首要大爹就是这把表级的MDL锁。3.2ADD PARTITION在拿什么锁ALTER刚开始一般拿的是MDL_SHARED_UPGRADABLE(SU锁)共享但声明“我后面要升级”。查下mdl.cc里的兼容矩阵SU和SW完全兼容。所以主阶段业务能愉快地INSERT。X和任何锁互斥。提交前要升X锁就必须死等所有的SW锁走完。于是时间线就变成了这样到这步系统还只是阻塞(Lock Wait)。要弄成死锁还得再加一把火。4. 未提交的长事务是怎么把等待链绕成死锁的最容易在生产环境复现的死锁压根不是“两个INSERT抢同一行”而是DDL在等DML释放MDL锁而DML(或另一条语句)又在等DDL已经占住的资源。来个最贴近线上的版本(跨表长事务)光盯着orders这一张表查现场往往是这样的会话A(ADD PARTITION)手里捏着SU眼巴巴等X锁(被B的SW锁挡着)。会话B(业务事务)手里攥着SW锁几把行锁同时又在等某个被A间接卡住的资源。结果环状等待死锁成型MySQL直接挑个软柿子回滚。很多人排查时看SHOW ENGINE INNODB STATUS只看到了行锁那一半。另一半其实藏在MDL里——去performance_schema.metadata_locks查一下就能对上谁死死捏着SHARED_WRITE谁又在苦等EXCLUSIVE。5. 4个锚点钉死InnoDB锁的降级与强制升级不用通读几万行的ALTER代码这四个锚点就够了①引擎层声明不搬数据→Prepare后可放行写入在ha_innopart::check_if_supported_inplace_alter()里因为alter_parts::need_copy()false直接抛出HA_ALTER_INPLACE_NO_LOCK_AFTER_PREPARE。②SQL层降锁Prepare用X锁随后降级SU在mysql_inplace_alter_table()里针对*_AFTER_PREPARE先upgrade_shared_lock(... MDL_EXCLUSIVE)准备成功后再downgrade_lock(MDL_SHARED_UPGRADABLE)。这一降就是业务端觉得DDL不阻塞的根本原因。③提交前升锁再次强制升X同一函数后半段直接调wait_while_table_is_used(thd, table, HA_EXTRA_PREPARE_FOR_RENAME)。这玩意底层还是upgrade_shared_lock(... MDL_EXCLUSIVE)。所有没提交的SW锁全成了拦路虎。④锁兼容矩阵X锁六亲不认mdl.cc写得明明白白想申请X锁必须等所有冲突锁释放——包括SW。所以线上现象极其统一DDL跑到99%时业务还在丝滑写入。最后1%收尾时像踩了急刹车INSERT瞬间堆积紧接着死锁报警满天飞。这不是分区算法有Bug而是一致性锁模型本来就是这么设计的。6. 同为分区变更为什么ADD PARTITION最容易踩雷都在InnoDB分区ALTER框架下不同操作痛感完全不同操作类型是否要拷数据主阶段锁倾向对业务写入的体感RANGE/LISTADD否几乎不挡写(SU)中间松收尾紧RANGE/LISTDROP否几乎不挡写(SU)同上REORGANIZE等常要主阶段就挡写(SNW)早期就冲突痛感反而直接DROP空分区和ADD新边界体感上都是“秒级元数据变更”所以最容易被人在业务高峰期随手敲进终端——也最容易在收尾升X锁时被长事务精准狙击。反倒REORGANIZE这种大动作主阶段就限制写入降低了最后关头诱发复杂死锁的概率。7. 告别DDL死锁的5条建议不想半夜被报警电话叫醒搞分区变更时死守这几条红线加分区放低峰期敲回车前先扫长事务动手前查下information_schema.innodb_trx和performance_schema.metadata_locks要是目标表上挂着好几分钟没提交的事务千万别头铁。业务侧管好事务边界“插一条、调个慢接口、再更新一条”这种长事务是DDL天敌。维护期内事务越短越好。别迷信Online DDL官方说的Online是指中间阶段。涉及数据字典的最终提交永远需要一个绝对干净的独占窗口。排查死锁别光盯行锁涉及DDL的死锁一定要看MDL监控抓出谁拿着SW锁谁在等X锁。能用脚本提前建就别临时搞最佳实践就是写个定时任务提前建好未来几个月的分区彻底告别高峰期临时救火式ALTER。以后碰到ADD PARTITION引发死锁别再怀疑是不是这命令锁表了。它的机制就是故意在中间放行结束前强行清场。当业务长事务死攥着SHARED_WRITE不放而DDL又非要EXCLUSIVE锁不可时稍微跟别的行锁缠一下死锁闭环就成了。不是分区表天生爱死锁而是长事务刚好卡在了“高并发写入”和“元数据一致性”交接的那道缝隙里。下次加分区前先问一句“这表上还有没提交的事务吗”大半的坑就避开了。
