存储基础(5):B+Tree vs LSM——读写与范围查询的权衡

少于 1 分钟阅读

发布于:

本文是「存储基础系列」的第 5 篇:用“工作负载”的视角比较 B+Tree 与 LSM,两者没有绝对优劣,只有取舍。

B+Tree vs LSM:选型速览

范围扫描对比:B+Tree 更自然,LSM 需要 iterator merge

1. 两个结构各自擅长什么

B+Tree(典型:MySQL/InnoDB、很多 OLTP 引擎)

  • 点查稳定:树高有限(page 粒度),读路径可预测
  • 范围扫描友好:叶子节点链表/连续 page,顺序 IO 更容易
  • 更新就地写(in-place):代价是随机写多、需要页分裂/合并

LSM(典型:RocksDB、LevelDB、很多 KV 引擎)

  • 写入友好:随机写变顺序写(WAL + memtable + SST)
  • 吞吐高:尤其在写多场景
  • 读路径复杂:多层文件 + compaction,读放大与写放大互相牵制

1.1 一句话抓住“结构差异”

  • B+Tree:读写都围绕“页(page)”做就地维护,代价是随机写与刷脏页的复杂性。
  • LSM:写入先变顺序写,读通过 filter/index 缩小范围,后台合并维持有序与空间回收,代价是 compaction 的重写与资源竞争。

2. 范围查询为什么常常更偏爱 B+Tree

范围查询的核心是“连续读”:

  • B+Tree 的叶子节点天然按 key 有序,范围扫描常常是顺序读 page
  • LSM 虽然每个 SST 也有序,但同一 key 空间被切成多层文件,范围扫描可能要合并多个来源

所以在“范围扫描很多”的工作负载里,B+Tree 往往更自然。

2.1 LSM 范围扫描为什么更难(但也能优化)

LSM 的范围扫描困难点在于:同一 key-range 可能分散在多层多个文件里,需要 iterator 合并多个来源。

常见优化方向:

  • 让底层更“稳定”(leveled、减少重叠)
  • 更积极的 compaction 把旧版本/tombstone 清掉
  • scan 走顺序预取/避免污染 cache(不同系统策略不同)

3. 什么时候 LSM 更合适

典型特征:

  • 写入量大,写入模式更随机
  • 点查为主(而不是大范围 scan)
  • 可以接受一定 compaction 成本(或有足够后台资源)

常见系统会用:

  • Bloom Filter 降低读不存在的成本
  • block cache / page cache 提高热点命中
  • 合理的 compaction 策略控制写放大

3.1 什么时候 B+Tree 更合适

如果你的负载符合这些特征,B+Tree 往往更省心:

  • 范围查询/排序/二级索引很多(OLTP/OLAP 混合也常见)
  • 对尾延迟抖动敏感(尤其是“写抖动”不可接受)
  • 能接受 buffer pool 的内存成本,并且有成熟的刷盘/检查点策略

4. 一个“选型快速问答”

如果你在做系统设计,可以用下面的问题快速收敛:

  • 写多还是读多? 写多 → LSM 倾向更强
  • 点查多还是范围查多? 范围查多 → B+Tree 倾向更强
  • 延迟抖动能接受吗? 不能 → 更偏 B+Tree 或做严格后台隔离
  • 是否有大量覆盖/删除? LSM 需要 compaction 才能真正回收

4.2 一个更“工程决策化”的对照表(建议按你的场景勾选)

维度 B+Tree 倾向 LSM 倾向
点查稳定性 更可预测(树高稳定) 依赖层数/文件数/缓存命中
范围扫描 更自然(leaf 顺序) iterator merge 更复杂
写入吞吐 随机写压力更大 随机写→顺序写,吞吐更强
尾延迟抖动 更容易靠 buffer pool/刷盘策略控制 compaction/backlog 更容易抖,需要预算化
删除/覆盖回收 依赖页回收/空间管理 依赖 compaction(tombstone)
“工程形态” 更像数据库(多索引/复杂查询) 更像 KV/日志型系统(写多/简单查询)

4.1 选型不要忘了“工程形态”

最后提醒一个经常被忽略的维度:更偏向哪种工程形态?

  • 更像数据库(事务/多索引/复杂查询):B+Tree 家族更自然
  • 更像 KV/日志型系统(写多、简单查询):LSM 家族更自然

到这里你已经具备“能读懂大多数存储系统基本 trade-off”的最小知识集。后续系列会逐步补上:缓存层次、分布式复制/一致性、以及真实系统里的工程细节。