← 返回首页

LSM-Tree 存储引擎从零实现

数据库内核 2026-10-08 · 阅读约 14 分钟

LSM-Tree(Log-Structured Merge-Tree)是现代高性能存储引擎的核心数据结构,被 LevelDB、RocksDB、Cassandra、HBase 等广泛采用。本文用 Rust 从零实现一个完整的 LSM-Tree 存储引擎。

一、LSM-Tree 核心架构

LSM-Tree 将写入操作先缓存在内存中的 MemTable(通常使用跳表实现),当 MemTable 达到阈值后冻结为 Immutable MemTable,然后 Flush 到磁盘形成 SSTable。后台 Compaction 线程持续合并 SSTable 文件,清理过期数据。

pub struct LSMTree {
    memtable: Arc, Option>>>,
    immutable_memtables: Vec, Option>>>>,
    levels: Vec>,
    options: LsmOptions,
    compaction_tx: Sender<()>,
}

pub struct LsmOptions {
    pub memtable_size_limit: usize,    // 默认 64MB
    pub level0_file_num_limit: usize,  // 默认 4
    pub level_size_multiplier: usize,  // 默认 10
    pub block_size: usize,             // 默认 4KB
    pub bloom_filter_bits: usize,      // 默认 10 bits per key
}

二、MemTable 与 WAL

MemTable 使用跳表实现,支持 O(log n) 的插入和查找。同时写入 WAL(Write-Ahead Log)保证持久性,崩溃恢复时从 WAL 重建 MemTable。

impl LSMTree {
    pub fn put(&self, key: &[u8], value: &[u8]) -> Result<()> {
        // 1. 先写 WAL
        self.wal.append(&LogEntry::Put(key.to_vec(), value.to_vec()))?;

        // 2. 写入 MemTable
        self.memtable.insert(key.to_vec(), Some(value.to_vec()));

        // 3. 检查是否需要 Flush
        if self.memtable.approximate_size() >= self.options.memtable_size_limit {
            self.trigger_flush()?;
        }
        Ok(())
    }

    pub fn delete(&self, key: &[u8]) -> Result<()> {
        self.wal.append(&LogEntry::Delete(key.to_vec()))?;
        self.memtable.insert(key.to_vec(), None);  // Tombstone
        Ok(())
    }
}

三、SSTable 文件格式

SSTable 由多个 Data Block 组成,每个 Block 存储有序的键值对。文件末尾是 Index Block(记录每个 Data Block 的起始 key 和偏移)和 Footer(元信息)。

pub struct SSTable {
    file: File,
    index: Vec<(Vec, u64)>,  // (start_key, block_offset)
    bloom_filter: BloomFilter,
    block_size: usize,
}

impl SSTable {
    pub fn get(&self, key: &[u8]) -> Result>> {
        // 1. Bloom Filter 快速过滤
        if !self.bloom_filter.may_contain(key) {
            return Ok(None);
        }
        // 2. 二分查找定位 Data Block
        let block_idx = self.index.partition_point(|(k, _)| k.as_slice() <= key);
        // 3. 在 Block 内二分查找
        let block = self.read_block(block_idx)?;
        Ok(block.binary_search_by_key(key))
    }
}

四、Compaction 策略

采用 Leveled Compaction 策略:Level 0 的 SSTable 之间 key 范围可能重叠,Level 1+ 的 SSTable 之间 key 范围不重叠。当某层 SSTable 数量或大小超过阈值时,选择该层的一个 SSTable 与下一层重叠的 SSTable 进行合并。

fn compact_level(&mut self, level: usize) -> Result<()> {
    let upper = self.levels[level].pick_compaction_candidate();
    let overlapping: Vec<_> = self.levels[level + 1].iter()
        .filter(|sst| sst.overlaps(&upper))
        .collect();

    let mut iter = MergeIterator::new(upper, overlapping);
    let mut new_sstables = Vec::new();

    while let Some((key, value)) = iter.next() {
        // 跳过 Tombstone 且底层无此 key 的记录
        if value.is_none() && !self.has_key_in_lower_levels(&key, level + 2) {
            continue;
        }
        new_builder.add(&key, &value);
    }
    Ok(())
}

五、Bloom Filter 优化

每个 SSTable 附带一个 Bloom Filter,以极小的内存开销(约 1.2 字节/key)避免 99% 以上的无效磁盘读取。使用多个哈希函数和位数组实现。

完整的 LSM-Tree 实现约 3000 行 Rust 代码,支持 Put/Get/Delete/Scan 操作,吞吐量可达 10 万 QPS。理解 LSM-Tree 是深入数据库内核的必经之路。

← 返回首页