B+ 树与 LSM 树:外存数据结构
一句话定义
B+ 树(B+ Tree)是把 BST 思想按磁盘块放大成「一节点一块、百路扇出、数据全在叶子」的多路平衡树,让树高即磁盘 IO 次数;LSM 树(Log-Structured Merge Tree)反其道而行,把随机写变成内存表 + 顺序追加 + 后台分层合并,用写性能换读放大。二者是数据库存储引擎的两大骨架。
为什么重要
内存里的「O(log n)」到了磁盘会失效:磁盘一次 IO 的代价相当于内存数十万次运算,瓶颈从「比较次数」变成「读块次数」。B+ 树与 LSM 树展示的是同一件事——按新的硬件代价模型重新设计结构。这是「数据结构选型 = 对硬件代价建模」这一工程方法论的最好教材,也解释了 MySQL InnoDB 与 LevelDB/RocksDB 的根本分野。
前置知识
kp-013(平衡树与旋转思想);磁盘页(通常 4KB–16KB)与 IO 成本的常识。
核心概念
- IO 代价模型:复杂度按「IO 次数」计;一个 16KB 页可装数百个键 ⇒ 扇出 m ≈ 数百,n 条记录树高仅 log_m n ≈ 3~4 层。
- B+ 树结构:内部节点只存键与孩子指针做路标;全部数据在叶子,叶子间以链表串联 ⇒ 范围扫描变成叶子链表顺序读。
- 节点规模约定:m 阶 B 树内部节点孩子数在 ⌈m/2⌉~m 之间,根例外;插入满则分裂、删除过半则借/合,树高只在根分裂时 +1。
- LSM 写路径:写 → 内存 memtable(常用跳表)→ 满了冻结为 SSTable 顺序落盘 → 层数逐级 compact 合并去重。
- LSM 读路径:memtable → L0 → L1… 逐层查,配合每层布隆过滤器(kp-010)挡「不存在」。
- 三大放大:读放大(一次读要看几层)、写放大(一条写入最终落盘几遍)、空间放大(旧版本未清时的膨胀);RocksDB 的调优本质是在三者间取舍。
直观类比
B+ 树是图书馆的分层索引牌:每层牌只告诉你去哪个大区(读一块,缩小百倍范围),书全在一楼书架(叶子)且书架之间连着传送带(叶子链表),查一个区间就是顺着传送带走。LSM 是「先往流水账本上按顺序记,记账极快;每隔一段时间把账本誊清合并去重」,代价是查一笔旧账要翻好几本账本。
原理与机制
树高估算(B+ 树为什么 3 层就够):页 16KB,键 8B + 指针 6B ⇒ 每页约容纳 1000 个路标;三层即 1000 × 1000 × 1000 ≈ 10⁹ 条记录的索引,而一次点查最多 3~4 次页读(含根缓存命中则更少)。LSM 的写放大来源:一条键值对在 L0→L6 的逐层 compact 中平均被重写多次(大小分层策略下与层数同量级),故读少写多选 LSM、读多写少 B+ 树常胜。
图示
B+ 树(扇出 4 简化):
[ 17 | 35 ] ← 内部节点(路标)
/ | \
[5|9|12] [17|21|28] [35|40|46] ← 叶子(含数据)
└──────┴─叶子链表─┴────────┘ 范围扫描沿链表走
LSM 写路径: 写请求 → memtable(内存) → 冻结SSTable(L0)
→ compact 合并 → L1 → L2 → …(逐层更大、有序)
读: memtable → L0 → L1…(布隆过滤器提前拦截未命中)
实例或案例
- MySQL InnoDB、PostgreSQL 主索引:B+ 树;叶子即数据页(聚簇)或行指针(二级索引)。
- LevelDB / RocksDB / Cassandra / HBase:LSM 系;写吞吐高,适合日志、时序、消息类负载。
- 文件系统(ext4/xfs 的目录项与 extents 管理)与 MongoDB WiredTiger 同样是 B+/B 树家族。
- 调优实例:RocksDB 增大
max_background_compactions缓解写停顿;InnoDB 调整页大小与 buffer pool 命中率。
常见误区
- 把 B 树当成「二叉树的多叉版」混谈:B+ 树的关键增量是「数据只在叶子 + 叶子链表」,直接决定范围查询性能。
- 认为 LSM 一切场景更快:读放大在点查密集负载上常把 LSM 打回原形,选型要按读写比。
- 忽略页大小与扇出的关系:页越小扇出越少树越高,IO 反而更多——结构参数由硬件代价模型决定,不是拍脑袋。
- 把树高等同于 IO 次数却不考虑缓存:根与上层常驻内存,实际 IO 常只有 1 次(叶层)。
自测题
- 扇出 200、1 亿行数据的 B+ 树约几层?一次点查最多几次页读?
答案要点:200² = 4×10⁴,200³ = 8×10⁹ ≥ 10⁸ ⇒ 3 层(含根),最多 3 次页读,缓存命中后常 1 次。
- LSM 为什么写快?读慢在哪?
答案要点:写全部转为内存表+顺序追加,无随机 IO;读可能要查 memtable 加多层 SSTable(读放大),靠布隆过滤器与缓存缓解。
- 读多写少的报表库应选 B+ 树还是 LSM?为什么?
答案要点:B+ 树;点查与范围读的 IO 路径短且稳定,LSM 的多层读放大在此得不偿失。
与其他知识点的关系
kp-013 的平衡/旋转思想是 B+ 树分裂合并的源头;kp-010 布隆过滤器是 LSM 读路径的标配;kp-023 外部排序的「有序 run + 归并」与 SSTable 的分层合并同构;kp-032 的选型决策树把两者放进同一张表。
延伸阅读
Comer, "Ubiquitous B-Tree";O'Neil 等 LSM-Tree 论文;Database Internals(Alex Petrov)第一部分。