想象你是一家快递仓库的主管。每天涌进数万件包裹,传统做法是:每来一件,立刻把它插到货架的正确位置,保持货架随时按编号有序。这意味着你得不停挪动整排已放好的包裹,位置越接近满,挪得越多——所有工人都耗在“腾位置”上,包裹堆积如山。这时候,有位老师傅提出新方案:前台放一张登记台,新包裹先堆上去、随手排个序;登记台满了,就把整批按编号整理好,封进大箱子,整箱摞进仓库。新箱子永远只放在最上层,工人再也不用挪动旧货。每隔一段时间,整理工把几个小箱子合并成一个大箱子,顺手清掉过期和重复的包裹。这就是 LSM 树(Log-Structured Merge Tree,日志结构合并树)的朴素智慧——用“先记账、后整理”的方式,把昂贵的随机写变成廉价的顺序写。
理解 LSM 树,你就掌握了 LevelDB、RocksDB 等现代 KV 存储引擎的命脉——这是设计写入密集型系统、优化 AI 数据管道底座的必备内功。
可视化演示
展示LSM树从MemTable内存写入、SSTable刷盘到Compaction层级合并的全流程,通过模式切换理解随机写转顺序写的核心机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
MemTable内存写入 WAL日志+内存表双写保障持久性 跳表结构支持有序写入 随机写转内存写实现高吞吐
写入瓶颈背景
想象一座藏书满到溢出的图书馆。管理员要求每来一本新书,必须立刻按编号插回书架的正确位置。可书架几乎满了,插一本新书意味着把后面一整排书全部往外挪一格,几百本书跟着移动,手臂酸、耗时长。数据库中的 B+ 树就像这座图书馆:叶子节点是书架,插入一条记录要定位叶子,节点满了还要分裂、向上调整父节点指针。在磁盘上,这种工作方式对应着随机写——磁头像搬运工一样反复寻道,每挪一次要花费数毫秒甚至十几毫秒。
这里的关键差距在于:机械硬盘的顺序写带宽可达每秒上百 MB,而随机写每秒只能完成几百次小 I/O。假设一次随机写要 10ms 寻道,1 万次随机写就要 100 秒;但如果把 1 万条小数据合并成一次顺序写,只需几十毫秒。两者差距可达一两个数量级。存储引擎的设计者们意识到:谁能把随机写凑成顺序写,谁就赢了。
1. 随机写耗时:一次随机写入磁盘的总开销 = 磁头寻道时间 + 旋转等待 + 实际传输数据的时间。
2. 远大于符号:\gg 表示左边远大于右边,强调随机写比顺序写慢得多。
3. 顺序写耗时:数据连续摆放,磁头几乎不用移动,耗时主要就是传输时间。
所以 LSM 树的发明者换了个思路:既然每次插书都要挪书架,那不如把新书全部堆在门口(内存),攒够一批后按编号排好,一次性推进仓库(磁盘)。这种“先记账、后整理”的模式,把数据库从“每步都严格排好”的教条主义,解放成了“分批入库、事后整理”的实用主义。
MemTable缓冲
新包裹到达快递仓库后,并不直接上货架,而是先放到前台登记台。工人们只需要把包裹往登记台上一放,随手按编号排个序——这个动作在内存里完成,快如闪电。这个登记台就是 LSM 树的 MemTable(内存表)。它通常用跳表(Skip List)实现:一种多层级链表,可以在内存中高效地保持 Key 有序,插入和查找的期望复杂度都是 \\(O(\log n)\\),比数组插入动辄移动整个区间要灵活得多。
你可能会问:数据只放内存,断电不就全丢了吗?仓库的解决办法是:每来一件包裹,先在签到簿上记一笔(WAL,Write-Ahead Log,预写日志),再放登记台。这本签到簿按顺序追加写在磁盘上,写着“编号 X 的包裹已签收”。哪怕内存断电蒸发,重启后照着签到簿就能把包裹重新唤回。WAL 是顺序追加写,速度极快,它用极小的代价换来了不丢数据的保证。
1. 操作代价:Cost 表示一次插入或查询需要的比较/移动次数。
2. 大 O 记号:O 用于描述算法耗时的增长趋势,忽略常数项。
3. 对数复杂度:\log n 表示耗时与数据量的对数成正比。n 是内存中数据条数。
有了登记台和签到簿,写入流程变成:先写 WAL(顺序写磁盘)→ 再写 MemTable(内存操作)→ 返回成功。两次都是廉价操作,所以单条写入的延迟极低,而且多条写入可以在内存里自然“攒批”——这为后面整批刷盘创造了条件。登记台看似只是临时堆放,实则是整个 LSM 快写体系的发动机。
SSTable落盘
登记台再快也有容量上限。当包裹堆到一定数量,仓库就会启动“封箱流程”:把登记台上的所有包裹按编号彻底排好序,封进一个标注好编号范围的包裹箱,然后让工人扛着这箱包裹,放到仓库的指定货区。这个动作在存储引擎里叫 flush(刷盘):先把 MemTable 冻结为不可变(Immutable)状态,不再接收新写入,然后由后台线程把它整体写成一个 SSTable 文件。
SSTable 全称 Sorted String Table(有序字符串表),它最关键的特性是内部按键有序、生成后不可修改。正因如此,刷盘过程可以一路顺序追加写磁盘,磁头从头走到尾,没有丝毫回头——这就是“新箱子永远只往最上层摞”的磁盘版本。新的写入请求会立刻使用新的 MemTable,旧表变成文件后不再变化,读写互不干扰。
1. S_k:第 k 层可容纳的数据总量,例如 L1 层的总容量。
2. S_0:第 0 层(最浅层)的基础容量,由单次刷盘的 MemTable 大小决定。
3. 容量放大因子 R:每向下移一层,容量放大 R 倍,通常取 10 左右。
4. k 次幂:S_0 乘以 R 的 k 次方——层数越深,容量指数级膨胀。
于是磁盘上的数据呈现出层次感:L0 层零星摆着刚刷下来的小箱子,L1、L2 层则是整理过的中箱和大箱。分层让“新数据永远在最上面、旧数据沉在下面”成为一种物理秩序,而这套秩序正是 Compaction 表演的舞台。
Compaction合并
仓库开久了,问题来了:相同编号的包裹可能散落在好几个箱子里,有的箱子贴着作废标签,有的内容重复。查找越来越费劲,空间越来越浪费。于是仓库安排了一位整理工,定期把几个小箱子倒出来,按编号重新归并为一个大箱子——这个过程就是 Compaction(压实/合并)。
Compaction 的触发时机通常看阈值:比如 L0 层的箱子数量超过 4 个,就触发一次合并;合并结果推入 L1,若 L1 又超过容量上限,就继续向 L2 合并。合并算法本质是多路归并:同时打开多个 SSTable,像流水线一样比较各个文件的当前 Key,挑最小的输出到新文件,遇到重复 Key 保留最新版本。这个“新版本覆盖旧版本”的规则,让更新操作也能延迟到合并时才落地。
1. 写放大:合并在后台重写数据文件,导致实际落盘量超过净新增量,这个倍数就是写放大。
2. 分子:包括新写入数据和所有合并重写的老数据——磁盘实际消耗的总 I/O。
3. 分母:用户真正提交的新数据总量。
整理工的付出是值得的:合并后箱子更少、更大,查找时可以跳过更多文件,存储空间也得到回收。但代价是额外的磁盘 I/O——写放大。LSM 树的设计本质是在写放大、读放大、空间放大三者之间拧螺丝:层数多一层,读更慢;合并勤一点,写放大更高;合并懒一点,空间占用上涨。优秀的存储引擎正是在这三把刀之间走钢丝。
读放大优化
在仓库里找一件包裹,直觉做法是从最新的箱子往前翻——因为新版本总比旧版本新。但 LSM 的层数一多,“从最新到最旧倒着翻”就变成了噩梦:最坏情况下要把每一层的每一个箱子的封面都拆开看一眼,这就是读放大(Read Amplification)。读放大的规模 = 层数 × 每层平均箱子数。相比 B+ 树从根走到叶子的 3~4 次 I/O,LSM 的读放大可以轻松到几十倍。
仓库的解决办法是:给每个箱子贴一张快查贴纸(布隆过滤器,Bloom Filter)。贴纸的原理很巧妙——数据写入箱子时,用 k 个哈希函数把 Key 映射到位数组的 k 个位置上,全部涂黑;查询时同样算 k 个位置,只要有一个没被涂黑,就说明“这个 Key 肯定不在箱子里”。它允许偶尔误判(说“在”其实不在),但绝不漏判(说“不在”就一定不在)。
1. 误判率 P:布隆过滤器“误报存在”的概率,记作 P。
2. 参数 k、n、m:k 个哈希函数;n 个已插入的 Key;m 个 bit 的位数组。
3. \frac{kn}{m}:每个 bit 被命中的平均次数,决定了某一位为 0 的概率。
4. k 次方:k 个哈希位置全部冲突的概率——这正是误判的来源。
贴上快查贴纸后,查询一个 Key:先扫 L0 最上面几个箱子的贴纸,说“不在”就跳过;说“在”才真正打开箱子找。绝大多数不存在的 Key 会被一张贴纸拦住,只需一次内存位运算,完全不用碰磁盘。配合块缓存(Block Cache)把热数据留在内存里,LSM 的读性能虽然还是不如 B+ 树,但已经足够应对绝大多数业务场景——毕竟它换来了惊人的写吞吐。
删除与更新
快递单填错了怎么办?你没法拆开已经封好的箱子把包裹拿出来——箱子是封死的。快递仓库的做法是:在箱子上贴一张醒目的作废标签,注明“编号 X 作废”,照常把箱子放进仓库。客户来取件时,看到作废标签,就回复“这个包裹没了”。在 LSM 树里,这张作废标签叫墓碑(Tombstone):删除操作并不去磁盘删数据,而是写入一条特殊标记 key -> tombstone。
这种设计看似笨拙,实则是大智慧。更新也一样:新版本直接写入 MemTable,旧版本留在地下层躺着。真正的物理删除发生在Compaction 整理时——当合并程序遇到墓碑标记,就知道该 Key 的旧版本可以全部丢弃;当遇到同一个 Key 的多个版本,只保留最新一个。于是,被“作废”的数据像落叶一样在一次次合并中被扫出仓库,空间得以回收。
1. Tombstone(墓碑):删除时写入的特殊占位标记,表示该 Key 已逻辑删除。
2. Key 的旧版本:历史上写入的旧数据,一直静静躺在深层的 SSTable 里。
3. 合并箭头:只有当 Compaction 扫描到这两者时,清理动作才会发生。
4. 空集符号:表示该 Key 的全部痕迹被彻底清除,物理空间被回收。
有趣的是,墓碑本身也有副作用:如果删除一个不存在的 Key,墓碑会一直存活到合并被清除,占用空间。好在工程上可以给墓碑加时间戳,到期自动丢弃。这套“延迟删除”机制让 LSM 在写密集场景下如鱼得水——删除不再是一次昂贵的磁盘操作,而是一条便宜的追加记录,完美绕开了传统 B+ 树“删除要改写文件”的痛点。
开源应用
这套仓库管理术一经问世,立刻被各家“大仓库”采用。Google 的 LevelDB 是教科书级实现,用 C++ 写就;Meta 在它基础上强化了并发与性能,推出 RocksDB,成为业界最流行的嵌入式 KV 存储引擎之一。为什么 AI 和分布式系统如此偏爱它?因为它们的写请求太疯狂了——点击日志、监控指标、特征数据,每秒几十万次写入,传统 B+ 树根本扛不住。
分布式数据库是 LSM 的最大受益者。TiKV(TiDB 的存储层)用 RocksDB 作为单节点引擎,上层做分布式事务,底层靠 LSM 的高写入吞吐支撑 OLTP 场景;HBase 的 HFile 同样源自 LSM 思想;Cassandra 更是将 LSM 与分布式哈希天然结合。可以说,现代“写多读少”的互联网基础设施,绕不开 LSM 的影子。
1. 总代价:存储引擎在运行中的综合开销,是调优要最小化的目标函数。
2. 三个放大项:写放大、读放大、空间放大,此消彼长。
3. 权重系数:\alpha、\beta、\gamma 表示业务对三者的敏感度——写多场景让 \alpha 大,读多场景让 \beta 大。
今天的 LSM 早已不是单纯的数据结构,而是一套成熟的工程哲学。当你面临“每秒百万级写入、数据不丢、还能按序扫出来”的需求时,选择 RocksDB 或 TiKV 这类 LSM 系存储,就是在聘请那位经验丰富的仓库整理工——它或许不算完美的读书人,但绝对是顶级的搬货工人。
AI 应用场景
实时推荐特征平台
阶段:特征工程与特征存取
解决的问题:推荐系统需要实时记录用户行为(点击、点赞、浏览),作为模型推理的特征输入。传统 B+ 树存储每秒仅能承受数千次随机写,面对百万级日活用户的实时行为流会瞬间崩溃;LSM 树通过内存缓冲 + 顺序刷盘,把随机写转为顺序写,轻松支撑每秒数十万次的特征写入。
具体案例:某电商平台的推荐特征平台使用 RocksDB 作为本地状态存储。用户点击商品后,行为特征以 <user_id, item_id, timestamp> 的 Key 写入 LSM 引擎,配合布隆过滤器加速特征读取,特征更新的 P99 延迟低于 5ms。每次推荐请求从同一引擎批量读取用户最近 100 条行为特征,组成模型输入。
大模型训练日志与实验追踪
阶段:模型训练与实验管理
解决的问题:AI 训练平台每秒要记录大量 metrics(loss、accuracy、学习率)和参数快照,写入量大且伴随高频更新。LSM 的延迟删除与合并机制让旧版本指标自动归档,训练中断时还能通过 WAL 快速恢复实验状态。
具体案例:某 MLflow 部署使用基于 LSM 的存储后端保存实验记录。一次千卡分布式训练每秒产生 5 万条指标写入,LSM 引擎承接了全部写入压力;训练结束后,按 run_id + step 范围扫描出整条 loss 曲线用于可视化,得益于 SSTable 的有序性,范围查询高效完成。
Python 演示
import hashlibimport os class MemTable: def __init__(self, limit=8): self.data = {} self.limit = limit def put(self, key, value): self.data[key] = value def is_full(self): return len(self.data) >= self.limit def freeze(self): items = sorted(self.data.items()) self.data.clear() return items class BloomFilter: def __init__(self, size=256, k=3): self.bits = [False] * size self.size = size self.k = k def _hash(self, key, i): raw = hashlib.md5("{}-{}".format(key, i).encode()).hexdigest() return int(raw, 16) % self.size def add(self, key): for i in range(self.k): self.bits[self._hash(key, i)] = True def may_contain(self, key): for i in range(self.k): if not self.bits[self._hash(key, i)]: return False return True class SSTable: def __init__(self, path, items): self.path = path self.bloom = BloomFilter() self.items = {} for k, v in items: self.items[k] = v self.bloom.add(k) with open(path, "w", encoding="utf-8") as f: for k, v in items: line = "{}\t{}\n".format(k, "TOMBSTONE" if v is None else v) f.write(line) def get(self, key): if not self.bloom.may_contain(key): return None, False if key in self.items: return self.items[key], True return None, True class LSM: def __init__(self, root="lsm_demo", limit=8, levels=3): self.root = root self.mem = MemTable(limit) self.levels = [[] for _ in range(levels)] self.seq = 0 os.makedirs(root, exist_ok=True) def put(self, key, value): self.mem.put(key, value) if self.mem.is_full(): self._flush() def delete(self, key): self.mem.put(key, None) def _flush(self): path = os.path.join(self.root, "sst_{}.sst".format(self.seq)) self.seq += 1 table = SSTable(path, self.mem.freeze()) self.levels[0].append(table) if len(self.levels[0]) >= 3: self._compact(0) def _compact(self, level): merged = {} for table in self.levels[level]: for k, v in table.items.items(): merged[k] = v alive = [(k, v) for k, v in sorted(merged.items()) if v is not None] target = level + 1 if level + 1 < len(self.levels) else level for table in self.levels[level]: os.remove(table.path) self.levels[level] = [] path = os.path.join(self.root, "sst_{}.sst".format(self.seq)) self.seq += 1 new_table = SSTable(path, alive) self.levels[target].append(new_table) if len(self.levels[target]) >= 3 and target < len(self.levels) - 1: self._compact(target) def get(self, key): if key in self.mem.data: return self.mem.data[key] for tables in self.levels: for table in tables: val, found = table.get(key) if found: return val return None if __name__ == "__main__": db = LSM() for i in range(30): db.put("user:{}".format(i), "value-{}".format(i)) print("查询 user:5 ->", db.get("user:5")) db.delete("user:5") print("删除后查询 user:5 ->", db.get("user:5")) print("查询 user:6 ->", db.get("user:6")) print("SSTable 文件数 ->", len(os.listdir("lsm_demo")))
items = sorted(self.data.items())冻结 MemTable 时先按键排序,保证刷盘出的 SSTable 内部有序。self.levels[0].append(table)新刷出的 SSTable 直接追加到 L0 层,磁盘写入天然顺序化。if len(self.levels[0]) >= 3:达到阈值立即触发 Compaction,控制 L0 文件数量,抑制读放大。merged[k] = v合并时用字典按键去重,后写入的版本自动覆盖旧版本,实现“最新版本胜出”。alive = [(k, v) for k, v in sorted(merged.items()) if v is not None]过滤掉墓碑(None 值),物理删除真正发生。if not self.bloom.may_contain(key):布隆过滤器说“没有”就直接跳过整个文件,省去一次磁盘读。db = LSM()一个迷你数据库实例,内部自包含内存表、分层文件和合并逻辑。
常见误区
误区我刚学了 LSM,感觉它又快又能抗大流量。既然写也快,读应该也不差,是不是可以把所有数据库都换成 LSM 实现?
正解LSM 只保证“写”和“范围扫描”有优势,随机点查反而慢。因为读的路径很长:先查内存表,再从 L0 倒查到底层,还有一层层布隆过滤器的判断。B+ 树的随机点查约 3~4 次 I/O 封顶,而 LSM 最坏情况要多查几个数量级的文件。它用读放大换写放大,适合写多读少,而非万能解药。就像快递仓库进件极快,但偶尔找一件压箱底的包裹要翻很久。
为何常见:初学者被“高吞吐写入”的宣传吸引,忽略了 LSM 读放大的天然代价,误以为它是全面超越的银弹。
误区Compaction 要重写大量数据文件,肯定会导致数据库卡顿很久,线上根本不敢开。
正解现代 LSM 引擎的 Compaction 是分层次、限流量、异步进行的。RocksDB 会把合并任务交给后台线程池,通过 rate_limiter 平滑合并速度;L0 到 L1 的合并只涉及少部分文件,单次停顿通常控制在毫秒级。只有在极端配置(如合并速度完全跟不上写入速度)时才可能出现显著抖动。整理工可以趁夜间加班,而不是让仓库白天空转。
为何常见:看到“大文件合并”就想到全量重写,忽略了工程实现中分片、限流、后台调度等大量优化手段。
误区如果布隆过滤器的位数组太小,或者哈希冲突厉害,真正存在的 Key 会不会被它说成“不存在”,导致读取丢失数据?
正解方向反了。布隆过滤器只允许假阳性(把不存在的说成存在),绝对不允许假阴性(把存在的说成不存在)。因为数据写入时会把对应位全部置 1,判断存在时这些位必然全是 1;只有“某些位没被全部置 1”才会判不存在,而这种情况只会发生在该 Key 从未插入时。它就像一个严谨的门卫:说“人不在”就一定不在,说“人可能在”才会进房间确认。
为何常见:容易把“宁可错杀一千”和“宁纵勿枉”搞反,对哈希冲突的方向缺乏直观理解。
误区我在 LSM 数据库里执行 `DELETE`,磁盘空间应该马上就释放了吧?删除很重才对吧?
正解LSM 的删除只是追加写入一条墓碑标记,旧数据仍然躺在 SSTable 里,物理空间并未立刻释放。要等后续某次 Compaction 扫描到这个墓碑时,旧数据才被真正清除。这意味着删除操作本身极快(也是一次顺序写),但磁盘空间回收有延迟。如果删了大量数据后立刻看磁盘占用,可能惊讶地发现空间没变——这不是没删掉,而是整理工还没上班。
为何常见:传统数据库的删除是原地标记或立刻改写索引页,人们自然会把“删除=立即释放”带到 LSM 的语境里。
面试题
Q1. LSM 树实现高写入吞吐最根本的手段是什么?
- A.使用更快的固态硬盘
- B.将随机写转化为顺序写← 正确答案
- C.减少数据落盘的次数
- D.使用更小的数据文件
答案:B。LSM 的核心哲学是把昂贵的随机写累积在内存表中,再以顺序追加的方式刷盘成 SSTable。选项 A 是硬件手段,C 和 D 是局部优化,只有 B 点中了 LSM 的灵魂——就像快递仓库把“插货架”改成“整摞入库”。
Q2. MemTable 在内存中通常用什么数据结构保持 Key 有序?
- A.哈希表
- B.跳表← 正确答案
- C.数组
- D.B+ 树
答案:B。LevelDB 的 MemTable 使用跳表(Skip List),支持高效的插入和有序遍历,期望复杂度为 O(log n)。哈希表无法保持有序,数组插入要移动元素,B+ 树主要用于磁盘索引而非内存临时缓冲。
Q3. WAL 预写日志的主要作用是什么?
- A.减少磁盘空间占用
- B.加速数据查询
- C.防止内存数据丢失后的崩溃恢复← 正确答案
- D.替代布隆过滤器
答案:C。WAL 以顺序追加方式记录每次写入,数据还在内存 MemTable 时即已落盘。若进程崩溃,内存表内容丢失,重启后可重放 WAL 恢复数据。它本质是快递仓库的签到簿,与读加速无关。
Q4. 关于 Compaction,下列说法正确的有哪些?
- A.会带来写放大← 正确答案
- B.可以清理被删除的数据← 正确答案
- C.一定会导致服务长时间不可用
- D.可能把 L0 的多个文件合并到 L1← 正确答案
答案:ABD。Compaction 重写文件产生写放大(A 对);合并时遇到墓碑会物理清理删除数据(B 对);L0 文件超阈值会合并下沉到 L1(D 对)。现代引擎通过后台限流使停顿通常在毫秒级,C 的“长时间不可用”过于绝对。
Q5. 布隆过滤器判定“某个 Key 不存在”时,真实情况可能是?
- A.Key 一定不存在← 正确答案
- B.Key 一定存在
- C.可能不存在也可能存在
- D.无法判断
答案:A。布隆过滤器没有假阴性,只要它说“不存在”,就说明至少有一个哈希位未被置 1,而该 Key 若存在则这些位必然全为 1,所以 Key 一定不存在。它唯一可能出错的方向是“存在”被误判为可能存在。
Q6. LSM 树的删除操作实际做的是什么?
- A.立即从磁盘物理删除数据块
- B.写入一条墓碑标记← 正确答案
- C.重建整个 SSTable
- D.直接修改原文件
答案:B。SSTable 不可修改,删除不能原地执行。LSM 采用延迟删除:写入一条特殊的墓碑标记(tombstone),逻辑上屏蔽旧数据,物理清理留给后续 Compaction。这和快递箱贴作废标签如出一辙。
Q7. L0 层与其他层的最大区别是什么?
- A.L0 存放最旧的数据
- B.L0 的 SSTable 之间 Key 范围可能有重叠← 正确答案
- C.L0 只能存放一个文件
- D.L0 不需要布隆过滤器
答案:B。L0 层由内存表直接刷盘而来,各文件之间没有进行过全局排序,Key 范围相互重叠,因此读取时可能需要查多个文件。从 L1 开始,每层内部文件互不重叠,有序性逐步增强。
Q8. 下列哪些数据库或系统使用或借鉴了 LSM 树?
- A.RocksDB← 正确答案
- B.LevelDB← 正确答案
- C.TiKV← 正确答案
- D.SQLite 默认的 B+ 树索引引擎
答案:ABC。RocksDB 是 Meta 基于 LevelDB 改进的 LSM 引擎,LevelDB 是 Google 的 LSM 经典实现,TiKV 用 RocksDB 作为底层单机存储,都是 LSM 家族成员。SQLite 默认使用 B+ 树,不属于 LSM 结构。
Q9. 若 LSM 分层模型每层容量放大因子为 R,共 k+1 层(L0 到 Lk),则第 k 层容量为?
- A.S0 + k × R
- B.S0 × R^k← 正确答案
- C.S0 ÷ R^k
- D.S0 + R^k
答案:B。分层容量按几何级数增长:第 0 层为 S0,第 1 层约 S0 × R,第 2 层约 S0 × R²,依此类推。指数增长保证了越深的地方能容纳越多数据,从而让大部分数据向更深层“沉淀”,减少顶层文件数量。
Q10. 以下哪些手段可以有效降低 LSM 树的读放大?
- A.使用布隆过滤器← 正确答案
- B.使用块缓存(Block Cache)← 正确答案
- C.增加 SSTable 文件数量
- D.增加 LSM 层级数
答案:AB。布隆过滤器在内存中直接跳过不含目标 Key 的文件,块缓存把热数据留在内存避免重复读盘,两者都直接削减读 I/O。增加文件数量和层级数反而会放大读取路径的长度,属于反向操作。