fountain 的 f_store 模块:基于 LSM-Tree 的无锁键值存储引擎实战指南

📅 发布时间:2026/10/15 2:13:03
fountain 的 f_store 模块:基于 LSM-Tree 的无锁键值存储引擎实战指南
后端Web框架【免费下载链接】fountain一个用于服务器应用开发的综合工具库。 - 零配置文件 - 环境变量和命令行参数配置 - 约定优于配置 - 深刻利用仓颉语言特性 - 只需要开发动态链接库fboot负责加载、初始化并运行 - 覆盖服务器应用开发的各种场景项目地址https://gitcode.com/Cangjie-SIG/fountain点击查看免费下载f_store 是 fountain 工具库中一个以极致性能为第一目标的嵌入式键值存储引擎基于 LSM-TreeLog-Structured Merge-Tree架构实现关键路径完全无锁可直接用于服务器应用中的本地持久化存储场景如缓存落盘、会话存储、时序数据分批写入等。读完本文你将掌握 f_store 的完整 API 用法、TTL 与前缀遍历等核心能力并理解其无锁并发设计、WAL 崩溃恢复、SSTable 格式与 Compaction 机制等底层原理。模块概览与依赖关系f_store 对外暴露的唯一主类是Store位于 Store.cj它是一个 LSM-Tree 存储引擎底层由无锁跳表ConcurrentSkipListMap、预写日志WAL和分层 SSTable 文件组成。根据 f_store/README.md 与 cjpm.toml 中的依赖声明模块依赖关系如下f_store ├── f_base (Path, unsafeBytes, mcopy, AtomicInt64扩展) ├── f_concurrent (ConcurrentSkipListMap) ├── f_bloom (BloomFilter) ├── f_collection (集合工具如 PriorityQueue) ├── f_util (crc32) └── f_io (SegmentedLog —— WAL 的 I/O 底座)核心组件分工组件文件职责StoreStore.cj对外主类编排所有组件MemTable/MemTableManagerMemTable.cj、MemTableManager.cj内存有序表与双缓冲管理WALWAL.cj、WALRecord.cj、WALReader.cj预写日志、记录编解码与崩溃恢复LevelManagerLevelManager.cjL0~Ln 分层管理、点查询路由SSTable/SSTableIterator/SSTableMergerSSTable.cj、SSTableIterator.cj、SSTableMerger.cj有序字符串表读写与多路归并CompactorCompaction.cj后台合并线程PrefixIteratorPrefixIterator.cj前缀遍历迭代器ByteArray/EntryValueByteArray.cj、EntryValue.cj键类型与条目类型更完整的架构推演可参考 架构设计方案.md其中包含数据结构定义、IO 抽象层Linux io_uring / 非 Linux 同步 IO与数据流设计。Store 生命周期从打开到关闭public class Store : Resource是 LSM-Tree 存储引擎主类生命周期模式为Store(path) → 使用 add/get/remove/ttl/prefix → close()close()会被注册到atExit进程退出时自动调用见 Store.cj即使忘记手动关闭也不会在退出时丢数据。init(path: String)创建或打开指定路径的 Store。如果路径已存在会自动从 WAL 恢复未持久化的数据并加载已有的 SSTable 文件。参数参数类型说明pathString存储目录路径。会在该目录下创建wal/和sst/子目录示例let store Store(/tmp/my_store)初始化流程对应 Store.cj 源码创建目录{path}/wal/和{path}/sst/如不存在从wal/目录扫描.wal文件通过WALReader.recoverAll()恢复数据到 MemTable同时找出最大序列号startSeq从sst/目录通过LevelManager.loadExisting()加载已有 SSTable 元数据并恢复全局 SSTable 文件序号防止重启后文件名冲突清理已恢复的旧 WAL 文件数据已在 MemTable 中以startSeq 1为起始序号创建新 WAL 文件创建 Compactor 后台线程并启动放在 init 最后确保所有状态就绪注册atExit { close() }实际落地时目录结构为{path}/ ├── wal/ │ └── wal_0000000000000001.wal // 文件名携带起始序列号 └── sst/ ├── L0_1.sst └── L1_2.sst // 文件名格式 L{level}_{sstIndex}.sstSSTable 文件名中的level与sstIndex在启动时会重新解析用于恢复全局序号见 store_func.cj 的parseLevelFromSSTablePath/parseSSTIndexFromPath。核心 API 详解add写入键值对func add(key: ArrayByte, value: ArrayByte): ?ArrayByte— 添加键值对如果 key 已存在则返回旧 valueNone表示 key 不存在或旧值为 tombstone。参数类型说明keyArrayByte键字节数组支持空数组valueArrayByte值字节数组支持空数组原子性WAL 写入 MemTable 修改后立即返回不等 IO 落盘完成源码路径为wal.append(key, value)→EntryValue(value, seq)→memTableManager.getActive().add(...)见 Store.cj。并发安全关键路径完全无锁ConcurrentSkipListMap.add()使用 CAS。// 添加 k1→v1 store.add(k1.unsafeBytes(), v1.unsafeBytes()) // 覆盖 k1返回旧值 v1 let old store.add(k1.unsafeBytes(), v2.unsafeBytes())add带过期时间写入两个重载分别支持绝对过期时间和相对 TTL// 绝对过期时间到达该时间后 get 返回 None func add(key: ArrayByte, value: ArrayByte, expireAt: DateTime): ?ArrayByte // 相对 TTL从调用时刻开始计时 func add(key: ArrayByte, value: ArrayByte, expire: Duration): ?ArrayByte参数类型说明keyArrayByte键valueArrayByte值expireAtDateTime绝对过期时间expireDuration相对过期时间从调用时刻开始计时源码实现上两种重载最终都转换为纳秒级时间戳DateTime.now().toUnixTimeStamp().toNanoseconds() expire.toNanoseconds()见 Store.cj随后与 value 一同原子写入 WAL 和 MemTable 的EntryValue.expireAt字段。// 24 小时后过期 let expireAt DateTime.now() Duration.hour * 24 store.add(session.unsafeBytes(), token_abc.unsafeBytes(), expireAt) // 5 分钟后过期 store.add(temp_key.unsafeBytes(), tmp_val.unsafeBytes(), Duration.minute * 5)注意过期检查是惰性的仅在 get/prefix 时检查expireAt now即视为不存在不会主动清理过期数据过期数据会在 Compaction 时被清理。get查询键值func get(key: ArrayByte): ?ArrayByte— 查询键对应的值含过期检查。参数类型说明keyArrayByte键返回值?ArrayByte— 值。以下情况返回Nonekey 不存在key 已被remove()key 已过期expireAt now查询优先级active MemTable immutable MemTable L0 SSTable L1 SSTable ... Ln SSTable对应 Store.cj 的逐级查找逻辑。并发安全无锁读ConcurrentSkipListMap.get() SSTable 点查询。if (let Some(v) - store.get(k1.unsafeBytes())) { println(found: ${v}) }contains判断键是否存在func contains(key: ArrayByte): Bool— 判断键是否存在已考虑过期与 tombstone。参数类型说明keyArrayByte键返回值键存在、未过期且未被remove()时为true否则false。查询优先级与get相同。if (store.contains(k1.unsafeBytes())) { println(k1 exists) }remove删除键func remove(key: ArrayByte): ?ArrayByte— 删除键。写入 tombstone删除标记不物理删除返回被删除的值。参数类型说明keyArrayByte键注意删除是惰性的写入 tombstone 而非物理删除wal.append(key, None)EntryValue(None, seq)见 Store.cjtombstone 在 Compaction 到最底层时被清理已持久化 SSTable 中的旧记录不会被修改let old store.remove(k1.unsafeBytes()) if (let Some(v) - old) { println(removed: ${v}) }ttl设置/更新过期时间两个重载分别接受绝对时间和相对时长func ttl(key: ArrayByte, expireAt: DateTime): Unit func ttl(key: ArrayByte, expire: Duration): Unit行为对应 Store.cj查找 key 的当前 valueactive → immutable → SSTable即lookupEntry以相同 value 新 expireAt 重新写入新 sequence 高于旧值查询时优先返回新版本key 不存在或已删除tombstone时为空操作store.add(k1.unsafeBytes(), v1.unsafeBytes()) store.ttl(k1.unsafeBytes(), Duration.hour * 1) // 1小时后过期prefix前缀遍历func prefix(prefix: ArrayByte): PrefixIterator— 前缀遍历返回所有以prefix开头的键值对迭代器自动去重高 sequence 优先。过期数据和 tombstone 被跳过。参数类型说明prefixArrayByte前缀字节数组返回值PrefixIterator— 实现Iterator(ArrayByte, ArrayByte)的迭代器实现代码见 PrefixIterator.cj内部用二叉堆做多路归并从 O(n) 降到 O(log n)。迭代器生命周期PrefixIterator不持有Store引用关闭 PrefixIterator 不影响 Store 的后续使用add/get/remove/ttl/prefix 等操作不受影响SSTable 遍历使用独立的File句柄SSTable.tailer() 内新建 File 传入 SSTableIterator关闭迭代器只关闭该独立句柄不影响 Store 管理的 SSTable 本身MemTable tailerConcurrentSkipListMap迭代器为纯内存操作不持有任何资源迭代器在创建时从 MemTable 和 SSTable 获取快照不受后续写入影响let iter store.prefix(user:.unsafeBytes()) while (let Some((k, v)) - iter.next()) { println(key${k} value${v}) }性能利用ConcurrentSkipListMap.tailer()的 O(log n) 索引定位起始位置源码层面prefix()会先计算前缀上界computePrefixEnd()将前缀最后一个字节 1 作为范围上界全 0xFF 时无上界只遍历[prefix, prefix1)范围内的数据见 Store.cj。syncWAL强制刷盘func syncWAL(): Unit— 强制将 WAL 缓冲区写入磁盘确保崩溃时可恢复。注意WAL 默认每 100 次append()自动执行一次fsync通过SegmentedLog的分段配置实现见 WAL.cj。调用此方法可确保调用之前的所有写入在系统崩溃后可恢复。close 后调用会抛出StoreClosedException。store.add(k1.unsafeBytes(), v1.unsafeBytes()) store.syncWAL() // 确保 k1 落盘close关闭存储func close(): Unit— 关闭 Store持久化所有未落盘数据。幂等多次调用不报错通过closed.compareAndSwap(false, true)CAS double-check见 Store.cj。关闭流程停止后台 Compaction 线程compactor.stop()flush 已有的 immutable MemTable 到 SSTableswapActive → flush 当前 active MemTable 到 SSTable还会二次 swap 补刷 flush 期间并发写入的新 activeWAL sync close关闭所有 SSTable 文件其他辅助方法func isClosed(): Bool— Store 是否已关闭。func levelSummary(): String— 返回各层 SSTable 数量摘要调试用如L0:3, L1:1实现见 LevelManager.cj。完整使用示例以下示例完整覆盖读写、覆盖写、TTL、删除、前缀遍历、同步与重开可直接运行let store Store(/tmp/demo_store) // 基本写入/读取 store.add(name.unsafeBytes(), Alice.unsafeBytes()) if (let Some(v) - store.get(name.unsafeBytes())) { println(Hello, ${v}) } // 覆盖写 store.add(name.unsafeBytes(), Bob.unsafeBytes()) // 带 TTL store.add(session.unsafeBytes(), tok_123.unsafeBytes(), Duration.minute * 30) // 删除 let old store.remove(name.unsafeBytes()) // 前缀遍历 store.add(user:1.unsafeBytes(), Alice.unsafeBytes()) store.add(user:2.unsafeBytes(), Bob.unsafeBytes()) let iter store.prefix(user:.unsafeBytes()) while (let Some((k, v)) - iter.next()) { println(${k} → ${v}) } // 同步 关闭 store.syncWAL() store.close() // 重新打开数据仍在 let store2 Store(/tmp/demo_store) let name store2.get(session.unsafeBytes()) store2.close()其他公开类型与函数func startsWith(key: ByteArray, prefix: ByteArray): Bool判断key是否以prefix开头prefix为空时恒为true。前缀遍历就是用它做判据见 PrefixIterator.cj也可以自己拿来过滤。ByteArraypublic struct ByteArrayStore 的键都以此承载可用字节数组构造ByteArray(bytes)用bytes取回原始字节可比较、可哈希。实现要点见 ByteArray.cj字典序比较逐字节比较短前缀为小全等则相等hashCode 在构造时预计算并缓存使ConcurrentSkipListMap查找节点时 hashCode() 从 O(n) 降为 O(1)这是 README 性能优化历程第 4 项的源码落点EntryValuepublic class EntryValueMemTable/SSTable/WAL 中的一条记录承载值字节、sequence与过期时间构造时传入None表示 tombstone删除标记见 EntryValue.cj。字段类型说明value?ArrayByteNone tombstonesequenceInt64全局单调递增序列号用于多版本合并高 sequence 优先expireAt?Int64过期时间戳纳秒None 永不过期异常异常抛出场景StoreClosedException对已关闭的 Store 执行操作PrefixIteratorException前缀迭代器读取过程中失败原始异常挂在suppressed上SSTableWritingException写 SSTable 失败构造时携带被包装的异常并发安全关键路径完全无锁Store 的所有操作均为无锁或原子操作详见 Store.cj 与 MemTableManager.cj操作并发安全说明add✅ 无锁WAL.append CAS AtomicInt64 sequenceremove✅ 无锁同 add写入 tombstoneget✅ 无锁跳表 SSTable 无锁点查询ttl✅ 无锁查找 CAS 写入prefix✅ 无锁弱一致性迭代器不阻塞写close✅ 原子CAS double-check底层无锁机制的源码证据MemTable 双缓冲MemTableManager将 active immutable 合并为MemTablePair由单一AtomicReference保护。swapActive() copy CASgetActive()原子加载快照见 MemTableManager.cj。swap 只在 MemTable 满默认阈值 8MB见 Store.cj时触发读路径永远无锁。LevelManager 无锁层级每层的 SSTable 列表用AtomicReferenceArrayListSSTable保护查询原子加载快照迭代增删走 copy CAS 原子替换见 LevelManager.cj。此外还有levelSizes与totalSSTableCount两个缓存计数器避免遍历全量文件判断是否 compaction、并在无 SSTable 时让 get 直接跳过全层循环。WAL 顺序性WAL 基于fountain::f_io.SegmentedLog实现写入委托给分段日志内部保证追加顺序。锁使用全景来自 架构设计方案.md锁保护对象持有时间说明MemTableManager.flushLockMemTable swap微秒级引用交换不阻塞读写LevelManager.levelLocks[n]SSTable 列表变更毫秒级Compaction 替换不阻塞查询SSTable.getLock点查询与 close 互斥微秒级防止并发 get 时 close 导致 fd 重用内部实现原理从 WAL 到 SSTableWAL预写日志与零分配编码每个 WAL 记录格式小端序见 WALRecord.cjchecksum(4) sequence(8) key_len(8) value_len(8) expire_at(8) key_bytes value_bytesvalue_len 0表示 tombstonevalue_len 0为正常 value含空数组expire_at 0表示永不过期非零为纳秒级过期时间戳checksumCRC32覆盖 sequence key_len value_len expire_at key_bytes value_bytes编码优化对应 README 优化历程第 1 项WAL.append走encodeDirect()零分配路径 —— 使用 ThreadLocal 4KB 编码缓冲区 Array[0..n]零拷贝切片每次 append 从 2 次堆分配降为 0 次超大记录 4KB回退到WALRecord.encode()分配路径见 WAL.cj。MemTable内存有序表MemTable直接委托ConcurrentSkipListMapByteArray, EntryValuebyteCountAtomicInt64 近似值用于判断是否刷盘。覆盖写时增量计算newSize - oldSize避免计数漂移见 MemTable.cj。SSTable有序字符串表单文件格式见 SSTable.cj 头部注释[Data Block 1][Data Block 2]...[Data Block N] [Index Block] [Bloom Filter Block] [Footer: 64 bytes]Data Block记录编码key_len(8) value_len(8) sequence(8) expire_at(8) key_bytes value_bytesvalue_len -1表示 tombstoneIndex Blockindex_count(8) [offset(8) first_key_len(8) first_key_bytes]...记录每个 Data Block 的起始偏移和最小 key点查询时二分定位Bloom Filter Block完整布隆过滤器参数和位集BloomFilter.serialize()直接序列化用于快速排除不存在的 keyFooter64 bytesindex_offset index_size bloom_offset bloom_size entry_count magic(0x53545354 STST) sequence_start sequence_end每个文件 ≤ 64MBMAX_SSTABLE_SIZEData Block 阈值为 4KB写入时校验键的有序性文件创建后不可变删除靠 tombstone物理删除靠 Compaction点查询路径bloomFilter.mightContain()快速否定 →binarySearchIndex()二分定位 Data Block → Linux 下pread(fd)单 syscall 定位读取非 Linux 走seek read→ 在块内零拷贝切片扫描比较scanBuffer免分配、免拷贝。LevelManager 与 Compaction分层规则见 LevelManager.cjLevelCompaction 触发条件L0SSTable 数量 ≥ 4L0 范围允许重叠必须合并L1总大小 ≥ 10MBL2总大小 ≥ 100MBLn总大小 ≥ 10^n MBCompaction 流程见 Compaction.cj选择输入 SSTableL0 从末尾选最多 4 个新文件在末尾数据最新Ln 优先选与下一层范围重叠的文件构建SSTableMerger多路归并迭代器同 key 取最高 sequence跳过过期数据底层bottom level移除 tombstone非底层保留防止更底层还有该 key 的旧版本写入新 SSTable满 64MB 自动分段finishWrite写 Index Bloom Footer 并 flush原子替换先addSSTable新文件再removeSSTables旧文件并删除磁盘文件并发读取安全优先看到新数据finally中关闭所有源迭代器防止异常时 FD 泄漏后台线程每轮扫描 L0~Ln-1无工作时休眠 100msclose()时通过 stopSignal 停止并等待线程结束。崩溃恢复启动恢复流程见 Store.cj 与 WALReader.cj启动 → 1. 扫描 WAL 文件按文件名排序文件名含 sequence_start → 2. 流式逐条读取checksum 校验失败即截断该记录及之后所有记录 → 3. 重放到 MemTable同时找出最大 sequence → 4. 加载 SSTable 元数据Footer Index Bloom → 5. 删除旧 WAL创建新 WAL起始序号 最大 sequence 1 → 6. 就绪WAL 默认每 100 次 append 自动 fsync最坏情况下崩溃仅丢失最后一次 fsync 之后的数据syncWAL()可随时强制落盘。SSTable 迭代器独立 File 句柄设计SSTable.iterator()和SSTable.tailer()每次调用都创建新的File(path, OpenMode.Read)传入SSTableIterator而非复用SSTable自身的file字段见 SSTable.cj。这是为了生命周期解耦。动机Compaction 场景Compaction 的典型流程依赖迭代器超越父 SSTable 的寿命let iter oldSSTable.iterator()— 创建独立 File 句柄通过iter耗尽旧 SSTable 的所有数据写入新 SSTableoldSSTable.close()— 父 SSTable 关闭其内部file被释放删除旧.sst文件 —unlink仅移除目录项已打开 fd 仍可读如果迭代器共享SSTable.file步骤 3 会导致迭代器不可用——但 compaction 需要在 close 之后才能安全删除文件顺序不可颠倒。为什么点查询可以共享迭代器不行SSTable.get()点查询通过getLock pread(fd)共享了 fd见 SSTable.cj。但迭代器不能采用相同方案场景get()点查询迭代器生命周期SSTable 范围内可能超越SSTable并发迭代器数1Mutex 串行可多个同时存在共享 fd 风险低SSTable 活着时用高SSTable close 后 fd 回收 → 静默读错非 Linux 平台—seekread 需独占 File 对象安全性保障即使文件被unlink已打开的 fd 在内核中保持数据可达pread可正常读取见 SSTableIterator.cj 的安全设计注释迭代器close()/ 耗尽时自动关闭自己的 File 句柄无泄漏Compaction 先耗尽迭代器再关闭 SSTable迭代器生命周期内文件始终可达性能基准测试与优化历程以下数据来源于 f_store/README.md基准测试环境为 WSL、Intel Core Ultra 7 155H、cjHeapSize8GB、2026-05-02。Bench框架测量所有测试用例均包含 1000 次操作数据为中位数多次运行。同一套 benchmark 分别在串行-j 1和 8 路并行-j 8下执行基准用例源码见 Store_bench.cj。单线程cjpm bench -j 1操作中位数误差并行偏差说明add115.3 ms±0.7%-0.3%1000 次顺序写入get115.3 ms±0.7%-0.2%写入 1000 条后随机读取remove113.2 ms±2.0%-0.2%写入 1000 条后依次删除prefix111.4 ms±0.9%-0.5%500 条前缀px:user:全量遍历ttl130.1 ms±8.4%-5.9%写入 1000 条后更新 TTLaddWithExpire113.8 ms±0.5%0.1%1000 次add(key, value, Duration)concurrentAdd114.4 ms±0.6%-0.0%4 线程各写入 250 条共 1000 条concurrentGet114.3 ms±1.1%0.3%写入 1000 条后 4 线程并发读取mixed0.663 s±2.7%-0.4%add/get/remove/prefix/ttl 5 线程混合运行 500ms8 路并行cjpm bench -j 8操作中位数误差对比-j 1add115.1 ms±0.4%-0.3%get115.4 ms±1.0%-0.2%remove112.5 ms±1.8%-0.2%prefix110.7 ms±1.1%-0.5%ttl116.3 ms±7.7%-5.9%addWithExpire113.9 ms±0.8%0.1%concurrentAdd114.4 ms±0.4%-0.0%concurrentGet114.6 ms±0.7%0.3%mixed0.664 s±1.8%-0.4%优化历程源码落点已在上文对应位置说明#优化项收益文件1WAL 编码零分配路径ThreadLocal 编码缓冲区 Array[0..n]零拷贝切片TTL -10.4%WAL.cj2SSTable scanBuffer 切片化零拷贝切片直接比较SSTable 读路径免分配SSTable.cj3Bloom Filter 双重检查消除getDirect()跳过内部 bloom 检查SSTable 读路径免 1 次 bloom 哈希SSTable.cj、LevelManager.cj4ByteArray hashCode 预计算大 key ~5-10%ByteArray.cj5LevelManager.get() 早期退出totalSSTableCount缓存MemTable-only 场景 ~0.5-1%LevelManager.cj6WAL mmap 写写 page cache 路径消除 syscall小记录~50Bsyscall 非瓶颈未见显著变化WAL.cj稳定性分析串行与 8 路并行各项中位数差异均在 ±2.5% 以内验证了无锁设计消除了锁竞争benchTTL误差最大±8%由 TTL 内部两次跳表遍历 WAL 写入的开销波动导致。复现命令cjHeapSize8GB cjpm bench -j 1 # 串行基准值 cjHeapSize8GB cjpm bench -j 8 # 8 路并行验证无锁测试与验证f_store 模块提供了完整的单元测试与集成测试位于 f_store/src 下的*_test.cj文件如 Store_test.cj、Concurrency_test.cj、WAL_test.cj、SSTable_test.cj、PrefixIterator_test.cj、LevelManager_test.cj 等覆盖基本读写、覆盖写、删除、TTL 更新、前缀遍历、并发安全与崩溃恢复等场景。# 模块目录下执行全量测试 cd f_store cjpm test --no-capture-output --show-all-output # 指定测试类 cjpm test --filter StoreTest # 指定测试用例 cjpm test --filter StoreIntegrationTest.testAddGetBasic总结f_store 通过无锁跳表 MemTable WAL 预写日志 分层 SSTable 后台 Compaction的经典 LSM-Tree 组合实现了键值存储的完整能力原子读写、惰性 TTL、前缀遍历、崩溃恢复与高并发无锁访问。其 API 设计简洁键值均为字节数组天然支持任意二进制数据配合 fountain 生态的f_base、f_concurrent、f_bloom、f_util、f_io等模块适合作为服务器应用的嵌入式持久化底座。如需进一步了解架构细节可继续阅读 架构设计方案.md、审查报告.md 与 开发计划.md。赞分享后端Web框架【免费下载链接】fountain一个用于服务器应用开发的综合工具库。 - 零配置文件 - 环境变量和命令行参数配置 - 约定优于配置 - 深刻利用仓颉语言特性 - 只需要开发动态链接库fboot负责加载、初始化并运行 - 覆盖服务器应用开发的各种场景项目地址https://gitcode.com/Cangjie-SIG/fountain点击查看免费下载相关推荐fountain f_store LSM-Tree 键值存储引擎从架构设计方案到无锁源码实现fountain f_store LSM Tree 键值存储引擎从架构设计方案到无锁源码实现 本篇基于 fountain 仓库中 f_store 模块的架构设后端Web框架fountain f_store LSM 存储引擎的 19 轮代码审查从审查发现到源码级修复全记录fountain f_store LSM 存储引擎的 19 轮代码审查从审查发现到源码级修复全记录 本文基于 fountain 仓库 f_store 代码审查后端Web框架Pebble 全面指南RocksDB 兼容的 Go 版 LSM 键值存储引擎Pebble 全面指南RocksDB 兼容的 Go 版 LSM 键值存储引擎 Pebble 是一个以 LevelDB/RocksDB 为灵感、专注于性能并内嵌KV存储嵌入式数据库上一篇Aderyn社区精选开发者最常问的10个问题与官方解答下一篇Krokiet 重复文件清理硬盘里藏了多少副本一次扫描就能找出来创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考