18 个版本
0.2.7 | 2021 年 12 月 6 日 |
---|---|
0.2.6 | 2021 年 12 月 2 日 |
0.2.5 | 2021 年 11 月 30 日 |
0.1.9 | 2021 年 11 月 5 日 |
0.1.6 | 2021 年 10 月 27 日 |
#2549 in 数据库接口
90KB
2K SLoC
YAKV 是一个非常简单的基于 B-Tree 的持久化键值存储,使用“传统”架构:B-Tree,缓冲区缓存,基于写时复制的 ACID 事务。 YAKV 实现了简单的 MURSIW(多读单写)访问模式,首先面向嵌入式应用程序。
它对其他模块的依赖最小,仅包含 2k 行代码。存储的 API 非常简单:用于更新信息的 put/remove
方法以及用于检索信息的 get/iter/range
范围方法。 put
执行更新或插入操作:如果键不在存储中,则将其插入,否则更新相关值。提供 put/remove
的批量版本,它接受键对的迭代器。批量更新是原子的:即所有操作都成功或失败。
可以使用双向(双端)迭代器和标准 Rust 范围进行迭代,这允许简单地指定任何具有开放/包含/排除边界的范围。迭代器不是原子的:即在迭代过程中,您可以看到存储的最新提交更新。此外,如果并发更新删除了当前迭代器位置的键,则迭代将在处理所有结果之前停止。
另一种分组操作的方法是显式启动事务。事务具有对数据库的独占访问权,可以执行更新和查找操作。事务结束时,应显式提交或中止,如果离开作用域之前未提交,则它将隐式中止。
YAKV 支持对存储的多线程访问。所有线程都可以共享单个存储实例(您应该使用 Arc 来做)。存储实现是线程安全的,所有方法都是不可变的,因此您可以从不同的线程并发调用它们。但在任何时刻只有一个线程可以更新存储,而多个线程可以读取它。读者可以在写入事务开始之前与写入者并发工作,观察数据库状态。
YAKV 需要键和值都是一个字节向量。如果您想存储其他类型,则需要先将它们序列化。如果您需要保留底层类型的自然比较顺序,那么您将不得不使用合适的序列化器。例如,对于无符号整数类型,您需要使用 大端 编码(即 key.to_be_bytes()
)来使字节向量比较的结果与两个数字的比较结果相同。对于有符号或浮点类型,编写这样的序列化器可能需要更多努力。
YAKV 使用写时复制(COW)机制来提供原子事务。为了保证持久性和提交数据的完整性,它在每次提交时都会执行两次 fsync
系统调用。这对于小型事务(仅插入一对)来说会带来显著的性能惩罚。但是,如果没有 fsync,则在程序异常终止或电源故障的情况下,数据库可能会损坏。要禁用 fsync,请在配置中将 nosync=true
设置为 true。
COW 机制允许消除写前日志(WAL)。但是,事务提交需要两次同步:我们首先需要确保数据库的新版本已持久化,然后原子地更新数据库元数据。因此,对于短事务的性能将比 WAL 差。但是,对于长事务,COW 机制将更高效,因为它避免了数据的重复复制。所以,最有效的方式是通过应用层(使用 start_transaction()
方法)进行大(几兆字节)事务的更改。但这并不总是可能的,因为更新结果可能需要其他只读事务。 YAKV 提供了两种将多个事务分组的方式
- 子事务。您可以使用
subcommit()
方法完成事务。这将触发子事务提交。其他事务将看到此事务的结果。但是,这种更改只有在事务提交后才会变得持久。回滚将取消所有子事务。 - 延迟提交。如果事务以
delay()
方法完成,那么更改在只读快照中将不可见。但是,它们对其他写或只读事务是可见的。只读事务由read_only_transaction()
方法启动,并在 MURSIW(多读单写)模式下执行。只读事务不能与写事务并发执行。因此,必须使用写时复制来提供隔离。这就是为什么带有延迟事务的 MURSIW 模式可能比带有快照和子事务的标准模式更快,允许并发执行读者和写入者。
以下是 YAKV 使用的示例
let store = Storage::open(data_path, StorageConfig::default())?;
// Simple insert/update:
let key = b"Some key".to_vec();
let value = b"Some value".to_vec();
store.put(key, value)?;
// Simple remove:
let key = b"Some key".to_vec();
store.remove(key)?;
// Bulk update
store.put_all(
&mut iter::repeat_with(|| {
let key = rand.gen::<[u8; KEY_LEN]>().to_vec();
let value = rand.gen::<[u8; VALUE_LEN]>().to_vec();
Ok((key, value))
} ).take(TRANSACTION_SIZE))?;
// Bulk delete
store.remove_all(
&mut iter::repeat_with(|| Ok(rand.gen::<[u8; 8]>().to_vec()))
.take(TRANSACTION_SIZE),
// Explicit transaction:
{
let trans = store.start_transaction();
trans.put(&1u64.to_be_bytes().to_vec(), &PAYLOAD)?;
trans.put(&2u64.to_be_bytes().to_vec(), &PAYLOAD)?;
trans.remove(&2u64.to_be_bytes().to_vec())?;
trans.commit()?;
}
// Simple lookup
let key = b"Some key".to_vec();
if let Some(value) = store.get(&key)? {
println!("key={:?}, value={:?}", &key, &value);
}
// Iterate through all records:
for entry in store.iter() {
let kv = entry?;
println!("key={:?}, value={:?}", &kv.0, &kv.1);
}
// Range iterator:
let from_key = b"AAA".to_vec();
let till_key = b"ZZZ".to_vec();
for entry in store.range(from_key..till_key) {
let kv = entry?;
println!("key={:?}, value={:?}", &kv.0, &kv.1);
}
// Backward iterartion:
let till_key = b"XYZ".to_vec();
let mut it = store.range(..=till_key);
while let Some(entry) = it.next_back() {
let kv = entry?;
println!("key={:?}, value={:?}", &kv.0, &kv.1);
}
// Close storage
store.close()?;
性能比较
以下是两个基准测试的结果(毫秒:越小越好)。
SwayDB 基准测试:使用 8 字节键和 8 字节值插入 1M 条记录。
db | seq | rnd |
---|---|---|
SwayDB | 5526 | 14849 |
LevelDB | 1107 | 7969 |
yakv | 594 | 1263 |
LMDB 基准测试:插入和读取 1M 条记录,键为 4 字节,值为 100 字节。
db | seq-write | rnd-write | seq-read | rnd-read |
---|---|---|---|---|
Chronicle | 836 | 894 | 613 | 634 |
LevelDB | 1962 | 2089 | 2223 | 2476 |
LMDB | 144 | 896 | 108 | 634 |
MapDB | 8876 | 9304 | 9676 | 9707 |
MVStore | 1328 | 1675 | 7757 | 8420 |
RocksDB | 1800 | 1850 | 1814 | 2067 |
Xodus | 674 | 13981 | 3486 | 4978 |
kv | 5626 | 7546 | 742 | 1943 |
yakv | 1079 | 1617 | 549 | 1020 |
性能依赖于事务大小(LMDB 与 YAKV 或 COW 与 WAL)。此基准测试插入 1M 条随机键(类似于 LMDB 基准测试),但插入分组在事务中(时间以毫秒计)
tx size | yakv(WAL) | yakv(COW) | LMDB |
---|---|---|---|
1000000 | 2252 | 1626 | 1853 |
100000 | 5860 | 3941 | 4709 |
10000 | 29285 | 17989 | 15718 |
1000 | 73223 | 54388 | 30050 |
100 | 131826 | 123618 | 83233 |
因此,对于大型交易,LMDB 略快;对于小型交易,带有 WAL 的 YAKV 更快;对于中等大小的交易,LMDB 的速度大约是 YAKV 的两倍。
依赖关系
~0.6–1MB
~15K SLoC