#key-value-database #key-value-store #embedded-database #cache #data-file #disk-cache

ckydb

一个简单快速、内存优先的线程安全键值嵌入式数据库,数据持久化在磁盘上

1个不稳定版本

0.0.5 2022年7月5日

#2755数据库接口

自定义许可

82KB
1.5K SLoC

ckydb

一个简单快速、内存优先的线程安全键值嵌入式数据库,数据持久化在磁盘上。

读作'ckydb',这是ckydb的Rust实现

快速开始

  • 创建一个新的项目并激活虚拟环境
cargo new ckydb_example
  • 将ckydb添加到Cargo.toml中作为依赖项
[dependencies]
ckydb = { version = "0.0.5" } # put the appropriate version. 0.1.0 is just an example
  • 将以下代码添加到src/main.rs文件
use ckydb::{connect, Controller};

fn main() {
    let mut db = connect("db", 4.0, 60.0).unwrap();
    let keys = ["hey", "hi", "yoo-hoo", "bonjour"].to_vec();
    let values = ["English", "English", "Slang", "French"].to_vec();

    // Setting the values
    println!("[Inserting key-value pairs]");
    for (k, v) in keys.clone().into_iter().zip(values) {
        let _ = db.set(k, v);
    }

    // Getting the values
    println!("[After insert]");
    for k in keys.clone() {
        let got = db.get(k).unwrap();
        println!("For key: {:?}, Got: {:?}", k, got);
    }

    // Deleting some values
    for k in &keys[2..] {
        let removed = db.delete(*k);
        println!("Removed: key: {:?}, resp: {:?}", k, removed);
    }

    for k in &keys {
        let got = db.get(*k);
        println!("[After delete: For key: {:?}, Got: {:?}", k, got);
    }

    // Deleting all values
    let cleared = db.clear();
    println!("Cleared: {:?}", cleared);

    println!("[After clear]");
    for k in &keys {
        let got = db.get(*k);
        println!("For key: {:?}, Got: {:?}", k, got);
    }
    db.close().expect("close");
}

  • 运行应用程序并观察终端
cargo run

示例

一些示例可以在/examples文件夹中找到。

cargo run --example hello_ckydb

如何运行测试

  • 克隆仓库
git clone [email protected]:sopherapps/ckydb.git
  • 进入Rust实现文件夹
cd ckydb/implementations/rs_ckydb
  • 运行测试命令
cargo test

内部机制

  • 每个键都有一个TIMESTAMP前缀,在创建时添加。这个带TIMESTAMP的键用于按顺序存储数据以便于检索。
  • 然而,用户知道的实际键保留在索引中。当ckydb初始化时,索引从索引文件(一个".idx"文件)加载到内存中。索引基本上是一个键到TIMESTAMPED-key的映射。
  • 带TIMESTAMPED-key及其值首先存储在日志文件(一个".log"文件)中。当前日志文件有一个我们称之为memtable的内存副本。
  • 当当前日志文件超过预定义的大小(默认为4MB)时,它被转换为排序数据文件(一个".cky"文件),memtable刷新,并创建一个新的日志文件。
  • 每个".cky"或".log"文件的名字是它们创建时的时间戳。请注意,".log"到"cky"的转换只是改变了文件扩展名。
  • 数据库文件夹中始终有一个".log"文件。如果在初始化时没有".log"文件,将创建一个新的。
  • 有一个内存中的排序文件列表,称为data_files,每次将".log"文件转换为".cky"时都会更新。
  • 当前日志文件(current_log_file)的名称也保存在内存中,并在创建新日志文件时更新。
  • 还有一个".del"文件,其中包含所有标记为删除的key: TIMESTAMPED-key对。
  • 默认情况下,每隔预设的时间间隔(5分钟),后台任务会删除与".del"文件中找到的key: TIMESTAMPED-key对对应的".cky"和".log"文件中的值。然后,从".del"文件中删除每个被删除的对。
  • 在首次加载时,.del文件中的任何密钥的值都应该从对应的".log"或".cky"文件中删除

操作

  • ckydb.set(key, value)

    • 在索引中搜索对应的TIMESTAMPED密钥
    • 如果密钥不存在
      • 创建一个新的TIMESTAMPED密钥,并将其用户定义的密钥添加到索引中
      • 然后将用户定义的密钥及其TIMESTAMPED密钥添加到索引文件(".idx")中
      • 然后将此TIMESTAMPED密钥及其值添加到memtable中。
      • 然后将此TIMESTAMPED密钥及其值添加到当前日志文件(".log")中
      • 检查日志文件的大小。如果日志文件超过允许的最大大小,则将其滚动到.cky文件,并创建一个新的日志文件,同时刷新memtable
    • 如果密钥存在
      • 提取其时间戳并与当前日志文件进行比较,以查看它是否晚于当前日志文件
      • 如果是晚于或等于,则更新memtable和当前日志文件
      • 否则,将时间戳与缓存中的"start"和"stop"进行比较,以查看它是否在缓存中
      • 如果它在缓存中,则更新缓存数据和对应的数据文件
      • 否则,在data_files中找到包含时间戳的数据文件。这是通过在升序排序的列表中找到时间戳存在时的两个数据文件来完成的。左侧的文件包含时间戳。
        • 然后从数据文件中提取键值对,并将新的键值对插入其中
        • 然后将新数据加载到缓存中
        • 然后将新数据加载到数据文件中
    • 如果在这些步骤中的任何步骤发生错误,则将前面的步骤反转,并在调用中返回/抛出/引发错误
  • ckydb.delete(key)

    • 从内存索引中删除其key: TIMESTAMPED-key对。
    • 从".idx"文件中删除其key: TIMESTAMPED-key
    • 将其key: TIMESTAMPED-key添加到".del"文件中
    • 如果在这些步骤中的任何步骤发生错误,则将前面的步骤反转,并在调用中返回/抛出/引发错误
  • ckydb.get(key)

    • 在索引中搜索对应的TIMESTAMPED密钥
    • 如果密钥不存在,则抛出/引发/返回NotFoundError。
    • 如果密钥存在,则提取其TIMESTAMP并检查它是否大于当前日志文件的名字。
    • 如果此TIMESTAMP较晚,则从内存中的memtable快速获取其值。如果由于某种疯狂的原因,它不存在那里,则抛出/引发/返回CorruptedDataError。
    • 如果此TIMESTAMP早于当前日志文件的名字,则将TIMESTAMP与内存中的cache范围进行比较,如果它落在其中,则从cache获取其值。如果由于某种原因找不到值,则抛出/引发/返回CorruptedDataError
    • 否则,如果文件名早于TIMESTAMP但与其相邻的右侧文件在内存中排序的data_files列表中晚于TIMESTAMP的".cky"文件被加载到内存中的cache中,其范围被设置为介于两个".cky"文件名之间的范围。
    • 然后从cache的数据中获取值。如果由于某种原因没有找到,则抛出/引发/返回CorruptedDataError。
  • ckydb.clear()

    • memtable被重置。
    • cache被重置。
    • 内存中的index被重置。
    • 内存中的data_files被重置。
    • 数据库文件夹中的所有文件都被删除。
    • 创建一个新的".log"文件。

文件格式

  • .idx索引文件的文件格式是"key<key_value_separator>TIMESTAMPED-key",通过唯一标记(例如"{&*/%}")和键值分隔符(例如"[><?&(^#]")分隔。
goat[><?&(^#]1655304770518678-goat{&*/%}hen[><?&(^#]1655304670510698-hen{&*/%}pig[><?&(^#]1655304770534578-pig{&*/%}fish[><?&(^#]1655303775538278-fish$%#@*&^&
  • .del文件的文件格式是"TIMESTAMPED-key",通过唯一标记(例如"{&*/%}")分隔。
1655304770518678-goat{&*/%}1655304670510698-hen{&*/%}1655304770534578-pig{&*/%}1655303775538278-fish$%#@*&^&
  • .log和.cky文件的文件格式是"TIMESTAMPED-key<key_value_separator>value",通过唯一标记(例如"{&*/%}")和类似"[><?&(^#]"的键值分隔符分隔。
1655304770518678-goat[><?&(^#]678 months{&*/%}1655304670510698-hen[><?&(^#]567 months{&*/%}1655304770534578-pig[><?&(^#]70 months{&*/%}1655303775538278-fish[><?&(^#]8990 months$%#@*&^&

注意:可以启用配置以在用户定义的任何键或值中转义"标记",以避免奇怪的错误。然而,转义很昂贵,因此默认情况下关闭。

致谢

  • 没有上帝,我们无能为力(约翰福音15:5)。赞美祂。
  • 其中一些想法改编自leveldb。谢谢。

许可证

版权所有(c)2022 Martin Ahindura。所有实现均根据MIT许可证授权。

无运行时依赖