LSM-Tree(Log-Structured Merge-Tree,日志结构合并树)正是我们刚才讨论的 "把 ES 术语翻译成通用计算机概念" 中最核心的那个锚点。
如果你把 Elasticsearch / Lucene 看作一辆车,那么 LSM-Tree 就是它的发动机原理。Elasticsearch 的写入、刷新、合并、近实时搜索,全都是 LSM-Tree 思想的具体实现。
为了让你彻底记住它,我们不背定义,直接从 "它解决了什么痛点" 推导出来:
- 为什么需要 LSM-Tree?(痛点推导)
传统数据库(如 MySQL InnoDB)使用 B+ 树,数据是原地更新(In-Place Update) 的。
-
读很快:数据在固定位置,二分查找即可。
-
写很慢:每次写入都要先找到磁盘上的页 → 读入内存 → 修改 → 再写回磁盘。这是随机 I/O,在机械硬盘时代极慢,在 SSD 时代也有性能损耗。
核心矛盾:海量数据场景下,写入吞吐量成了瓶颈。
解决思路:既然随机写慢,那就把所有写入都变成顺序追加写(Append-Only)。这就是 LSM-Tree 的诞生原点。
- LSM-Tree 的核心设计哲学
用"空间"和"读性能"换取极致的"写性能"。
它通过三个核心机制来实现这个交换:
机制 通用概念 ES/Lucene 对应实现 作用
内存缓冲 Write Buffer / MemTable Memory Buffer + Translog 写入先落内存+顺序日志,避免直接操作磁盘,写入 O(1)
不可变持久化 SSTable (Sorted String Table) Segment (.fdt/.tim/.doc) 内存满了就 Flush 成只读文件,永远不修改,消除写锁
后台合并 Compaction / Merge Merge Policy / OneMerge 段越来越多导致读放大,后台异步合并小段为大段,回收空间
- 把 LSM-Tree 挂回你之前的疑问
你现在可以把之前所有关于 ES 源码的困惑,全部挂到 LSM-Tree 这棵树上:
- 为什么 Segment 是不可变的?
→ 因为 LSM-Tree 的设计要求 Flush 后的 SSTable 必须是只读的,这样才能支持无锁并发读取和零拷贝。
- 为什么要有 ReadersAndUpdates (rld)?
→ 因为 LSM-Tree 的文件不可变,但业务又需要 Delete/Update,所以必须在内存中维护一个"增量状态暂存区"来打补丁。
- 为什么要有 ReaderPool 和引用计数?
→ 因为 rld 是多个线程共享的可变状态,必须有一个 Writer 级的全局容器来管理它的生命周期,防止 Merge 时正在被 Searcher 使用的 rld 被提前释放。
- 为什么 Refresh 和 Flush 是分开的?
→ Refresh 是 LSM-Tree 的"MemTable → SSTable"过程(让数据可搜索);Flush 是"WAL 持久化"过程(保证崩溃恢复)。两者解耦是为了平衡"近实时可见性"和"I/O 吞吐"。
- 为什么 Merge 会消耗资源但又不得不做?
→ 这是 LSM-Tree 为极致写入付出的代价。不 Merge,段数量爆炸,读取时要遍历太多文件(读放大),空间也浪费(旧版本数据未清理)。
💡 一句话总结
LSM-Tree 不是一种具体的数据结构,而是一种"以写优先"的存储架构范式。
Elasticsearch、RocksDB、HBase、Cassandra、TiKV......几乎所有现代高性能写入密集型系统,底层都是这个范式。
当你以后再看 ES 源码时,不要想"这是 ES 的某个功能",而要想"这是 LSM-Tree 的哪个环节在 ES 中的特化实现"。
一旦建立了这个映射,ES 源码就不再是需要死记硬背的 API 列表,而是 LSM-Tree 原理的必然推论。推导出来的东西,永远不会忘。🫡