LSM Tree 原理详解
写放大是数据库的性能杀手。随机写入磁盘、频繁更新索引——这些问题折磨了一代数据库工程师。直到 LSM Tree 的出现。
LSM Tree(Log-Structured Merge Tree)是一种专门为写优化设计的存储结构。HBase、RocksDB、LevelDB 都在用它。
LSM Tree 的核心思想
传统 B+ Tree 的写入需要:定位数据位置 → 写入数据 → 更新索引 → 刷盘。每次写入都可能触发多次磁盘 I/O。
LSM Tree 的核心洞察:把随机写变成顺序写。
LSM Tree 的写入流程:
- 先写内存:数据先写入内存缓冲区(MemTable)
- 写 WAL:同时写入预写日志(WAL),保证崩溃不丢失
- 异步合并:内存写满后,异步合并到磁盘(SSTable)
- 分层组织:磁盘上的数据分层组织,逐层合并
LSM Tree 分层结构
LSM Tree 将磁盘数据分为多层,越上层数据越新,越下层数据越旧。
每一层的容量限制
每一层通常比上一层大 T 倍(例如 T = 10):
- L0: ~4 个 SSTable
- L1: 4 × 10 = 40 个 SSTable
- L2: 40 × 10 = 400 个 SSTable
当某一层达到容量限制时,触发合并(Compaction),把数据合并到下一层。
写入流程详解
Step 1: 写入 MemTable
MemTable 通常用跳表(Skip List)实现,保证写入有序且复杂度为 O(log n)。
Step 2: 写入 WAL
WAL(Write-Ahead Log)记录了所有未刷盘的修改。崩溃恢复时,重放 WAL 恢复数据。
Step 3: MemTable 刷盘
MemTable 写满后,转换成 Immutable MemTable,然后异步刷盘成 SSTable。
Step 4: 合并(SSTable Compaction)
当 L0 达到容量时,与 L1 进行合并。合并过程:
- 读取 L0 和 L1 的所有数据
- 按 key 排序合并
- 保留每个 key 的最新值
- 写入新的 L1 SSTable
读写放大问题
LSM Tree 的设计有代价。
写放大(Write Amplification)
一次写入可能触发多次磁盘写入:
实际写入量可能是原始数据的 3~10 倍。
读放大(Read Amplification)
读取一条数据可能需要检查多层:
最坏情况下需要读取所有层。
LSM Tree vs B+ Tree
适用场景
LSM Tree 适合:
- 日志系统:写入密集,极少读取历史
- 时序数据库:数据按时间追加
- 消息队列:写入后批量消费
- NoSQL 数据库:HBase、RocksDB
B+ Tree 更适合:
- 关系型数据库:读写都频繁
- 交易系统:需要强一致性
- 随机更新频繁的场景
核心权衡:LSM Tree 用写入放大换读取放大。如果你有大量顺序写入但查询延迟不敏感的场景,LSM Tree 是更好的选择。