首页 › programming › 删除要先写:LSM树从不真的修改

删除要先写:LSM 树从不真的修改

导语:为什么最快的数据库写入,其实什么都没"改"

如果你问一个程序员:"数据库里删一条数据会发生什么?"十个人里九个会直觉地回答:"把那条数据从硬盘上抹掉。"剩下那个会谨慎一点说:"标记为删除,腾出空间。"——这两个人都错了,至少在 RocksDB、Cassandra、CockroachDB、HBase、LevelDB 这些统治现代数据基础设施的引擎里全错。

真实的答案是:删一条数据,反而要先往硬盘上再写一条数据。 你想抹掉的东西不仅没消失,反而又多出来一份。这种"删除即写入"的荒诞感,正是 Log-Structured Merge-Tree(LSM 树)最反直觉的地方——也是它能在写入性能上碾压传统 B+ 树的根本原因。

本期解读聚焦 YouTube 上《What Is an LSM Tree? The Data Structure Behind Most Modern Databases》这期高热度技术视频。这期内容之所以值得每个后端工程师停下手里活计认真看完,是因为它戳破了一个普遍的幻觉:很多人以为数据库的快慢取决于"用了什么语言写的""内存多大""SSD 够不够猛"。但 LSM 树用一个 1996 年就提出、至今统治半个数据库世界的核心思想告诉你——真正的瓶颈从来不是算力,而是"随机 I/O"这个恶魔。 一旦你把所有操作都改造成"只追加、不修改",性能就会脱胎换骨,代价则藏在读放大、写放大、空间放大这三个你看不见的角落里。

这篇文章我会把视频的核心论点拆成 8 个要点,补足视频没展开的存储介质原理、压实算法(compaction)的工程细节,以及那张让所有数据库调优工程师夜不能寐的"放大三角"。读完之后,你应该能回答一个面试经典题:为什么写多读少的场景要用 LSM 树,而银行转账系统却死守 B+ 树?

视频背景卡片

项目 内容
视频标题 What Is an LSM Tree? The Data Structure Behind Most Modern Databases
视频链接 https://www.youtube.com/watch?v=rOC7jkN748w
频道 数据库系统设计教学类频道(受搜索工具限流影响,频道名与精确播放量未能完整核验,视频描述明确指向 RocksDB / Cassandra / CockroachDB 主题)
内容定位 面向后端/系统设计方向,讲透现代 NoSQL 与时序数据库的写入引擎原理
时长区间 约 15–25 分钟的深度讲解类长视频
核心主题 LSM 树:从 1996 年 O'Neil 论文到当代数据库存储引擎的写入优化哲学
关键事实 LSM 树由 Patrick O'Neil 等人于 1996 年提出,发表于 Acta Informatica 33(4):351–385

说明:受 WebSearch 与内容抓取工具当周配额限制,部分元数据(频道名、确切播放量、分钟数)无法精确获取,上表已如实标注。技术内容以某百科 LSM 条目、O'Neil 原始论文与 RocksDB 官方文档交叉核实。

核心内容拆解

要点一:真正的敌人是随机 I/O,不是数据量

要理解 LSM 树,必须先理解一个被多数人低估的事实:硬盘的随机读写和顺序读写,性能差距是数千倍甚至十万倍量级的。 在机械硬盘(HDD)上,顺序写可以逼近 100 MB/s,而随机写每秒只能做几十到几百次(每次都要磁头物理寻道)。即使到了固态硬盘(SSD),这个鸿沟依然存在——闪存的最小擦除单位(erase block)是 256KB 甚至 4MB,而你写一个 4KB 的页就可能触发整块擦除重写,这就是闪存层的"写放大"。顺序追加写入能让 SSD 把零散的小写聚合起来,从而把吞吐量拉满。

视频用一句话点破了 LSM 的全部秘密:把所有随机写,都改造成顺序写。 这正是 1996 年 Patrick O'Neil、Edward Cheng、Dieter Gawlick、Elizabeth O'Neil 在论文《The Log-Structured Merge-Tree (LSM-Tree)》里提出的核心动机——他们要"减少硬盘的总寻道时间"。维基条目也明确记载:LSM 的性能优势"源于每个组件都针对其底层存储介质的特性做了调优,并以滚动批次的方式在介质间高效迁移数据,使用类似归并排序的算法……以顺序方式写入数据,而非一系列独立的随机访问请求"。

延伸判断:这条原理在 SSD 时代依然成立,而且随着 3D NAND 堆叠层数增加、QLC 闪存随机写寿命下降,"顺序追加"反而比二十年前更重要。这也是为什么 Meta(Facebook)要用 RocksDB 替换 MySQL 的 InnoDB 引擎(即 MyRocks 项目)——同样的数据,写吞吐显著提升。

要点二:写入的秘密——永远只追加,从不修改

LSM 树的写入路径只有四个动作,且每一个都是"追加",没有一个是"修改":

  1. 写 WAL(Write-Ahead Log,预写日志):数据先顺序追加到磁盘上的 WAL 文件,保证机器宕机也不丢。
  2. 写 MemTable(内存表):数据进入内存里一个有序结构(通常是跳表 skip list 或红黑树),常驻内存、可修改。
  3. Flush(刷盘):当 MemTable 撑到阈值(RocksDB 默认 64MB,可配),它就被冻结成不可变(immutable)的 SSTable(Sorted String Table,有序字符串表),顺序写到磁盘的 Level 0。
  4. Compaction(压实):后台线程把 SSTable 从低层级归并到高层级。

注意第 3 步:MemTable 一旦落盘就变成 SSTable,而SSTable 是永远不可变的——它被写下来那一刻就钉死在硬盘上,谁也不能改它一个字节。你要"更新"一个键?对不起,旧值原封不动躺着,你只是在更高层(更新的)SSTable 里追加了一个新值。这就是标题那句话的来由:LSM 树从不真的修改数据,它只追加新版本。 旧版本像地层里的化石一样一层层压在底下,等着后台清道夫来清理。

为什么这样做快? 因为追加是纯顺序写,磁头(或闪存控制器)只需一路写下去,不需要先找到老位置、读出来、改完再写回去(B+ 树正是这么干的,每次更新都是一次"读-改-写"的随机 I/O)。把"更新"降级成"插入",是 LSM 性能跃迁的工程魔法。

要点三:删除不是删除,是写入一块"墓碑"

这是整个 LSM 故事里最反直觉、也最让人印象深刻的一点。当你在 Cassandra 或 RocksDB 里执行 DELETE FROM table WHERE id=42,引擎做的事情是:往 MemTable 里插入一条特殊的标记,叫"墓碑"(tombstone)。 删除,本质上是一次插入操作。

为什么不能真删?因为真删意味着要打开那个不可变的 SSTable,找到那条记录,抹掉它,再把整个文件重新写一遍——这就退化成了随机 I/O + 全量重写,等于把 LSM 的全部优势亲手毁掉。所以引擎选择"撒谎":它不删,它只是写一张墓碑贴上去,宣称"42 这个键,从现在起死了"。之后任何读操作查到 42,看到墓碑就知道"这个键已经不存在",于是返回"未找到"。

这块墓碑会一直躺着,直到压实(compaction)把它和原始数据一起合并掉。 这带来一个真实存在的工程陷阱:如果你疯狂删除数据,但压实没跟上,你的磁盘空间不降反升——因为每删一条都在追加墓碑。Cassandra 运维圈子里有个经典故障模式:大量删除后磁盘报警,根因正是墓碑堆积(tombstone overload),一次读甚至要扫过成千上万块墓碑才能确认"真的没了",读延迟飙升。处理方式是强制触发压实来清掉墓碑。

要点四:更新也是写入——同一份数据,硬盘上存了好几份

理解了删除和更新都是"追加新版本",一个必然的推论就出现了:同一份逻辑数据,在硬盘上可能同时存在好几个版本。 你把用户 42 的余额从 100 改成 200,再改成 300,硬盘上就同时躺着三条记录:42→100(最老)、42→200(中间)、42→300(最新,在最高层 SSTable)。

这就是 LSM 的空间放大(Space Amplification):实际占用的磁盘空间,比你"逻辑上"拥有的数据量大得多。比例可能达到 1.5 倍甚至数倍,直到压实把旧版本清理掉。这也是为什么 LSM 树的存储引擎通常需要预留相当大的额外磁盘空间(RocksDB 文档建议预留 30%–50%),不能把盘塞满——塞满之后压实无处可写,写入会直接卡死。

读的时候怎么知道哪条是最新?靠层级的时间戳/序号:LSM 树按层级组织,越高层(Level 0 最新,逐级往下越老)的数据越新。读操作从最新层往老层找,找到的第一个匹配项就是当前有效值。这是一条简单却关键的规则,它让"永远只追加"成为可能而不至于读到陈旧数据。

要点五:读为什么变慢了——读放大的代价

写入快如闪电的代价,全部转嫁到了读上。在 B+ 树里,一次点查询(point lookup)只需 O(log N) 次磁盘访问,路径明确。但在 LSM 树里,一次读要做的事情多得多:先查 MemTable,再查 Level 0 的多个 SSTable(这一层键范围是重叠的,每个文件都可能命中),再逐层往下查每一层的 SSTable。 维基条目原文印证:"要对一个键做查询以获取其关联值,必须在 Level 0 树以及每个 run(有序数据段)中搜索。" 这就是读放大(Read Amplification)——为了读到一条数据,可能要翻找好几个文件。

工程师当然不会坐以待毙,他们用两件武器把读放大压下去:

延伸判断:即便有布隆过滤器加持,LSM 树的点读延迟和尾延迟(P99)通常仍逊于 B+ 树,因为前者本质上需要"多查几个地方再取最新"。这就是为什么纯读密集、要求稳定低延迟的 OLTP 系统(如银行核心、订单库)至今偏爱 B+ 树(InnoDB、PostgreSQL),而写密集的场景才用 LSM。

要点六:后台的清道夫——Compaction(压实)才是真正的灵魂

前面反复提到的"压实(compaction)",是 LSM 树真正复杂、也是真正花钱的地方。SSTable 是不可变的,旧版本和墓碑只能越堆越多,必须有后台线程持续地把它们归并(merge sort)、丢弃旧版本和过期墓碑、重新写成更大的文件下沉到更深层级。这个过程就是 compaction。

维基条目用"类似归并排序(merge sort)的算法"来描述它——这正是关键。压实把多个有序的 SSTable 当成多个已排序数组的归并:相同键只保留最新版本,墓碑和被覆盖的旧值在此刻被真正消灭。

但压实不是免费的,它带来了写放大(Write Amplification)——这是 LSM 最隐蔽也最致命的成本。所谓写放大,指你逻辑上写一次数据,底层物理上会被重写很多次。在 RocksDB 默认的"分级压实(leveled compaction)"下,一条数据从 Level 0 一路被压实到 Level N,会被反复读出、归并、重写。粗略估计,分级压实的写放大约为 T × L(层级大小比 × 层数),典型配置下 T≈10、L≈6–7,写放大可达 30–50 倍。也就是说,你写入 1 GB 数据,硬盘底层可能实际写了 30–50 GB。

压实的策略决定了系统性格走向,业界主要有两派:

这就是为什么 RocksDB Wiki 有一句箴言:"大小分级压实以牺牲读放大和空间放大为代价,换取最小的写放大。" 天下没有免费的午餐,压实策略的本质就是在三个"放大"之间挪筹码。

要点七:三个"放大"的不可能三角

把上面三个要点合起来,就浮现出 LSM 树最核心的工程真理——写放大、读放大、空间放大,三者构成一个不可能三角,你最多只能同时优化其中两个。

flowchart LR
    subgraph TRI["LSM 三大放大 不可能三角"]
      WA["✍️ 写放大 Write Amplification<br/>1次写入被重写几十次<br/>代价: 压实CPU/IO/SSD寿命"]
      RA["🔍 读放大 Read Amplification<br/>1次读取要查多个文件<br/>代价: 读延迟/P99升高"]
      SA["💾 空间放大 Space Amplification<br/>同一数据存多份<br/>代价: 磁盘占用/成本"]
    end
    WA <-->|"压实越激进 → 写放大大 但读/空间小"| RA
    RA <-->|"布隆过滤/缓存 → 降读放大 但费内存"| SA
    SA <-->|"频繁压实 → 降空间 但推高写放大"| WA

    style WA fill:#ffe0e6
    style RA fill:#e3f2fd
    style SA fill:#fff3e0

RocksDB 暴露了几十个参数(write_buffer_size、max_background_compactions、level0_file_num_compaction_trigger、target_file_size_base……)就是让你在这个三角里精细挪位。每一个生产环境的 RocksDB 调优故事,本质上都是在三个角之间权衡取舍。没有"最优配置",只有"最匹配你工作负载的配置"——这是 LSM 工程的终极心法。

要点八:为什么是它统治了 NoSQL 与时序世界

理解了原理,就不难理解 LSM 树为何在过去十年攻城略地。它的绝对优势在写密集场景:物联网海量传感器数据、应用日志、监控指标、社交时间线、聊天消息、点击流、广告埋点——这些工作负载共同的特征是"写入洪流 + 偶尔读取",恰好踩在 LSM 的甜点上。

这就是为什么 LSM 成了 NoSQL 和时序数据库的事实标准:

反观读密集、强事务、要求稳定低延迟点读的场景——银行转账、电商订单、ERP——依然是 B+ 树(InnoDB、PostgreSQL)的地盘。LSM 树并没有"取代"B+ 树,二者是在性能光谱的两端各占一头。 选型的唯一标准是你的工作负载形状:读写比、是否容忍尾延迟、磁盘预算、能否接受墓碑与放大成本。能把这一点讲清楚的工程师,才算真正"懂"数据库。

知识结构图:一次写入,在 LSM 树里的完整一生

下图把写入路径、压实过程和读取路径串成一张全景图,帮你建立空间直觉:

flowchart TD
    W([✍️ 写入请求 Write/Delete/Update]) --> WAL["① WAL 预写日志<br/>顺序追加,保持久性"]
    WAL --> MEM["② MemTable 内存表<br/>有序,常驻内存"]
    MEM -->|"满 → 冻结 immutable"| L0["③ Level 0 SSTable<br/>不可变,键范围重叠"]
    L0 --> COMP["④ Compaction 压实<br/>归并排序 + 丢旧值/墓碑"]
    COMP --> L1["Level 1<br/>约 10× 大"]
    COMP --> L2["Level 2<br/>约 100× 大"]
    COMP --> LN["Level n ..."]

    R([🔍 读取请求 Read]) --> Q1{"查 MemTable?"}
    Q1 -->|未命中| BF{"逐层问 Bloom Filter<br/>这个键可能在此文件吗?"}
    BF -->|肯定不在| SKIP["跳过 0 磁盘IO"]
    BF -->|可能在| SST["读 SSTable 磁盘"]
    SST --> RTN["取层级最新的版本返回"]
    Q1 -->|命中| RTN

    style W fill:#e8f5e9
    style WAL fill:#c8e6c9
    style MEM fill:#fff3e0
    style L0 fill:#ffe0b2
    style COMP fill:#fce4ec
    style BF fill:#bbdefb
    style RTN fill:#dcedc8

记住这条主线:写永远向右追加(WAL → MemTable → SSTable → 压实下沉),读永远向左回溯(从最新层往老层找,靠 Bloom Filter 加速)。 写快是因为只追加不修改;读慢是因为要回溯多层——这两件事是同一枚硬币的两面。

实战启示:你应该怎么用上 LSM 树的智慧

1. 给选型决策一把新标尺。 下次技术选型别再纠结"MySQL 还是 MongoDB"这种伪命题,先问三个真问题:你的读写比是多少?能不能容忍 P99 偶尔抖动?磁盘预算给得起 1.5 倍冗余吗?写多读少、能容忍尾延迟、磁盘宽松 → LSM 系(Cassandra、ScyllaDB、RocksDB 嵌入式);读多写少、强事务、要求稳定点读 → B+ 树系(PostgreSQL、MySQL/InnoDB)。

2. 把"删除即写入"的直觉刻进肌肉记忆。 在 LSM 数据库里,批量删除绝不等于腾出空间。如果你在做数据保留策略(retention policy),别用 DELETE,而要用 TTL(time-to-live)+ 分区/表级整体丢弃(如 Cassandra 的 TTL、ClickHouse 的 partition 删除),让旧数据被成段抹掉而非留下成山墓碑。否则你会遇到"删了一半数据,磁盘反而满了"的灵异故障。

3. 调优就是在三个放大间挪筹码。 当你接到"RocksDB 写入太慢/CPU 飙高"的工单,第一反应不是加机器,而是查写放大:是不是分级压实太激进?换成大小分级(level_compaction_dynamic_level_bytes=false + tiered)能否降写放大?反之,"读延迟高"则查读放大:布隆过滤器够不够大(bloom_bits)?块缓存够不够(block_cache_size)?每个旋钮都对应三角的一个角,想清楚你在优化哪一个、牺牲哪一个。

4. 给系统设计面试一个降维答案。 面试官问"设计一个每天写入百亿条日志的监控系统"时,不要只说"用 Kafka + 数据库"。说出 LSM:解释为什么日志型写入该选基于 LSM 的时序库(InfluxDB、ClickHouse 的 MergeTree 本质也是 LSM 思想),点出写放大、压实、布隆过滤器这几个关键词,再用"读放大需要布隆过滤器和缓存兜底"收尾——这一套下来,证明你不只是会用数据库,而是理解它内部在烧什么。

5. 顺序写的思想可以迁移到你自己写的代码里。 LSM 的哲学本质是"批量 + 顺序"。当你写一个高频日志/埋点上报服务,与其每条都同步 fsync 随机写文件,不如先攒进内存 buffer,再顺序批量刷盘——这就是 LSM 思想在应用层的微缩版。性能提升往往来自对底层介质特性的尊重。

延伸学习路径

  1. 原论文:《The Log-Structured Merge-Tree (LSM-Tree)》,O'Neil 等,Acta Informatica 33(4):351–385, 1996。论文 PDF 可在 cs.umb.edu/~poneil/lsmtree.pdf 找到,是一切 LSM 知识的源头。
  2. 《Database Internals》(Alex Petrov 著,O'Reilly):系统讲透 B+ 树、LSM 树、SSTable、压实,是后端工程师的存储引擎圣经。YouTube 上也有基于此书的深度讲解视频。
  3. RocksDB 官方 Wiki(GitHub facebook/rocksdb/wiki):尤其是 "Basic Operations"、"Compaction"、"Write Buffer" 三页,含真实可调参数与写放大实测数据。
  4. Arpit Bhayani 的系统设计系列(YouTube/Blog):他有专门拆解 LSM 树、布隆过滤器、压实算法的高质量中文/英文讲解,适合配套视频对照学习。
  5. 动手实践:用 LevelDB(API 简洁、单文件)写一个 100 行的小 KV 存储,观察 MemTable flush 与压实过程;再用 strace/iotop 实测写入时的写放大,亲手摸到那 30–50 倍的物理重写。

一句话总结

LSM 树用"永不修改、只追加新版本"这一条铁律,把随机 I/O 这个性能恶魔彻底关进笼子——代价是删除变成写入、读变慢、空间翻倍,而所有调优的本质,都是在写放大、读放大、空间放大这三个角之间挪筹码。


参考来源

  1. What Is an LSM Tree? The Data Structure Behind Most Modern Databases — 本期解读的核心视频
  2. The Log-Structured Merge-Tree (LSM-Tree) — O'Neil et al., 1996 — LSM 树原始论文,Acta Informatica 33(4):351–385
  3. Compaction · facebook/rocksdb Wiki — 分级压实 vs 大小分级压实的写/读/空间放大权衡官方说明
  4. Optimizing LSM-tree Compaction in RocksDB: A Deep Dive into Write Amplification — 2026 年关于写放大调优的工程实战分析

本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入