- 后端
- Web框架
【免费下载链接】fountain
一个用于服务器应用开发的综合工具库。 - 零配置文件 - 环境变量和命令行参数配置 - 约定优于配置 - 深刻利用仓颉语言特性 - 只需要开发动态链接库,fboot负责加载、初始化并运行 - 覆盖服务器应用开发的各种场景
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 底座)核心组件分工:
| 组件 | 文件 | 职责 |
|---|---|---|
Store | Store.cj | 对外主类,编排所有组件 |
MemTable/MemTableManager | MemTable.cj、MemTableManager.cj | 内存有序表与双缓冲管理 |
WAL | WAL.cj、WALRecord.cj、WALReader.cj | 预写日志、记录编解码与崩溃恢复 |
LevelManager | LevelManager.cj | L0~Ln 分层管理、点查询路由 |
SSTable/SSTableIterator/SSTableMerger | SSTable.cj、SSTableIterator.cj、SSTableMerger.cj | 有序字符串表读写与多路归并 |
Compactor | Compaction.cj | 后台合并线程 |
PrefixIterator | PrefixIterator.cj | 前缀遍历迭代器 |
ByteArray/EntryValue | ByteArray.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 文件。
参数:
| 参数 | 类型 | 说明 |
|---|---|---|
path | String | 存储目录路径。会在该目录下创建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: Array<Byte>, value: Array<Byte>): ?Array<Byte>— 添加键值对,如果 key 已存在则返回旧 value(None表示 key 不存在或旧值为 tombstone)。
| 参数 | 类型 | 说明 |
|---|---|---|
key | Array<Byte> | 键(字节数组),支持空数组 |
value | Array<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>| 参数 | 类型 | 说明 |
|---|---|---|
key | Array<Byte> | 键 |
value | Array<Byte> | 值 |
expireAt | DateTime | 绝对过期时间 |
expire | Duration | 相对过期时间(从调用时刻开始计时) |
源码实现上,两种重载最终都转换为纳秒级时间戳: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>— 查询键对应的值,含过期检查。
| 参数 | 类型 | 说明 |
|---|---|---|
key | Array<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。
| 参数 | 类型 | 说明 |
|---|---|---|
key | Array<Byte> | 键 |
返回值:键存在、未过期且未被remove()时为true,否则false。查询优先级与get相同。
if (store.contains("k1".unsafeBytes())) { println("k1 exists") }remove:删除键
func remove(key: Array<Byte>): ?Array<Byte>— 删除键。写入 tombstone(删除标记),不物理删除,返回被删除的值。
| 参数 | 类型 | 说明 |
|---|---|---|
key | Array<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 被跳过。
| 参数 | 类型 | 说明 |
|---|---|---|
prefix | Array<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)。
关闭流程:
- 停止后台 Compaction 线程(
compactor.stop()) - flush 已有的 immutable MemTable 到 SSTable
- swapActive → flush 当前 active MemTable 到 SSTable(还会二次 swap 补刷 flush 期间并发写入的新 active)
- WAL 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),也可以自己拿来过滤。
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 |
sequence | Int64 | 全局单调递增序列号,用于多版本合并(高 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 |
底层无锁机制的源码证据:
- MemTable 双缓冲:
MemTableManager将 active + immutable 合并为MemTablePair,由单一AtomicReference保护。swapActive()= copy + CAS,getActive()原子加载快照(见 MemTableManager.cj)。swap 只在 MemTable 满(默认阈值 8MB,见 Store.cj)时触发,读路径永远无锁。 - LevelManager 无锁层级:每层的 SSTable 列表用
AtomicReference<ArrayList<SSTable>>保护,查询原子加载快照迭代,增删走 copy + CAS 原子替换(见 LevelManager.cj)。此外还有levelSizes与totalSSTableCount两个缓存计数器,避免遍历全量文件判断是否 compaction、并在无 SSTable 时让 get 直接跳过全层循环。 - WAL 顺序性:WAL 基于
fountain::f_io.SegmentedLog实现,写入委托给分段日志,内部保证追加顺序。
锁使用全景(来自 架构设计方案.md):
| 锁 | 保护对象 | 持有时间 | 说明 |
|---|---|---|---|
| MemTableManager.flushLock | MemTable 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_bytesvalue_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):
| Level | Compaction 触发条件 |
|---|---|
| L0 | SSTable 数量 ≥ 4(L0 范围允许重叠,必须合并) |
| L1 | 总大小 ≥ 10MB |
| L2 | 总大小 ≥ 100MB |
| Ln | 总大小 ≥ 10^n MB |
Compaction 流程(见 Compaction.cj):
- 选择输入 SSTable:L0 从末尾选最多 4 个(新文件在末尾,数据最新);Ln 优先选与下一层范围重叠的文件
- 构建
SSTableMerger多路归并迭代器(同 key 取最高 sequence,跳过过期数据) - 底层(bottom level)移除 tombstone,非底层保留(防止更底层还有该 key 的旧版本)
- 写入新 SSTable(满 64MB 自动分段),
finishWrite写 Index + Bloom + Footer 并 flush - 原子替换:先
addSSTable新文件,再removeSSTables旧文件并删除磁盘文件(并发读取安全:优先看到新数据) 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 的寿命:
let iter = oldSSTable.iterator()— 创建独立 File 句柄- 通过
iter耗尽旧 SSTable 的所有数据(写入新 SSTable) oldSSTable.close()— 父 SSTable 关闭,其内部file被释放- 删除旧
.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):
| 操作 | 中位数 | 误差 | 并行偏差 | 说明 |
|---|---|---|---|---|
add | 115.3 ms | ±0.7% | -0.3% | 1000 次顺序写入 |
get | 115.3 ms | ±0.7% | -0.2% | 写入 1000 条后随机读取 |
remove | 113.2 ms | ±2.0% | -0.2% | 写入 1000 条后依次删除 |
prefix | 111.4 ms | ±0.9% | -0.5% | 500 条前缀px:user:全量遍历 |
ttl | 130.1 ms | ±8.4% | -5.9% | 写入 1000 条后更新 TTL |
addWithExpire | 113.8 ms | ±0.5% | +0.1% | 1000 次add(key, value, Duration) |
concurrentAdd | 114.4 ms | ±0.6% | -0.0% | 4 线程各写入 250 条(共 1000 条) |
concurrentGet | 114.3 ms | ±1.1% | +0.3% | 写入 1000 条后 4 线程并发读取 |
mixed | 0.663 s | ±2.7% | -0.4% | add/get/remove/prefix/ttl 5 线程混合运行 500ms |
8 路并行(cjpm bench -j 8):
| 操作 | 中位数 | 误差 | 对比-j 1 |
|---|---|---|---|
add | 115.1 ms | ±0.4% | -0.3% |
get | 115.4 ms | ±1.0% | -0.2% |
remove | 112.5 ms | ±1.8% | -0.2% |
prefix | 110.7 ms | ±1.1% | -0.5% |
ttl | 116.3 ms | ±7.7% | -5.9% |
addWithExpire | 113.9 ms | ±0.8% | +0.1% |
concurrentAdd | 114.4 ms | ±0.4% | -0.0% |
concurrentGet | 114.6 ms | ±0.7% | +0.3% |
mixed | 0.664 s | ±1.8% | -0.4% |
优化历程(源码落点已在上文对应位置说明):
| # | 优化项 | 收益 | 文件 |
|---|---|---|---|
| 1 | WAL 编码零分配路径:ThreadLocal 编码缓冲区 +Array[0..n]零拷贝切片 | TTL -10.4% | WAL.cj |
| 2 | SSTable scanBuffer 切片化:零拷贝切片直接比较 | SSTable 读路径免分配 | SSTable.cj |
| 3 | Bloom Filter 双重检查消除:getDirect()跳过内部 bloom 检查 | SSTable 读路径免 1 次 bloom 哈希 | SSTable.cj、LevelManager.cj |
| 4 | ByteArray hashCode 预计算 | 大 key ~5-10% | ByteArray.cj |
| 5 | LevelManager.get() 早期退出:totalSSTableCount缓存 | MemTable-only 场景 ~0.5-1% | LevelManager.cj |
| 6 | WAL 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负责加载、初始化并运行 - 覆盖服务器应用开发的各种场景
相关推荐
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存储嵌入式数据库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考