想象有一家图书馆,管理员小绿每天要面对潮水般涌来的新书。她从不急着把书一本本插进大书架,因为那样又慢又乱。她的做法是:先在随身的笔记本上记一笔账,然后把新书堆到桌面上按书名排好;等桌面堆满了,就把整桌书装订成一本厚厚的“藏书册”——册子里的词条严格按字母顺序排列,而且装订好后就再也不许任何人涂改。读者想找书时,先查册子封底的“地图”,再查章末的“索引”,最后才翻到具体那一页。每隔一段时间,小绿还会把好几本旧册子重新合订成一本更大的新册子,腾出书架空间。 这套“先记账、再排队、批量成册、定期合订”的图书管理法,就是 LSM 树存储引擎的核心思路。而那本反复被检索、合并、压缩的“藏书册”,正是本文的主角——SSTable(Sorted String Table,有序字符串表)。
读懂 SSTable 就等于拿到了 LevelDB / RocksDB 等存储引擎的“内部地图”。无论是调优写入性能、排查读放大问题,还是面试时讲清 LSM 树原理,这都是绕不开的硬核内功。
目录
可视化演示
展示SSTable文件结构、数据块二分查找和布隆过滤器查询三个核心机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
SSTable文件结构 数据块按Key有序存储,支持二分查找 索引块记录每块最大Key和偏移量 File = Data[] + Index + Bloom + Footer 不可变磁盘文件,一次写入多次读取
文件结构总览:藏书册的装帧
小绿今天要给新到的一批书装订成册。一部好用的藏书册是有讲究的:正文必须连续,章节要分明,封底务必贴一张“整册地图”。任何读者拿到册子,第一件事不是翻第一页,而是翻封底地图——上面写着“索引在第几页”“元数据存放在哪一块”。没有这张地图,再整齐的册子也会让人抓瞎。
SSTable 文件正如这部藏书册。它由一串数据块(Data Block)和索引块(Index Block)组成,最后固定一块很小的 Footer。LevelDB 和 RocksDB 把 Meta Index Block(元数据索引块,负责指向布隆过滤器等)与 Index Block 的偏移量和大小都记录在 Footer 里。打开文件时,引擎先读取文件尾部的 Footer,再顺着它的指引去定位索引,整个过程只需要一次跳转,开销极小。
所有 key-value 条目在文件内按 key 的字典序排列,这是 SSTable 一切高效操作的基础。文件布局采用“头轻脚重”:数据块在前,索引块在后,元数据索引夹在中间,Footer 收尾。整个文件一旦生成便不可修改,新的写入只会追加出新的 SSTable,不会被篡改。这种不可变性让引擎可以放心缓存、并发读取,也让崩溃恢复变得简单。
小绿把地图印在封底,读者就不用翻遍全书找目录。SSTable 把 Footer 放在文件末尾,正是同样的智慧:用固定的入口、最小的成本,撬动整册书的全局导航。
- 数据块区(DataBlocks)存放真正的 key-value 数据,按 key 有序地切成一段段连续字节,每一段就是一个数据块。
- 索引块(IndexBlock)为每个数据块记录“最后一个 key、文件偏移、块大小”,作为从数据块导航到具体位置的跳板。
- 元数据索引(Metaindex)记录布隆过滤器等元数据块的位置,让引擎能快速找到“馆藏查询机”。
- Footer(封底地图)文件末尾固定大小的入口,记录 IndexBlock 与 Metaindex 的偏移和大小,是读取整册书的起点。
数据块格式:节省纸张的抄写法
藏书册的每一章,词条一个挨一个排列。为了节省纸张,小绿抄写时玩了个巧:如果新词条和上一个词条有相同的开头,她就只写“不同的尾巴”。比如已经写过 “apple“,下一个词是 ”apples“,她只写一个 ”s“;但每隔 16 个词条,她会完整重写一遍,作为一个”定位锚点”,防止读者一路猜前缀猜到晕头转向。
SSTable 的数据块(Data Block)正是这样组织的。数据块默认几 KB 到几十 KB 不等,内部由一条条 entry 组成。每条 entry 的 key 长度和 value 长度都用变长编码(Varint)存储——小数字只占一个字节,大数字才多占几个字节。LevelDB 与 RocksDB 还支持前缀压缩:对相邻 key 只记录公共前缀长度 shared_len、新增后缀长度 unshared_len,以及 value 本身。
每次从前缀压缩恢复时,都需要知道“上一条 key 长什么样”,所以引擎设置了 restart(重启)点。在每个 restart 点,shared_len 强制为 0,完整存储整条 key。块尾部还保存了一个 restart 偏移数组,二分查找时可以先用它跳转到某个 restart 点,再逐条向后比较,避免从头扫描整块。
这种“只写差异”的思路,就像小绿在藏书册里用省略号代替重复的单词前缀。读者只要找到最近的 restart 点,就能顺着差异链条还原出完整词条,既省纸又不迷路。
- shared_len(共享前缀长度)当前 key 与上一条 key 相同的公共前缀字节数,是前缀压缩的核心字段。
- unshared_len(非共享后缀长度)当前 key 除去公共前缀后,新增部分的字节长度。
- value_len(值的长度)value 字段占用的字节数,供读取时截取 value。
- key_suffix + value(后缀与值)真正落在磁盘上的差异化数据,是 entry 的实际载荷。
索引块机制:章末的检索页
藏书册如果只有正文,读者要找 “orange“ 就得从第一页翻起。小绿于是在每一章末尾附了一页”章节索引”:每个小节记录三个信息——这个小节的最后一个词条、小节在整个册子里的页码、小节占了几页纸。读者先用二分法在索引页上定位候选小节,再翻到那一节仔细查找。
SSTable 的索引块(Index Block)承担同样的角色。它的一条 Index Entry 对应一个数据块,记录该数据块的最后一个 key、数据块在文件中的偏移量 offset、数据块大小 size。查询某个 key 时,引擎先在索引块上做二分查找:找到第一个 last_key 不小于目标 key 的索引项,就锁定了唯一的候选数据块。
进入候选数据块后,块内仍然用二分或顺序扫描来定位具体 entry——因为块内还保留着 restart 点数组,可以借助它快速跳转。整个查找过程形成“两级二分”:先块级二分,再块内二分。相比在整份文件中线性扫描,这种导航方式把查找次数从“所有记录数”压缩到“两个对数的和”,这正是 SSTable 能在海量数据中保持低延迟的秘密。
小绿把章节标签贴在每一格的边上,读者就能跳过无关格子,直达目标。索引块就是 SSTable 贴在每一个数据块上的“侧边标签”,让随机读不再依赖从头扫描。
- last_key(块的边界 key)数据块内最后一个 key,用于二分比较时判断目标是否落在这个块的范围内。
- block_offset(块偏移)数据块在 SSTable 文件中的绝对起始位置,读取时直接 seek 到此偏移。
- block_size(块大小)数据块占用的字节数,配合偏移量精确切出整块数据。
布隆过滤器:门口的馆藏查询机
图书馆门口有一台“馆藏查询机”:它内部是一排小灯泡(位数组)。新书上架时,小绿用三把不同的“尺子”(哈希函数)量出三个位置,把对应灯泡点亮。读者问“有《MySQL 45 讲》吗?”查询机用同样三把尺子量一遍,只要发现任何一个灯泡没亮,就斩钉截铁地回答:“没有。”但如果三个灯泡都亮,它只能说:“可能有。”
这台查询机就是布隆过滤器(Bloom Filter),它是 SSTable 家族里最巧妙的“否定大师”。布隆过滤器用一个 m 位的位数组和 k 个哈希函数来标记集合。插入 key 时,把 k 个哈希位置全部置为 1;查询时,检查这 k 个位置——只要有一位是 0,就说明该 key 从未被插入过,可以百分百断定“不存在”;如果全是 1,则只能推断“可能存在”,因为其他 key 的哈希可能恰好把这些位置也点亮了。
这种“宁可误报、绝不漏报”的特性,让引擎在读取时能提前排除大量不相关的 SSTable 文件。每本藏书册门口都装一台查询机,读者还没进门就知道“这本册子里肯定没有我要的书”,连翻都不用翻。布隆过滤器的误判率是可以计算的,而且能通过调整位数组长度和哈希函数数量来控制。
小绿的查询机从不撒谎说“没有”——只要说没有就肯定没有。正是这种“拒绝空翻”的能力,让 LSM 引擎在多层 SSTable 之间查找时,能迅速跳过大量绝对不可能命中的文件,省下宝贵的磁盘 IO。
- 参数定义n 是已插入的元素个数,m 是位数组总位数,k 是哈希函数个数。
- 某位仍为 0 的概率插入 n 个 key、每个 key 随机置 k 位后,某一位始终未被置 1 的概率约为 $e^{-kn/m}$。
- 误判率查询一个不存在 key 时,恰好它对应的 k 位全为 1 的概率,即 $(1 - e^{-kn/m})^k$。
- 最优哈希函数数对误判率求最小值,得到 $k_{\mathrm{opt}} = (m/n)\ln 2$。
压缩合并:定期整理书架
日子一长,小绿的藏书册越堆越多:同一个词条可能同时出现在好几本册子里,书架都快塞不下了。于是她每周做一次“合订”:把若干本旧册子按字母顺序重新编排,合并成一本更大的新册子,把被覆盖的旧词条扔掉,腾出大片书架。虽然合订时累得满头大汗,但整理完的书架让人眼前一亮。
LSM 树的压缩合并(Compaction)正是这场“合订”。SSTable 在磁盘上是有层级的:第 0 层(L0)是内存表刷盘直接生成的,文件之间 key 范围可能重叠;从第 1 层(L1)开始,每层内部经过合并后 key 范围互不重叠,且容量按固定倍数增长。合并过程类似归并排序:多路读取旧文件,按 key 从小到大归并,写入新文件,同时重建数据块、索引块和布隆过滤器。
合并带来的最大代价是“写放大”:明明应用只写了 1 MB 数据,磁盘却可能实际写入了 10 MB、20 MB,因为旧数据要被反复读取、合并、重写。这个比率叫做写放大(Write Amplification),是评估 LSM 存储引擎性能的关键指标。层级越多,查阅越精确,但合订时搬动的书也越多;层数越少,合订越省力,但单层内的文件越多、读放大越高。小绿必须在“勤整理”和“少搬书”之间找平衡。
小绿每周末累得腰酸背痛,但换来的是工作日的轻松查找。Compaction 也一样:它把凌乱的短期文件整理成规整的长期结构,用后台的写放大换取前台的点查效率。
- 分子(实际写入磁盘字节数)包含应用写入的数据,以及 Compaction 过程中读取、归并、重写旧文件产生的全部磁盘写入。
- 分母(应用层写入字节数)用户通过 put 接口真正想写的数据量,是衡量成本的基准。
- 比值含义WriteAmp 越接近 1 越好;大于 10 时说明大部分磁盘带宽被“整理书架”占用了。
读取流程:逐层找书的完整路线
一位读者来借《orange 种植手册》。小绿的找书流程像剥洋葱:先看桌面上正在整理的书堆(MemTable)有没有;没有,再看旁边待装订的备用桌(Immutable MemTable);还没有,才走向书架区,从最新的一层(L0)往里找。每层书的门口都有查询机,说“没有”就整层跳过,绝不多翻一本。
SSTable 的读取流程正是如此。一次 Get(k) 会依次检查:内存 MemTable、不可变内存表 Immutable MemTable、磁盘上 L0 到 Ln 各层 SSTable。L0 是刷盘直接生成的,文件之间 key 可能重叠,所以该层要检查多个候选文件;L1 及以下每层内部有序、范围不重叠,最多只需查一个候选文件。每查一个 SSTable,先用布隆过滤器判断“是否存在”——若判定不存在,整个文件直接跳过;若可能存在,再用索引块二分定位数据块,块内扫描找到记录。
由于数据的新版本总是比旧版本更早被写入,越新的层优先级越高。只要在某层找到了 key,就可以直接返回,不需要继续向更老的层查找。但如果一直找不到,就要把所有层都滤一遍,这就是“读放大”的来源:一次点查可能触发多次磁盘读取。读放大的公式很直观——它是所有层级需要检查的文件数总和。
小绿找书时“先近后远、层层过滤”,既保证能找到最新版本,又尽量少翻旧册子。读放大就是她每次找书需要翻动的册数——布隆过滤器、索引块、层级设计,全是为了让这个数字尽可能小。
- L0 贡献(n₀)第 0 层文件之间 key 重叠,最坏情况下要找遍该层所有文件,所以 n₀ 可以很大。
- 高层的贡献(nᵢ)从 L1 开始,每层 key 范围不重叠,所以每层最多只需检查一个候选文件,nᵢ = 1。
- 总和语义ReadAmp 越大,一次点查读取的文件块越多,P99 延迟越高,这是 LSM 读性能的瓶颈所在。
写路径与元数据:新书上架的流水线
每天都有新书涌入图书馆。小绿从不把书直接往大书架上插——那既慢又要挪动一堆旧书。她的做法是:先在随身小本本上记一笔“某年某月某日,收到《xxx》”,再把书放到桌面按字母排好。桌面满了就换一张备用桌继续放,而已经排满的桌面内容,会由管理员在后台统一装订成一本新的藏书册,放到 L0 层书架上。整个过程看似多了一步“记账”,却让每一次写入都变成快速追加,而不是昂贵的随机插入。
SSTable 的写入路径完美复刻了这套流水线。写入一条 (k, v) 时,引擎先把操作追加到 WAL(预写日志,相当于小绿的随身笔记本),保证即使机器突然断电也不会丢数据;然后更新内存中的 MemTable(跳表或有序结构)。当 MemTable 达到阈值,它被标记为只读的 Immutable MemTable,同时新开一张空的 MemTable;后台线程再把 Immutable 刷盘,生成一个新的 SSTable 追加到 L0。
SSTable 一旦落盘就永远不可变,因此不会有“修改已有文件”的操作。新数据覆盖旧数据,只是生成新文件、在更高层留下新版本,旧版本留待 Compaction 清理。元数据方面,SSTable 的 Footer 固定位于文件末尾,记录 Meta Index 和 Index Block 的位置;Meta Index 则指向布隆过滤器等元数据块。这种“尾部锚点”的设计,让小绿闭着眼睛都能从封底摸到整册书的导航信息。
小绿用“记账+排队+批量装订”的流水线,把原本又慢又乱的插书操作变成了一条高效的传送带。SSTable 的写路径也是如此:所有随机的小写请求先被吸收、排队,再批量落盘成有序文件,这就是 LSM 引擎写入快于 B+ 树引擎的根本原因。
- WAL(预写日志)先把 (k,v) 顺序追加到磁盘日志,崩溃后可以重放恢复,这是数据不丢失的第一道保险。
- MemTable(内存表)将 (k,v) 插入内存中的有序结构,写入只发生在内存,微秒级完成。
- Immutable(只读内存表)MemTable 写满后转为只读,不再接受新写入,等待后台刷盘。
- SSTable_L0(刷盘落层)后台把 Immutable 内容排序、编码、构建索引和布隆过滤器,形成一个新 SSTable 追加到 L0。
AI 应用场景
分布式数据库 TiKV 的点查优化
阶段:存储引擎选型与性能调优
解决的问题:海量订单状态更新如果用传统 B+ 树,每一笔随机写都会落到磁盘,延迟和写放大难以承受。LSM 引擎通过 WAL + MemTable 吸收随机写,再批量刷成 SSTable;点查时用布隆过滤器跳过绝大多数不存在的文件。
具体案例:TiKV 使用 RocksDB 作为底层 KV 引擎,它正是基于 SSTable 格式构建的。当你在 TiDB 上执行一笔 UPDATE,数据先写 WAL、再进 MemTable、最终落到 SSTable;执行 SELECT 时,读取流程会逐层过滤,先用布隆过滤器排除不可能的文件,再用索引块精确定位。这套机制让 TiKV 在混合读写压力下依然保持稳定延迟。
物联网时序数据的写入管道
阶段:数据接入层架构设计
解决的问题:物联网传感器每秒产生海量带时间戳的写入请求,时序数据库必须吸收突发写入,同时又不能把点查做得太慢。LSM 树天然适配“写入密集、近期数据读多”的时序场景。
具体案例:InfluxDB 的 TSM 存储引擎采用类 LSM 的架构:数据先缓存在内存缓冲区(类似 MemTable),攒够一批再刷成类似 SSTable 的有序文件,后台定期合并成更大的文件。布隆过滤器(这里是 Sketch)同样用于跳过不存在的分片文件。当你查询某个传感器最近一小时的数据时,引擎只需读取时间戳索引定位到的少数几个文件。
Python 演示
import hashlibimport struct class BloomFilter: def __init__(self, n, bits_per_key=10): self.bits = n * bits_per_key self.bitmap = bytearray((self.bits + 7) // 8) self.k = max(1, int(bits_per_key * 0.7)) def _hashes(self, key): h1 = int(hashlib.md5(key.encode()).hexdigest()[:8], 16) h2 = int(hashlib.sha1(key.encode()).hexdigest()[:8], 16) return [(h1 + i * h2) % self.bits for i in range(self.k)] def add(self, key): for pos in self._hashes(key): self.bitmap[pos // 8] |= 1 << (pos % 8) def contain(self, key): return all(self.bitmap[pos // 8] & (1 << (pos % 8)) for pos in self._hashes(key)) def pack(k, v): kb, vb = k.encode(), v.encode() return struct.pack('I', len(kb)) + kb + struct.pack('I', len(vb)) + vb class MiniSSTable: BLOCK = 3 def __init__(self, pairs): self.data = sorted(pairs) self.blocks = [] self.index = [] self.bloom = BloomFilter(len(self.data)) self.build() def build(self): offset = 0 for i in range(0, len(self.data), self.BLOCK): chunk = self.data[i:i + self.BLOCK] raw = b''.join(pack(k, v) for k, v in chunk) self.blocks.append(raw) self.index.append((chunk[-1][0], offset, len(raw))) offset += len(raw) for k, _ in chunk: self.bloom.add(k) self.footer = struct.pack('QQ', 0, len(self.index)) def get(self, key): if not self.bloom.contain(key): return None lo, hi = 0, len(self.index) while lo < hi: mid = (lo + hi) // 2 if self.index[mid][0] < key: lo = mid + 1 else: hi = mid if lo == len(self.index): return None pos = 0 raw = self.blocks[lo] while pos < len(raw): klen = struct.unpack('I', raw[pos:pos + 4])[0] pos += 4 k = raw[pos:pos + klen].decode() pos += klen vlen = struct.unpack('I', raw[pos:pos + 4])[0] pos += 4 v = raw[pos:pos + vlen].decode() pos += vlen if k == key: return v return None if __name__ == '__main__': sst = MiniSSTable([('apple', '1'), ('banana', '2'), ('cherry', '3'), ('durian', '4'), ('mango', '5'), ('orange', '6')]) for key in ('apple', 'mango', 'pear'): print(key, '->', sst.get(key))
self.index.append((chunk[-1][0], offset, len(raw)))每构建完一个数据块,记录它的最后一个 key、文件偏移和块大小,这正是索引块的核心条目。if not self.bloom.contain(key): return None查 key 前先问布隆过滤器,说“不存在”直接返回,省去一切磁盘读取。self.bitmap[pos // 8] |= 1 << (pos % 8)把多个哈希函数算出的位全部置 1,这是布隆过滤器写入的关键操作。raw = b''.join(pack(k, v) for k, v in chunk)把一个块内的所有 entry 按“长度+key+长度+value”格式打包成字节串,模拟数据块的物理编码。self.data = sorted(pairs)构建前先对全部 key-value 排序,保证 SSTable 有序不变性的根基。while pos < len(raw):从数据块起始处逐条解码 entry,比较 key 是否命中,这是块内扫描的微型实现。for key in ('apple', 'mango', 'pear'):验证读路径:apple 与 mango 能查到,pear 不存在但可能通过布隆过滤器,最终在数据块中确认无此记录。
常见误区
误区“SSTable 是 LSM 树在内存里存 key-value 的哈希表吧?我想当然地觉得它跟 Redis 的字典差不多。”
正解SSTable 的 S 是 Sorted String Table,它是一种只读的磁盘文件格式,文件内 key 有序排列。内存中真正承担写入的是 MemTable(通常用跳表实现),SSTable 是 MemTable 攒满后刷盘生成的产物。一旦生成,SSTable 就不可修改,只能被 Compaction 合并重写。
为何常见:名字里带"Table",又常与 LSM 树整体绑定讨论,初学者容易把它和内存里的表结构混为一谈。
误区“LSM 树的各层 SSTable 都严格按 key 范围不重叠,查任何一层都只要看一个文件。”
正解只有 L1 及以下各层通过 Compaction 保证 key 范围不重叠;L0 是 MemTable 直接刷盘生成的,文件之间可能重叠。因此查询 L0 时不得不检查该层的多个候选文件,这也是 LSM 读放大在 L0 层最严重的根源。
为何常见:讲解 LSM 时为了强调优点,常说“高层有序不重叠”,把 L0 这个特殊层忽略掉了。
误区“布隆过滤器返回 true,说明 key 一定在 SSTable 里,可以放心去读。”
正解布隆过滤器是“否定专家”——它说 false,key 一定不存在;它说 true,只代表“可能存在”,有误判概率。因为不同 key 的哈希可能点亮同一位,出现假阳性很正常。所以布隆过滤器只能用来跳过不存在的文件,命中后仍要进索引块和数据块精确确认。
为何常见:“过滤器”这个名称让人以为它是筛选出真数据的工具,忽略了它本质上只负责排除假数据。
误区“SSTable 的前缀压缩就是把整个数据块用 zlib 或者 Snappy 压一遍,减少磁盘占用。”
正解前缀压缩是 SSTable 特有的差分编码:相邻 key 共享前缀只存差异后缀,每个 restart 点重置。RocksDB 确实可以在块级再套 Snappy 等通用压缩,但那是另一层操作。前缀压缩是无损的、专门针对有序 key 的优化,与通用压缩算法不是一回事。
为何常见:“压缩”一词太宽泛,加上 RocksDB 同时开启块压缩,新手很容易把两个概念合并。
误区“LSM 树是顺序写友好架构,所以不会有写放大问题,磁盘磨损自然也很小。”
正解顺序写消除了“随机写”的磁道寻道开销,但 Compaction 会反复读取旧文件、归并、重写新文件,产生可观的写放大。LevelDB 的典型写放大在 10~30 倍,这也是 SSD 寿命消耗的重要来源。没有写放大的是纯追加型日志,不是 LSM。
为何常见:宣传材料常说“LSM 顺序写性能好”,把“随机写变顺序写”误推广为“零额外写入”。
面试题
Q1. 关于 SSTable 中数据的排列方式,下列说法正确的是?
- A.按写入时间先后排列
- B.按 key 的字典序排列← 正确答案
- C.按 value 长度升序排列
- D.按哈希值随机均匀排列
答案:B。SSTable 全称 Sorted String Table,核心特征就是 key 严格有序。写入时先在内存 MemTable 排好序再刷盘成文件,按时间排列会导致范围查询失效,按哈希排列则无法二分定位。
Q2. SSTable 文件中的 Footer 位于文件的什么位置?
- A.文件开头前四十八字节
- B.与各个数据块交错分布
- C.文件末尾的固定区域← 正确答案
- D.文件头部的独立区域
答案:C。SSTable 采用“从尾读起”的设计,Footer 固定在文件末尾,记录 Meta Index 和 Index Block 的偏移与大小。打开文件时先读尾部这片固定区域,就能拿到整册书的“地图”,再按图索骥。
Q3. 关于布隆过滤器查询结果,以下说法正确的有?
- A.返回不存在则一定不存在← 正确答案
- B.返回不存在也可能实际存在
- C.返回存在时可能实际不存在← 正确答案
- D.返回存在就是一定真实存在
答案:AC。布隆过滤器零漏报、有误报。只要有一个哈希位为 0,就说明该 key 从未插入,A 对;全部位为 1 时只是“可能存在”,C 对。B 和 D 恰好说反了,是初学者最容易踩的坑。
Q4. 数据块前缀压缩中,restart 点处记录的 key 有什么特点?
- A.与上一条 key 共享全部前缀
- B.shared_len 为 0 并写完整 key← 正确答案
- C.只记录 key 的哈希值
- D.与上一条 key 完全相同
答案:B。restart(重启点)每隔若干条出现一次,它把 shared_len 置为 0,重新写完整 key,作为块内二分查找的锚点。若共享全部前缀则没有区分度,也不能作为查找起点。
Q5. 一次点查定位候选数据块时,首先使用 SSTable 的哪个部分?
- A.用布隆过滤器直接定位块
- B.用索引块二分查找定位← 正确答案
- C.用 Meta Index 遍历定位
- D.用 Footer 逐项扫描定位
答案:B。布隆过滤器只负责“过滤不存在”,并不是定位器;真正的定位交给 Index Block。索引块记录每个数据块的末 key、偏移和大小,二分找到候选块后读入该块内部再查。Footer 只在打开文件时读一次。
Q6. 关于 LSM 树的层级关系,下列说法正确的有?
- A.L0 由刷盘生成且文件间有重叠← 正确答案
- B.L1 及以下各层内部不重叠← 正确答案
- C.每层容量按固定倍数递增← 正确答案
- D.所有层都必须一次查多个文件
答案:ABC。L0 是 MemTable 直接落盘,不经过合并,文件间可以重叠;L1 以下通过 Compaction 保证不重叠;容量通常按 T、10T、100T 增长。只有 L0 需要查多个文件,高层每层最多一个候选文件,D 错。
Q7. 位数组长度 m、元素数 n、哈希函数 k 时,最优 k 的近似值为?
- A.m 与 n 的乘积
- B.m 除以 n 再乘 ln2← 正确答案
- C.n 除以 m 再乘 ln2
- D.恒定为八个哈希函数
答案:B。最优哈希函数数 k_opt = (m/n)·ln2。比如每个元素分配 10 bit 时约用 7 个哈希。C 选项分子分母写反了,A 是量纲错误,D 是工程经验值而非理论最优。
Q8. 前缀压缩字段 shared_bytes 表示什么含义?
- A.与块内第一条 key 的公共前缀长度
- B.与上一条 key 的公共前缀长度← 正确答案
- C.与下一个 key 的公共前缀长度
- D.与索引块首个 key 的公共前缀长度
答案:B。shared_bytes 是当前 key 相对“上一条已编码 key”的公共前缀字节数。restart 点处 shared_bytes 为 0,写完整 key。它是按顺序局部差分,与块内第一条 key 或索引块无关,理解这一点才能读懂数据块布局。
Q9. 一条写入 (k,v) 在 LSM 引擎中的完整路径包括?
- A.先追加写 WAL 日志← 正确答案
- B.更新内存中的 MemTable← 正确答案
- C.原地修改已落盘的 SSTable
- D.MemTable 满后刷盘成新 SSTable← 正确答案
答案:ABD。写路径是 WAL → MemTable → Immutable → 刷盘生成新 SSTable。SSTable 一旦生成不可变,绝不原地修改已有数据块,覆盖靠新文件在更高层压过旧版本,所以 C 错。
Q10. 某 SSTable 有 N 个数据块,每块平均 K 条记录,块内二分查找,一次点查的时间复杂度约为?
- A.O(N + K) 需线性扫描两级
- B.O(N × K) 需全文件扫描
- C.O(logN + logK) 两级二分← 正确答案
- D.O(log(N × K)) 整文件二分
答案:C。先在索引块二分定位数据块,耗时 O(logN);再在数据块内二分定位 entry,耗时 O(logK),总复杂度 O(logN + logK)。SSTable 的数据块之间物理不连续,不能把整文件当成一个数组做一次二分,D 错。