线上搞分区表,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 PARTITION,InnoDB大致分三步走:
准备阶段(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锁不可时,稍微跟别的行锁缠一下,死锁闭环就成了。
不是分区表天生爱死锁,而是长事务刚好卡在了“高并发写入”和“元数据一致性”交接的那道缝隙里。下次加分区前,先问一句:“这表上还有没提交的事务吗?”,大半的坑就避开了。