☰
fountain 的 f_store 模块:基于 LSM-Tree 的无锁键值存储引擎实战指南
2026/10/15 2:12:47 网站建设 项目流程
  • 后端
  • Web框架

【免费下载链接】fountain

一个用于服务器应用开发的综合工具库。 - 零配置文件 - 环境变量和命令行参数配置 - 约定优于配置 - 深刻利用仓颉语言特性 - 只需要开发动态链接库,fboot负责加载、初始化并运行 - 覆盖服务器应用开发的各种场景

项目地址:https://gitcode.com/Cangjie-SIG/fountain
点击查看免费下载

f_store 是 fountain 工具库中一个以极致性能为第一目标的嵌入式键值存储引擎,基于 LSM-Tree(Log-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 源码):

  1. 创建目录{path}/wal/和{path}/sst/(如不存在)
  2. 从wal/目录扫描.wal文件,通过WALReader.recoverAll()恢复数据到 MemTable,同时找出最大序列号(startSeq)
  3. 从sst/目录通过LevelManager.loadExisting()加载已有 SSTable 元数据,并恢复全局 SSTable 文件序号防止重启后文件名冲突
  4. 清理已恢复的旧 WAL 文件(数据已在 MemTable 中)
  5. 以startSeq + 1为起始序号创建新 WAL 文件
  6. 创建 Compactor 后台线程并启动(放在 init 最后,确保所有状态就绪)
  7. 注册atExit { => close() }

实际落地时目录结构为:

{path}/ ├── wal/ │ └── wal_0000000000000001.wal // 文件名携带起始序列号 └── sst/ ├── L0_1.sst └── L1_2.sst // 文件名格式 L{level}_{sstIndex}.sst

SSTable 文件名中的level与sstIndex在启动时会重新解析,用于恢复全局序号(见 store_func.cj 的parseLevelFromSSTablePath/parseSSTIndexFromPath)。

核心 API 详解

add:写入键值对

func add(key: Array<Byte>, value: Array<Byte>): ?Array<Byte>— 添加键值对,如果 key 已存在则返回旧 value(None表示 key 不存在或旧值为 tombstone)。

参数类型说明
keyArray<Byte>键(字节数组),支持空数组
valueArray<Byte>值(字节数组),支持空数组

原子性: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: Array<Byte>, value: Array<Byte>, expireAt: DateTime): ?Array<Byte> // 相对 TTL:从调用时刻开始计时 func add(key: Array<Byte>, value: Array<Byte>, expire: Duration): ?Array<Byte>
参数类型说明
keyArray<Byte>键
valueArray<Byte>值
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: Array<Byte>): ?Array<Byte>— 查询键对应的值,含过期检查。

参数类型说明
keyArray<Byte>键

返回值:?Array<Byte>— 值。以下情况返回None:

  • key 不存在
  • 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: Array<Byte>): Bool— 判断键是否存在,已考虑过期与 tombstone。

参数类型说明
keyArray<Byte>键

返回值:键存在、未过期且未被remove()时为true,否则false。查询优先级与get相同。

if (store.contains("k1".unsafeBytes())) { println("k1 exists") }

remove:删除键

func remove(key: Array<Byte>): ?Array<Byte>— 删除键。写入 tombstone(删除标记),不物理删除,返回被删除的值。

参数类型说明
keyArray<Byte>键

注意:

  • 删除是惰性的:写入 tombstone 而非物理删除(wal.append(key, None)+EntryValue(None, seq),见 Store.cj)
  • tombstone 在 Compaction 到最底层时被清理
  • 已持久化 SSTable 中的旧记录不会被修改
let old = store.remove("k1".unsafeBytes()) if (let Some(v) <- old) { println("removed: ${v}") }

ttl:设置/更新过期时间

两个重载分别接受绝对时间和相对时长:

func ttl(key: Array<Byte>, expireAt: DateTime): Unit func ttl(key: Array<Byte>, expire: Duration): Unit

行为(对应 Store.cj):

  • 查找 key 的当前 value(active → 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: Array<Byte>): PrefixIterator— 前缀遍历:返回所有以prefix开头的键值对迭代器,自动去重(高 sequence 优先)。过期数据和 tombstone 被跳过。

参数类型说明
prefixArray<Byte>前缀字节数组

返回值:PrefixIterator— 实现Iterator<(Array<Byte>, Array<Byte>)>的迭代器(实现代码见 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 tailer(ConcurrentSkipListMap迭代器)为纯内存操作,不持有任何资源
  • 迭代器在创建时从 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, prefix+1)范围内的数据(见 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)。

关闭流程:

  1. 停止后台 Compaction 线程(compactor.stop())
  2. flush 已有的 immutable MemTable 到 SSTable
  3. swapActive → flush 当前 active MemTable 到 SSTable(还会二次 swap 补刷 flush 期间并发写入的新 active)
  4. WAL sync + close
  5. 关闭所有 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),也可以自己拿来过滤。

ByteArray

public struct ByteArray:Store 的键都以此承载,可用字节数组构造(ByteArray(bytes)),用bytes取回原始字节;可比较、可哈希。实现要点(见 ByteArray.cj):

  • 字典序比较:逐字节比较,短前缀为小,全等则相等
  • hashCode 在构造时预计算并缓存,使ConcurrentSkipListMap查找节点时 hashCode() 从 O(n) 降为 O(1)(这是 README 性能优化历程第 4 项的源码落点)

EntryValue

public class EntryValue:MemTable/SSTable/WAL 中的一条记录,承载值字节、sequence与过期时间;构造时传入None表示 tombstone(删除标记,见 EntryValue.cj)。

字段类型说明
value?Array<Byte>None = tombstone
sequenceInt64全局单调递增序列号,用于多版本合并(高 sequence 优先)
expireAt?Int64过期时间戳(纳秒),None = 永不过期

异常

异常抛出场景
StoreClosedException对已关闭的 Store 执行操作
PrefixIteratorException前缀迭代器读取过程中失败(原始异常挂在suppressed上)
SSTableWritingException写 SSTable 失败(构造时携带被包装的异常)

并发安全:关键路径完全无锁

Store 的所有操作均为无锁或原子操作(详见 Store.cj 与 MemTableManager.cj):

操作并发安全说明
add✅ 无锁WAL.append + CAS + AtomicInt64 sequence
remove✅ 无锁同 add,写入 tombstone
get✅ 无锁跳表 + SSTable 无锁点查询
ttl✅ 无锁查找 + CAS 写入
prefix✅ 无锁弱一致性迭代器,不阻塞写
close✅ 原子CAS double-check

底层无锁机制的源码证据:

  1. MemTable 双缓冲:MemTableManager将 active + immutable 合并为MemTablePair,由单一AtomicReference保护。swapActive()= copy + CAS,getActive()原子加载快照(见 MemTableManager.cj)。swap 只在 MemTable 满(默认阈值 8MB,见 Store.cj)时触发,读路径永远无锁。
  2. LevelManager 无锁层级:每层的 SSTable 列表用AtomicReference<ArrayList<SSTable>>保护,查询原子加载快照迭代,增删走 copy + CAS 原子替换(见 LevelManager.cj)。此外还有levelSizes与totalSSTableCount两个缓存计数器,避免遍历全量文件判断是否 compaction、并在无 SSTable 时让 get 直接跳过全层循环。
  3. WAL 顺序性:WAL 基于fountain::f_io.SegmentedLog实现,写入委托给分段日志,内部保证追加顺序。

锁使用全景(来自 架构设计方案.md):

锁保护对象持有时间说明
MemTableManager.flushLockMemTable swap微秒级(引用交换)不阻塞读写
LevelManager.levelLocks[n]SSTable 列表变更毫秒级(Compaction 替换)不阻塞查询
SSTable.getLock点查询与 close 互斥微秒级防止并发 get 时 close 导致 fd 重用

内部实现原理:从 WAL 到 SSTable

WAL:预写日志与零分配编码

每个 WAL 记录格式(小端序,见 WALRecord.cj):

checksum(4) + sequence(8) + key_len(8) + value_len(8) + expire_at(8) + key_bytes + value_bytes
  • value_len < 0表示 tombstone;value_len >= 0为正常 value(含空数组)
  • expire_at = 0表示永不过期,非零为纳秒级过期时间戳
  • checksum(CRC32)覆盖 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直接委托ConcurrentSkipListMap<ByteArray, EntryValue>,byteCount(AtomicInt64 近似值)用于判断是否刷盘。覆盖写时增量计算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_bytes,value_len = -1表示 tombstone
  • Index Block:index_count(8) + [offset(8) + first_key_len(8) + first_key_bytes]...,记录每个 Data Block 的起始偏移和最小 key,点查询时二分定位
  • Bloom Filter Block:完整布隆过滤器参数和位集(BloomFilter.serialize()直接序列化),用于快速排除不存在的 key
  • Footer(64 bytes):index_offset + index_size + bloom_offset + bloom_size + entry_count + magic(0x53545354 "STST") + sequence_start + sequence_end
  • 每个文件 ≤ 64MB(MAX_SSTABLE_SIZE),Data Block 阈值为 4KB,写入时校验键的有序性
  • 文件创建后不可变,删除靠 tombstone,物理删除靠 Compaction

点查询路径:bloomFilter.mightContain()快速否定 →binarySearchIndex()二分定位 Data Block → Linux 下pread(fd)单 syscall 定位读取(非 Linux 走seek + read)→ 在块内零拷贝切片扫描比较(scanBuffer,免分配、免拷贝)。

LevelManager 与 Compaction

分层规则(见 LevelManager.cj):

LevelCompaction 触发条件
L0SSTable 数量 ≥ 4(L0 范围允许重叠,必须合并)
L1总大小 ≥ 10MB
L2总大小 ≥ 100MB
Ln总大小 ≥ 10^n MB

Compaction 流程(见 Compaction.cj):

  1. 选择输入 SSTable:L0 从末尾选最多 4 个(新文件在末尾,数据最新);Ln 优先选与下一层范围重叠的文件
  2. 构建SSTableMerger多路归并迭代器(同 key 取最高 sequence,跳过过期数据)
  3. 底层(bottom level)移除 tombstone,非底层保留(防止更底层还有该 key 的旧版本)
  4. 写入新 SSTable(满 64MB 自动分段),finishWrite写 Index + Bloom + Footer 并 flush
  5. 原子替换:先addSSTable新文件,再removeSSTables旧文件并删除磁盘文件(并发读取安全:优先看到新数据)
  6. finally中关闭所有源迭代器,防止异常时 FD 泄漏

后台线程每轮扫描 L0~Ln-1,无工作时休眠 100ms;close()时通过 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 的寿命:

  1. let iter = oldSSTable.iterator()— 创建独立 File 句柄
  2. 通过iter耗尽旧 SSTable 的所有数据(写入新 SSTable)
  3. oldSSTable.close()— 父 SSTable 关闭,其内部file被释放
  4. 删除旧.sst文件 —unlink仅移除目录项,已打开 fd 仍可读

如果迭代器共享SSTable.file,步骤 3 会导致迭代器不可用——但 compaction 需要在 close 之后才能安全删除文件,顺序不可颠倒。

为什么点查询可以共享,迭代器不行

SSTable.get()点查询通过getLock + pread(fd)共享了 fd(见 SSTable.cj)。但迭代器不能采用相同方案:

场景get()点查询迭代器
生命周期SSTable 范围内可能超越SSTable
并发迭代器数1(Mutex 串行)可多个同时存在
共享 fd 风险低(SSTable 活着时用)高(SSTable close 后 fd 回收 → 静默读错)
非 Linux 平台—seek+read 需独占 File 对象

安全性保障

  • 即使文件被unlink,已打开的 fd 在内核中保持数据可达,pread可正常读取(见 SSTableIterator.cj 的安全设计注释)
  • 迭代器close()/ 耗尽时自动关闭自己的 File 句柄,无泄漏
  • Compaction 先耗尽迭代器再关闭 SSTable,迭代器生命周期内文件始终可达

性能:基准测试与优化历程

以下数据来源于 f_store/README.md,基准测试环境为 WSL、Intel Core Ultra 7 155H、cjHeapSize=8GB、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 条后更新 TTL
addWithExpire113.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 线程混合运行 500ms

8 路并行(cjpm bench -j 8):

操作中位数误差对比-j 1
add115.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.cj
2SSTable scanBuffer 切片化:零拷贝切片直接比较SSTable 读路径免分配SSTable.cj
3Bloom Filter 双重检查消除:getDirect()跳过内部 bloom 检查SSTable 读路径免 1 次 bloom 哈希SSTable.cj、LevelManager.cj
4ByteArray hashCode 预计算大 key ~5-10%ByteArray.cj
5LevelManager.get() 早期退出:totalSSTableCount缓存MemTable-only 场景 ~0.5-1%LevelManager.cj
6WAL mmap 写:写 page cache 路径消除 syscall小记录(~50B)syscall 非瓶颈,未见显著变化WAL.cj

稳定性分析:串行与 8 路并行各项中位数差异均在 ±2.5% 以内,验证了无锁设计消除了锁竞争;benchTTL误差最大(±8%),由 TTL 内部两次跳表遍历 + WAL 写入的开销波动导致。

复现命令:

cjHeapSize=8GB cjpm bench -j 1 # 串行(基准值) cjHeapSize=8GB 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
点击查看免费下载
上一篇:Aderyn社区精选:开发者最常问的10个问题与官方解答
下一篇:Krokiet 重复文件清理:硬盘里藏了多少副本,一次扫描就能找出来

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询