把 BadgerDB 移植到仓颉(一):没有裸指针怎么做无锁跳表

💡 原文中文,约4600字,阅读约需11分钟。
📝

内容提要

作者将Go的BadgerDB移植到无裸指针的仓颉语言,实现badger-cj。核心难点是并发跳表,采用数组下标偏移量加原子数组替代unsafe.Pointer和CAS,并避免repeat共享原子对象。代价是读性能约慢3倍,但获得无锁并发安全。项目支持事务、MVCC、WAL恢复、Compaction,并作为storm-cj等数据库的存储基础。

🔎

延伸解读

兼容性边界:数据格式未经验证,迁移需从零建库

文章明确区分了三种兼容状态:API 语义兼容、数据格式理论兼容但未经跨语言测试、不支持直接迁移。这意味着即使按 Go 的 SSTable/VLog/MANIFEST 格式实现,也不能保证能读取现有 Badger 数据目录。元数据编码是手动实现的,缺乏充分对拍。因此,若计划使用 badger-cj,应从零开始建库,避免接管已有数据,否则可能遇到难以排查的格式错误。

无锁跳表的实现代价:读性能约慢 3 倍,换取数据安全

由于仓颉没有 unsafe.Pointer,作者用数组下标偏移量加原子数组替代指针 CAS。改造后,写性能因真 CAS 的内存序代价而变慢,读性能明显下降约 3 倍。但改造前所谓的“快”是因为没有真正同步,存在数据损坏风险。文章强调,数据安全是 0 或 1 的问题,性能再好也不能以损坏数据为代价。这提醒读者,在评估存储引擎时,正确性优先于基准数字。

使用注意:事务粒度显著影响写入性能

文章给出一个实用提示:批量写的事务粒度对性能影响很大。同一写路径下,把单键事务改成 8 个 key 一个事务,每 key 成本降到约 1/3;64 个 key 一个事务可降到约 1/4。原因是写路径的同步成本无法靠框架内部批量提交摊薄,每个 commit 必须被单独唤醒。因此,真正的优化杠杆在用户侧的事务粒度,而非框架内部。

生态定位:作为多个仓颉数据库的存储基础

badger-cj 并非孤立库,而是 storm-cj、sunku-cj、tsdb-cj、holt-cj 等项目的底层存储引擎。这些项目在同一套存储原语上叠加不同的数据模型,分别面向 JSON 文档、Redis 兼容 KV、时序和图数据库。这表明 badger-cj 的稳定性和正确性会影响上层多个数据库,其移植质量具有生态级意义。

❓

Q&A

badger-cj 是什么?它和 Go 的 BadgerDB 是什么关系?

badger-cj 是用仓颉语言移植 Go 版 BadgerDB v4 实现的嵌入式 KV 存储引擎,采用 LSM-Tree 架构。它兼容 Go 版本的 API 语义,数据格式理论上兼容但未经跨语言测试验证,且不支持直接读取 Go BadgerDB 的数据目录,需要从零建库。

仓颉没有 unsafe.Pointer,怎么实现无锁跳表?

放弃裸指针,改用数组下标偏移量:所有节点集中存储,节点间记录对方在存储块中的偏移,CAS 变成对偏移量的比较交换(compareAndSwap)。插入时用标准 CAS 循环重试,失败就重新查找插入位置。存储按分段扩展,避免预分配大数组。

仓颉里用 Array(n, repeat: AtomicUInt64(0)) 初始化原子数组有什么坑?

对引用类型,repeat 是值拷贝,所有元素会指向同一个原子对象,并发下必然出错。必须逐个覆盖为独立对象,例如用循环 arr[i] = AtomicUInt64(0) 重新赋值。这类 bug 单线程测试全绿,一上并发就数据错乱。

改用真正的 CAS 后,badger-cj 的性能代价是什么?

写性能变慢,因为真 CAS 要付内存序代价;读性能明显变慢,约 3 倍。改造前那条路径“快”是因为根本没做真正的同步。但数据安全是 0 或 1 的问题,所以选择正确性优先。

badger-cj 支持哪些功能?

支持事务(含 SSI 可串行化快照隔离 + 冲突检测)、前向/反向迭代器与前缀过滤、快照、MVCC、WAL 崩溃恢复、SSTable 前缀压缩 + Block 级 CRC、Value Log 分离存储与 GC、MANIFEST、完整 L0→L6 Compaction、BloomFilter(默认 1% 误判率)、Snappy 压缩、Stream API、全量/增量 Backup/Restore。

怎么用 badger-cj?能举个例子吗?

引入依赖 "badgercj" = "1.6.21",然后:let opt = Config(dir: "/tmp/mydb"); let db = Badger.open(opt); db.update({ txn => txn.set("name".toArray(), "badger-cj".toArray()) }); let got = db.get("name".toArray()); if (let Some(vs) <- got) { println(String.fromUtf8(vs.value)) }; db.close()。还支持链式配置、纯内存模式、TTL、批量写等。

批量写时事务粒度对性能有什么影响?

同一写路径下,把单键事务改成 8 个 key 一个事务,每 key 成本降到约 1/3;64 个 key 一个事务可降到约 1/4。原因是写路径的同步成本没法靠框架内部批量提交摊薄,真正的杠杆在用户侧的事务粒度。

badger-cj 在生态中扮演什么角色?

它是底层存储地基,支撑多个项目:storm-cj(嵌入式 JSON 文档数据库,纯仓颉零 FFI)、sunku-cj(Redis 兼容的本地 KV 数据库)、tsdb-cj(嵌入式时序数据库)、holt-cj(嵌入式图数据库)。它们在同一套存储原语上叠加不同数据模型。

🏷️

标签

➡️

继续阅读