新闻详情

新闻详情

首页 / 资讯中心 / 详情

揭秘InnoDB 自增主键插入延迟与页分裂机制

发布时间:2026/9/27 21:44:25来源:尧图网络
揭秘InnoDB 自增主键插入延迟与页分裂机制
主键自增明明是顺序插入为什么还会偶尔“卡”一下高并发往一张 InnoDB 表里INSERT主键是自增的。监控上经常能看到这样一条曲线平时 QPS 挺稳但隔一段时间延迟就会猛地翘一下QPS 跟着掉一截过几秒后自动恢复再过一会儿又翘一次。曲线呈规律的锯齿状。很多人把这个现象粗略地归结为「页分裂Page Split」认为是 B 树把数据页从中间劈开一半记录被搬走从而导致了慢。但这只说对了一半。页分裂确实发生了但在自增主键的场景下InnoDB 根本不会把页「从中间劈开」。它会将新行直接放到右边新开的页上而左边的老页依然是满的空间并没有被对半撕裂。真正让所有INSERT瞬间卡住的元凶是它们都挤在了同一片最右边的叶子页上。当这页快满需要分裂时分配新页、改前后链表、改父节点这段时间里所有后来的插入操作全都在这页的门口排队死等。分裂只是导火索对最右页的Latch闩锁排队踩踏才是你在监控上看到的那个“尖刺”。MySQL 8.0的InnoDB就是这么干的。我们扒开源码从底层来看看这个过程。1. 先看一页里有什么InnoDB默认一页是16KBinnodb_page_size。你可以把它想象成一张固定大小的带网格的纸数据绝不是随便往上堆的。纸的两头有两个固定的「哨兵」infimum比页内所有真实记录都小和supremum比所有真实记录都大。用户记录从页头往页尾长而用于二分查找的页目录Page Directory则从页尾往页头长中间剩下一截空地。叶子页还有指向「上一页/下一页」的指针FIL_PAGE_PREV、FIL_PAGE_NEXT串成了一个双向链表。范围扫描顺着链表走就行不用每次都回根节点。当插入一行数据时InnoDB会先在页里找到合适的位置再看中间的空地够不够。如果页内因为之前的删除留下了空洞虽然总字节数够但连不成一整块InnoDB会先在页内做一次整理reorganize把记录挤紧凑了再插。如果整理完还是放不下才会触发分裂。这条「先试试不行再分裂」的代码入口叫btr_cur_optimistic_insert()乐观插入放得下只改这一页写点Redo Log结束。日常绝大多数插入走的都是这条路。悲观分裂放不下返回失败。上层接着进入btr_page_split_and_insert()。要申请新页、搬运数据、修改父节点甚至导致B树长高。你在监控里看到的尖刺对应的就是少数几次悲观分裂耗时叠加当时所有并发插入都在这一页排队等待的综合结果。2. 聚簇页vs二级索引页痛点截然不同两棵树的页结构长得一样但叶子里装的“货”不一样导致它们的并发痛点完全不同。聚簇索引主键叶子节点存的是完整的整行数据附带InnoDB隐藏列trx_id和roll_ptr。二级索引叶子节点只存索引列主键列。因为装的货不同分裂时的代价差异巨大如果聚簇索引的一行非常宽比如2KB去掉页头信息一页根本放不了几条数据。没插几下就要分裂且一次搬走的字节量极大。行越宽一页装得越少监控上的尖刺就越密集。二级索引的记录通常很窄一页能塞很多条分裂没那么频繁。但是主键是顺序自增的二级索引列比如user_id往往是无序散列的。主键的痛点所有写入都集中在最右页排队。二级索引的痛点插入点满树乱跳页经常被从中间切开索引更容易产生碎片变胖。延伸提醒主键如果发生修改整行要在聚簇索引里搬家所有二级索引里的主键副本也得跟着改。所以主键千万别用业务上会变的列。3. 自增插入真的不是对半切到底在哪切这取决于btr_page_split_and_insert()怎么选切点。InnoDB每个数据页的头部有个字段叫PAGE_LAST_INSERT专门记录上一次数据插在了哪儿。如果本次插入的位置正好接在上一次的后面InnoDB就会判定“当前是顺序向右插入”从而走btr_page_get_split_rec_to_right()逻辑当插到页的最右端时**新记录会自己去当右边新页的第一条左边的老页原封不动。**如果右边还剩几条旧记录InnoDB会把它们搬到新页并在当前页保留一条。源码注释解释留这一条是为了让后续的顺序插入还能利用自适应哈希AHI在当前页对齐位置。自增主键就是典型的这种模式左页继续保持满载右页刚打开后面的INSERT继续往右页填填满再来一次。所以自增插入绝对不会让索引变成一堆半空的碎片页。相反如果主键是UUID或者是状态值这种跳来跳去的数据InnoDB看不出顺序就会走page_get_middle_rec()从中间切。这才是大家口中常说的「劈成两半」。大约一半记录被搬走产生更长的Redo Log分裂后两页都只有半满。如果持续这样随机插半满的页会越来越多不仅浪费Buffer Pool范围扫描也更吃亏。4. 一次悲观分裂底层到底在忙什么悲观分裂是一套组合拳都在btr_page_split_and_insert()里挨个执行定切点往左、往右还是从中间切。要新页调用btr_page_alloc()申请新页并尽量要求物理上靠近当前页减少随机IO。申请不到直接报错返回。挂链表调用btr_attach_half_pages()把新页挂进B树修改前后页的指针并在父节点加上指向新页的记录如果父页也满了分裂会向上传导极端情况下根节点分裂树高加一。释放树锁如果新行放得下InnoDB会尽早释放整棵索引树的Latch。如果树锁抓太久别的插入连其他无关的页都进不去。搬数据最后才是把该搬的记录搬过去把新行写进去。一个小细节预留的1/16空间在连续向一侧插入且页未压缩时如果页内剩余空间小于「这一行的宽度一页的1/16约1KB」乐观插入会主动放弃提前去分裂。 注释里的理由很实在如果页被连续插入彻底塞死以后要是发生UPDATE把变长字段改大页会碎得很厉害。所以最右页「看着还有一点空就开始分裂」多半是触发了这个1KB的保护线而不是空间算错了。5. 尖刺在监控上到底长什么样假设有一张普通的订单表CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, payload VARCHAR(512), PRIMARY KEY (id), KEY idx_user (user_id) ) ENGINEInnoDB;业务侧有几十个连接同时INSERT。因为id越来越大所有线程算出来的插入目标全落在最右边那片叶子上。修改这页必须拿到排他X性质的Page Latch。同一时刻只能有一个插入在改它其他人全在门口等。正常情况下如果空地够乐观插入极快排队无感。 一旦空地不够触发悲观分裂申请新页、改链表、改父节点……操作耗时拉长门口的队伍瞬间变长监控上延迟直接翘起来QPS掉下去。分裂一结束新的最右页诞生继续飞速接客延迟瞬间掉回去。等这页再次被填满再来一轮。所以你看到的曲线是锯齿而不是越跑越慢。如果payload字段很大一页装不了几行这个锯齿就会非常密集。避坑指南别把页Latch和表级的AUTO-INC锁混为一谈。在MySQL 8.0中innodb_autoinc_lock_mode默认是2普通INSERT获取自增值几乎没有表级锁等待。如果你在锁监控里没看到AUTO-INC锁只有大量等待同一个index page的semaphore那就是最右页Latch被打满了。6. 两种常见的对比场景删数据也会卡但卡的不在同一处当你批量DELETE历史数据时页被掏空btr_compress()会尝试把空页和相邻页合并。这同样需要拿Page Latch、改链表、改父节点。 白天高并发自增写入卡在最右页夜里跑定时任务删老数据卡在被删的旧数据段。两件事经常叠在一张表上遇到这种情况先排查慢在哪别一上来就喊「索引坏了重建吧」。换成UUID主键会怎样UUID会让插入点散落在满树的各个节点单页上的并发争抢立刻缓解最右页的锯齿尖刺会大幅减轻。 但代价转移了页经常从中间切开产生大量半满页同样的数据量占用更多的页导致Buffer Pool命中率下降磁盘随机IO和逻辑读大幅上升。自增主键是用「所有写挤在一页」换取「树更紧凑、范围扫描更顺」。没有绝对的好坏取决于你的并发量和查询模式是否吃索引的紧凑度。7. 线上碰到了该怎么看和处理排查路径确认尖刺是否伴随大量INSERT且当时没有大规模DELETE排除页合并的干扰。使用SHOW ENGINE INNODB STATUS查看semaphore段。如果有大量线程等同一个index page且行锁和MDL锁都很干净基本就是最右页Latch争用。检查单行数据的宽度行越宽分裂越频繁以及是否有另一个非常热的二级索引。处理手段保持自增瘦身表结构主键继续自增。把大JSON、大文本TEXT/BLOB拆分到旁路表让主表叶子节点变窄。一页能放的行数翻倍分裂频率和搬运代价就会减半。保持参数确认innodb_autoinc_lock_mode2别乱改回1或0避免引入不必要的表级自增锁。打散热点如果写入并发实在太高单页Latch成了绝对瓶颈可以考虑按租户、时间或者Hash分表。让「1个最右页」变成「N个表的最右页」把并发摊开。别白费力气调innodb_fill_factor消除不了这个锯齿。该参数主要影响重建索引DDL时的填充率无法阻止运行时为了预留Update空间而触发的1/16分裂机制。总结最右页满了会分裂新行去右边左页保持紧凑。所有并发写入都在同一页抢Latch分裂那一下排队队伍变长监控上就会出现一下一下的尖刺。理清了这个底层逻辑你就能精准地决定是该“缩窄行宽”还是该“打散热点”而不是盲目地去重建索引。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

P1043 数字游戏【洛谷算法习题】 2026/9/27 22:48:37

P1043 数字游戏【洛谷算法习题】

P1043 数字游戏 网页链接 P1043 数字游戏 题目描述 丁丁最近沉迷于一个数字游戏之中。这个游戏看似简单,但丁丁在研究了许多天之后却发觉原来在简单的规则下想要赢得这个游戏并不那么容易。游戏是这样的,在你面前有一圈整数(一共 nnn 个…

阅读更多 →
【基于 Swoole+Hyperf 的微服务实战】第七周·周五:综合运用 RabbitMQ 生产者、消费者、死信队列、TTL 延迟消息、幂等消费和事件机制 2026/9/27 22:48:37

【基于 Swoole+Hyperf 的微服务实战】第七周·周五:综合运用 RabbitMQ 生产者、消费者、死信队列、TTL 延迟消息、幂等消费和事件机制

【基于 SwooleHyperf 的微服务实战】第七周周五:综合运用 RabbitMQ 生产者、消费者、死信队列、TTL 延迟消息、幂等消费和事件机制今天我们进入第七周周五,也是异步消息章节的收官之战。我们将综合运用 RabbitMQ 生产者、消费者、死信队列、TTL 延迟消息…

阅读更多 →
【三个月 AI Agent 实战学习】Day 19:第一阶段测验准备 —— 手动模拟 ReAct 循环 2026/9/27 22:48:37

【三个月 AI Agent 实战学习】Day 19:第一阶段测验准备 —— 手动模拟 ReAct 循环

Day 19:第一阶段测验准备 —— 手动模拟 ReAct 循环 欢迎来到第十九天!在前面的学习中,我们已经掌握了 Function Calling(Day 11),理解了模型如何请求调用工具。但 Function Calling 是 API 层面的机制&…

阅读更多 →
【八个月网安课程】第七周·周五:CSRF 防御——Token、Referer 校验、SameSite 2026/9/27 22:48:36

【八个月网安课程】第七周·周五:CSRF 防御——Token、Referer 校验、SameSite

以下是第七周周五学习内容的详细展开。今天你将武装目标站点,从攻击者视角切换到防御者视角,系统学习并亲手配置 CSRF 的三大主流防御手段。你将看到昨天的攻击页面在加固后的环境中一一失效,并理解每种防御的精确边界与绕过风险。第七周周五…

阅读更多 →
烧烤店扫码点单选型:9 个维度对比加单、串数、分桌与后厨分单 2026/9/27 22:48:36

烧烤店扫码点单选型:9 个维度对比加单、串数、分桌与后厨分单

烧烤店的单是餐饮里最不好扫的一类:一桌人边喝边加,既按串也按份,中途还有分桌、并台,凉菜、烤串、酒水要走出餐口。通用扫码点单放到烧烤场景,往往在“临时加单不重开单”和“按串/按份混合计价”这两步就卡住。本文把…

阅读更多 →
二维码在企业系统里怎么用?资产标签、产品页和客户入口 2026/9/27 22:48:29

二维码在企业系统里怎么用?资产标签、产品页和客户入口

二维码在企业系统里很常见:贴在设备上,扫一下报修;贴在产品包装上,扫一下看说明书;放在展会物料上,扫一下打开产品页;放在客户服务卡上,扫一下提交售后单。 但二维码不是“把 URL 生…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉