B+ 树并发插入时页分裂怎么保证读写安全?
简化版
B+ 树并发插入时,页分裂会同时影响叶子页、父页和兄弟链表。数据库通常用页级 latch、锁耦合、分裂顺序和右兄弟指针等机制,保证读线程不会走到坏结构,写线程不会互相覆盖修改。
详细版
并发下的 B+ 树要区分两类保护:
- 事务锁:保护数据逻辑并发,如行锁、间隙锁;
- latch:保护内存页结构短时间不被并发破坏。
页分裂大致步骤:
- 对目标叶子页加写 latch;
- 分配新页,把部分记录搬过去;
- 设置新页和旧页的兄弟指针;
- 将分隔 key 插入父页;
- 必要时递归分裂父页;
- 释放 latch。
为了避免读线程在父页尚未更新时找不到新页,很多实现会维护右兄弟指针和 high key,让读者能从旧页向右追到正确页。具体实现依数据库而异,但核心目标是结构一致性和死锁规避。
完整版教学
一、为什么并发页分裂比单线程复杂
单线程里,页满了就分裂,再把分隔 key 插入父节点,过程可以一步步完成。并发场景中,读线程可能正在查这个范围,另一个写线程可能也想插入同一页。如果中间状态暴露出去,读可能漏数据,写可能覆盖指针,甚至树结构断裂。
B+ 树页分裂不仅改一个页,还会改叶子链表、父节点分隔 key,有时还会向上传播。并发控制必须保证这些局部变化对其他线程要么安全可见,要么能被纠正。
记忆钩子:事务锁管“谁能改哪条数据”,latch 管“页结构别在我手里散架”。
二、事务锁和 latch 有什么区别
事务锁面向用户数据语义,可能持有到事务提交。latch 面向内存结构保护,持有时间很短,通常只覆盖一次页读写或结构调整。把两者混淆,会很难理解数据库索引并发。
| 维度 | 事务锁 | latch |
|---|---|---|
| 保护对象 | 行、范围、事务语义 | 内存页、指针、页目录 |
| 持有时间 | 可能到提交 | 很短 |
| 目的 | 隔离性 | 结构一致性 |
| 例子 | 行锁、间隙锁 | 页读写 latch |
B+ 树分裂主要需要 latch 保护结构,同时还要配合事务锁保证逻辑隔离。
三、分裂过程有哪些危险窗口
假设叶子页 P 满了,要分裂出新页 Q。如果先把部分记录搬到 Q,但还没更新父页,读者从根走下来仍会到 P。此时如果 P 不知道 Q 的存在,读者可能以为目标 key 不存在。若先更新父页但 Q 还没填好,读者可能进入未完成的新页。
因此实现需要规定安全顺序。例如先构造好 Q,再连接兄弟链,再发布父节点分隔信息。某些 B-link tree 设计会让旧页有 right link 和 high key,即使父节点暂时没更新,读者也能发现目标 key 超过当前页范围,然后向右追到新页。
P highKey=50, right=Q
查 key=60 到达 P
发现 60 > highKey,于是沿 right 到 Q
这种设计降低了父节点更新滞后的风险。
四、锁耦合和自顶向下保护
锁耦合指沿树向下走时,先拿到子页 latch,再释放父页 latch,保证移动过程中结构不会消失。插入时如果预判子页安全,不会分裂,可以早释放祖先 latch;如果子页可能分裂,可能要保留或重新获取父页写 latch。
不同数据库实现策略不同。有的采用自顶向下提前分裂,有的采用乐观下降,发现需要分裂再重试。核心目标都是减少 latch 持有范围,同时避免结构修改冲突。
五、右兄弟指针和 high key 的作用
叶子链表本来用于范围扫描,并发分裂时也能帮助查找纠偏。high key 表示当前页负责的最大 key 边界,right link 指向右兄弟。如果线程走到一个已经分裂但父节点还没完全同步的旧页,可以通过 high key 判断自己应该继续向右。
| 元信息 | 作用 |
|---|---|
| right link | 找到分裂后的右兄弟 |
| high key | 判断目标 key 是否超出当前页 |
| page latch | 保护页内结构修改 |
| parent separator | 让后续查询从父页直接走对 |
这些机制组合起来,让 B+ 树在高并发写入下仍能保持可查找。
六、为什么要小心死锁和长时间 latch
如果线程 A 拿着叶子页 latch 等父页,线程 B 拿着父页 latch 等叶子页,就可能死锁。因此 B+ 树实现通常规定 latch 获取顺序,或在拿不到时释放并重试。latch 持有时间越短,系统吞吐越好。
页分裂还可能触发父页分裂,甚至一路到根。为了避免一次操作长时间锁住整条路径,工程实现会设计复杂的重试和安全节点判断。这也是数据库索引并发远比教材插入算法难的地方。
七、常见误区与追问
- 误区:B+ 树并发只靠事务锁。 事务锁保护逻辑数据,页结构还需要 latch。
- 追问:父页还没更新时读者怎么找到新页? 可通过 right link 和 high key 从旧页追到右兄弟。
- 误区:页分裂只改叶子页。 它还会改兄弟指针和父节点分隔 key,可能向上传播。
- 追问:为什么 latch 不能持有太久? 它阻塞结构访问,持有过久会严重影响并发吞吐。
- 误区:所有数据库 B+ 树并发实现都一样。 具体协议不同,但目标都是结构一致、可恢复和高并发。
八、加强记忆
B+ 树并发分裂要同时照顾结构安全和查询可达。latch 短时间保护页结构,事务锁保护业务隔离;分裂要先让新页内容和兄弟关系安全,再发布父节点分隔信息;right link 和 high key 能处理父页更新滞后的窗口。面试中不用背某个数据库源码,能讲清这些危险窗口和保护手段就很扎实。