cube_db 是一个用 Zig 0.16.0 编写的嵌入式键值存储引擎。固定页(4KB) + freelist 页面复用 + COW B-tree:
- O(1) compact:只写 meta page,不重写数据
- ~1× 写放大:旧页进 freelist 原地复用
- O(1) 恢复:读两个 meta page
- MVCC reader 安全:脏页持有到读者释放
- 溢出页:大 value 自动走溢出页链
纯同步 API,调用方无需准备 runtime。
依赖:
- Zig 0.16.0
- 本地
../zio仓库(build.zig.zon的 path 依赖)
zig build test # 跑全部测试
zig build -Doptimize=ReleaseFast # 编译库
zig build bench -Doptimize=ReleaseFast # 跑基准测试作为依赖引入:
// build.zig
const cube_dep = b.dependency("cube_db", .{ .target = target, .optimize = optimize });
const cube_mod = cube_dep.module("cube_db");
exe.root_module.addImport("cube_db", cube_mod);代码中导入:
const cube = @import("cube_db");
const Db = cube.Db;
const Entry = cube.Entry;
const Options = cube.Options;cube_db 使用固定大小页(4KB)和 freelist 页面复用,无需全量 rewrite。
const std = @import("std");
const zio = @import("zio");
const cube = @import("cube_db");
const Db = cube.Db;
const MemPageStore = cube.page_store.MemPageStore;
pub fn main() !void {
const allocator = std.heap.page_allocator;
var ms = MemPageStore.init(allocator, 1 << 20); // 1M 页
defer ms.deinit();
var db = try Db.open(allocator, ms.store(), .{});
defer db.close();
try db.put("hello", "world");
const v = try db.get("hello");
defer allocator.free(v.?);
std.debug.print("got: {s}\n", .{v.?});
}var fps = try cube.file_page_store.FilePageStore.init(allocator, "my.db"); // LMDB 式 1TB 预留 mmap
defer fps.deinit();
var db = try Db.open(allocator, fps.store(), .{});
defer db.close();
try db.put("hello", "world");open 从 meta page(page 1/2)恢复状态:root、sequence、entry_count、byte_size。恢复无需扫全文件,O(1)。
FilePageStore 特性:
- mmap 1TB 预留区:64-bit 系统虚拟地址空间充裕,文件按需
ftruncate增长 - 零拷贝读:reader 直接经 mmap 指针读,无需
read()系统调用 - 双 meta 页交替:meta 写入 page 1 和 page 2 交替进行,崩溃后取 sequence 较大者恢复
- fsync 落盘:
commit后自动fsync,保证崩溃安全
// 内存模式
var ms = MemPageStore.init(allocator, mapsize_pages);
defer ms.deinit();
var db = try Db.open(allocator, ms.store(), .{});
defer db.close();// 文件模式(mmap)
var fps = try cube.file_page_store.FilePageStore.init(allocator, "path.db");
defer fps.deinit();
var db = try Db.open(allocator, fps.store(), .{});
defer db.close();const v = try db.get("hello");
if (v) |value| {
defer allocator.free(value); // ⚠️ 返回值由 allocator 分配,调用方负责 free
std.debug.print("hello = {s}\n", .{value});
} else {
std.debug.print("(missing)\n", .{});
}- 返回
!?[]u8:null= key 不存在(或被 delete)。 - 非 null 的 value 是新分配的拷贝,必须
free。 - get 无锁、无 fsync,读原子 root 快照。
- 读路径优化:跳过 CRC 校验,get 100B ~2.7µs(接近 LMDB 级)
- 高频读选型:逐次 alloc/free 有成本,热路径(缓存/索引层)用
getInto(见 3.2.1)。
var buf: [4096]u8 = undefined; // 调用方持有 buffer,可复用
const n = (try db.getInto("hello", &buf)) orelse return; // null = key 不存在
std.debug.print("hello = {s}\n", .{buf[0..n]});getInto(key, buffer) !?usize:value 拷入 buffer,返回写入字节数;ReadTxn.getInto同款(快照 pin 于 txn root)。null唯一保留给 key 不存在(含 tombstone)——与 T-23 移除的旧 borrowed API 的 null 多义性彻底解耦(拷贝语义,无借用失效来源)。- buffer 不足返回
error.BufferTooSmall,且 buffer 不被写入/不清空(无部分 写入,可安全换大 buffer 重试)。 - 精确边界:buffer 恰好等长成功;溢出值(>3800B)经溢出页链逐页直拷, 与 get() 逐字节一致。
- 全链路零分配:分配权归调用方,高频读摆脱 per-call alloc/free。
迁移说明:零拷贝 borrowed 读 API(Db 层与 ReadTxn 层)已于 T-23 移除 — 其 null 返回值存在多义性(溢出值 / 墓碑 / 不存在三种语义不可区分)。T-29 以
getInto(拷贝入调用方 buffer + 显式 BufferTooSmall 错误)解决同一问题:null 语义唯一,分配权归调用方。读取可用get()/getInto(),语义等价、性能特性不同。
try db.put("hello", "world"); // 单条,1 次 commit
try db.delete("hello"); // tombstone批量写(推荐):
const entries = [_]Entry{
.{ .key = "a", .value = "1" },
.{ .key = "b", .value = "2" },
.{ .key = "c", .value = "3" },
};
try db.putBatch(&entries); // 整批 1 次 commitMicro-batching(自动批量提交):
var db = try Db.open(allocator, store, .{
.micro_batch = .{ .batch_threshold = 100 },
});
// 前 99 次 put 暂存,第 100 次自动 flush
try db.put("k1", "v1");
try db.put("k2", "v2");
// ... 第 100 次 put 触发批量提交
// 强制提交暂存数据
try db.flush();
// 跳过 batching,立即提交
try db.putDirect("urgent", "now");batch_threshold = 0(默认):禁用 batching,put/delete立即提交batch_threshold > 0:暂存到 pending,达到阈值自动flush()flush():强制提交所有 pending entriesputDirect()/deleteDirect():跳过 batching,立即提交close():自动 flush 残留 entries
Entry 结构:
pub const Entry = struct {
key: []const u8,
value: []const u8,
tombstone: bool = false, // true 表示删除该 key
};- 同 key 后者胜:批内多个相同 key,最后一个生效。
- key/value 是借用切片,在
putBatch返回前必须保持有效。
批量删除:
const dels = [_]Entry{
.{ .key = "a", .value = "", .tombstone = true },
.{ .key = "b", .value = "", .tombstone = true },
};
try db.putBatch(&dels);范围删除(deleteRange):按 [min, max) 半开区间批量删除 key,边界语义与 select 完全一致——null min/max 表示该侧无界,(null, null) 清空全库。内部基于 select 迭代器 + tombstone 批量提交实现,micro-batch 下会先 flush 暂存项再删除,保证 pending 的 key 也在删除范围内。
// 删除 [b, d):删 b、c,保留 a、d、e
try db.deleteRange("b", "d");
// null 边界 = 无界
try db.deleteRange(null, "c"); // 删所有 < c 的 key
try db.deleteRange("m", null); // 删所有 >= m 的 key
try db.deleteRange(null, null); // 清空全库- 半开区间
[min, max):max本身不被删除(同select)。 - 反向/空区间(
min >= max,均非 null):no-op 成功,不报错。 - 对已不存在的 key 幂等:再次删除同一区间仍成功。
- 删除后
entryCount()相应减少(与单条delete记账一致)。
put/putBatch/delete 是便捷 API(内部包隐式 WriteTxn,立即提交)。需多步原子或 abort 时用显式事务:
// 写事务:单写者互斥,commit = applyBatch + meta 切换 + fsync;abort 丢弃不落盘
var w = try db.beginWriteTxn();
defer w.deinit(); // 未 commit/abort 时 deinit 自动 abort
try w.put("k", "v");
try w.delete("old");
try w.commit(); // 原子提交
// 读事务:MVCC 快照(持有开 txn 时的 root),不阻塞写者
var r = try db.beginReadTxn();
defer r.end();
const v = try r.get("k");
defer if (v) |val| allocator.free(val);- WriteTxn:单写者互斥(同一时刻仅一个活跃);
commit后原子可见,abort丢弃。 - ReadTxn:快照隔离,写者提交新版本后 reader 仍读旧快照,直到
end。 - 跨
beginWriteTxn/commit的 key/value 须保持有效(借用切片)。
var it = try db.select("b", "d"); // [min, max)
defer it.deinit();
while (try it.next()) |entry| {
std.debug.print("{s} = {s}\n", .{ entry.key, entry.value });
}- 区间是 [min, max):min 包含、max 不包含。
null表示无界:db.select(null, null)遍历全部。- 自动跳过 tombstone。
- 借用契约(T-29 Phase B):
entry.key/entry.value为借用切片,仅到 下次next()或deinit()前有效。内联 value/key 直接借自 mmap 页(零拷贝); 溢出 value(>3800B)经迭代器内部复用缓冲拼接——连续迭代溢出条目时,前一 entry 的 value 会被新条目覆盖(借用契约的可观察面)。 - 快照 pin(T-29 Phase B):迭代器生命周期持有 MVCC 读者名额——迭代中途
写者提交(COW)不影响已借用页,迭代器始终看到 select 时刻的快照;脏页延迟
到
deinit()(末位读者退出)才回收。忘记 deinit 会滞留读者名额(脏页 不回收、dirt计数不降),务必defer it.deinit()。ReadTxn.select的 迭代器持有独立 pin(与 txn 的 pin 可叠加),同样须 deinit。 - 直接用
btree.select(raw 页存储层)时无 pin,调用方自行保证 Store 稳定性。 - 扫描性能(T-29 Phase B 借用化):下推栈 O(深度)(无每节点 dupe 副本), 内联值扫描零分配,溢出值每迭代器惰性分配一次复用缓冲。
保留 entry 内容:
var keys = std.ArrayList([]u8).empty;
defer {
for (keys.items) |k| allocator.free(k);
keys.deinit(allocator);
}
var it = try db.select(null, null);
defer it.deinit();
while (try it.next()) |e| {
try keys.append(allocator, try allocator.dupe(u8, e.key));
}compact 是 O(1) 的——只写 meta page,不重写数据。
try db.compact(); // 立即回收所有脏页(需要无活跃 reader)- 有活跃 reader 时,脏页会留在 pending_free 中直到 reader 结束。
- compact 会自动 flush 所有可 flush 的 pending_free。
var db = try Db.open(allocator, store, .{
.fsync = true, // 每次写入后 fsync
});| 字段 | 类型 | 默认 | 含义 |
|---|---|---|---|
fsync |
bool |
true |
写操作是否 fsync 落盘。false = 更快但 crash 丢数据 |
compact 是 O(1) 的(meta 页切换,不重写数据)。
所有写/读操作返回 !T(错误联合)。常见错误:
| 错误 | 含义 |
|---|---|
OutOfMemory |
分配失败 |
PageNotFound |
页号无效(文件损坏) |
MapFull |
页空间耗尽(mapsize 不足) |
FileLocked |
文件已被另一个打开者锁定(T-34 多进程防护,见下节) |
db.put("k", "v") catch |err| switch (err) {
error.PageNotFound => { /* 存储损坏 */ },
else => return err,
};- 多线程读:安全。
get/select读原子 root 快照,无锁无 fsync。点读(get/getInto)与select一样在读取期间持有 MVCC reader pin(register-then-capture),在途读取引用的 COW 旧页不会被并发提交回收。 - MVCC reader:写入器在 reader 活跃时延迟回收脏页:
const reader = db.beginRead(); // 开始读,返回显式 Reader 句柄(携带快照)
const v = try db.get("k"); // 读一致性快照
db.endRead(reader); // 结束读(带句柄注销),释放脏页
beginRead返回的是 Reader 句柄(不是序列号);endRead必须传入同一句柄以配对注销。
- 没有活跃 reader 时:脏页在每次 commit 后自动回收。
- 不要跨线程共享一个迭代器;每个线程各开各的
select。
- 同一数据库文件同时只允许一个打开者:
FilePageStore.init在 open 后立即取 advisory 排他锁(flock(fd, LOCK_EX | LOCK_NB))。第二个进程(或同进程的另一个 fd)对同一路径 open 将得到error.FileLocked。 - 锁随持有者的 fd 关闭释放:正常
deinit()/进程退出,或持有进程崩溃/被 kill —— 内核关闭 fd,锁自动消失,无死锁残留。fork 出的子进程继承父进程 fd(共享同一 open file description), 不会与父进程自冲突;若子进程自己重新 open 同一路径,则按第二个打开者被拒。 - NFS 等网络文件系统上 flock 语义不保证,仅支持本地文件系统。
fn incr(db: *Db, key: []const u8) !void {
const cur = try db.get(key);
const n: u64 = if (cur) |c| blk: { defer allocator.free(c); break :blk std.fmt.parseInt(u64, c, 10) catch 0; } else 0;
var buf: [20]u8 = undefined;
const s = try std.fmt.bufPrint(&buf, "{d}", .{n + 1});
try db.put(key, s);
}注意:单 key 的 read-modify-write 非原子(get 与 put 之间有窗口)。
fn import(db: *Db, items: []const struct { k: []const u8, v: []const u8 }) !void {
var entries = try allocator.alloc(Entry, items.len);
defer allocator.free(entries);
for (items, 0..) |it, i| entries[i] = .{ .key = it.k, .value = it.v };
try db.putBatch(entries);
}var it = try db.select(null, null);
defer it.deinit();
var count: usize = 0;
while (try it.next()) |_| count += 1;
std.debug.print("total entries: {d}\n", .{count});fn deleteKeys(db: *Db, keys: []const []const u8) !void {
var entries = try allocator.alloc(Entry, keys.len);
defer allocator.free(entries);
for (keys, 0..) |k, i| entries[i] = .{ .key = k, .value = "", .tombstone = true };
try db.putBatch(entries);
}const cube = @import("cube_db");
const Db = cube.Db;
const Entry = cube.Entry;
const Options = cube.Options;
// 打开
var ms = cube.page_store.MemPageStore.init(allocator, 1 << 20);
defer ms.deinit();
var db = try Db.open(allocator, ms.store(), .{});
defer db.close();
// 读
const v = try db.get(key); // !?[]u8,调用方 free
const n = (try db.getInto(key, &buf)) orelse 0; // !?usize 零拷贝入 buf;null=key 不存在
// 写
try db.put(key, value); // 单条
try db.putBatch(&entries); // 批量(推荐)
try db.delete(key); // tombstone
// 范围 [min, max)
var it = try db.select(min, max); // min/max 可 null
defer it.deinit();
while (try it.next()) |e| { /* e.key, e.value 借用,next() 后失效;it pin 快照,勿忘 deinit */ }
// 压缩
try db.compact(); // O(1) meta 切换
// MVCC reader(显式句柄配对)
const reader = db.beginRead();
db.endRead(reader);
// 显式事务(LMDB 式)
var w = try db.beginWriteTxn(); // 单写者互斥
defer w.deinit(); // 未 commit/abort 自动 abort
try w.put(key, value); // 暂存
try w.commit(); // applyBatch + meta 切换 + fsync
// w.abort(); // 丢弃
var r = try db.beginReadTxn(); // MVCC 快照,不阻写者
defer r.end();
const rv = try r.get(key); // 借用快照
// try r.select(min, max);
try db.sync(); // async 模式下显式冲刷| 类型 | 定义 |
|---|---|
Db |
数据库句柄,所有操作的入口 |
Entry |
struct { key: []const u8, value: []const u8, tombstone: bool = false } |
Options |
struct { fsync: bool = true }(fsync=false 开 async 模式) |
WriteTxn |
db.beginWriteTxn(),单写者互斥,commit/abort/deinit |
ReadTxn |
db.beginReadTxn(),MVCC 快照,get/select/end/deinit |
Iterator |
select 返回,有 .next() !?LeafEntry 与 .deinit() |
get返回的[]u8:调用方 free。put/putBatch/delete的 key/value:借用,返回前有效。select的 min/max:借用,迭代器存活期间有效。- 迭代器
entry.key/entry.value:借用迭代器内部,下次next()前有效。 db:open分配,close释放。