姚凯文 姜嘉琪
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
姚凯文 aa4044fd8f 更新 'README.md' 3週間前
Report 上传文件至 'Report' 删除 1ヶ月前
benchmarks init the repo with base 删除 1ヶ月前
cmake init the repo with base 删除 1ヶ月前
db init the repo with base 删除 1ヶ月前
doc init the repo with base 删除 1ヶ月前
helpers/memenv init the repo with base 删除 1ヶ月前
include/leveldb init the repo with base 删除 1ヶ月前
issues init the repo with base 删除 1ヶ月前
port init the repo with base 删除 1ヶ月前
table init the repo with base 删除 1ヶ月前
test init the repo with base 删除 1ヶ月前
third_party init the repo with base 删除 1ヶ月前
util init the repo with base 删除 1ヶ月前
.clang-format init the repo with base 1ヶ月前
.gitignore init the repo with base 1ヶ月前
.gitmodules init the repo with base 1ヶ月前
AUTHORS init the repo with base 1ヶ月前
CMakeLists.txt init the repo with base 1ヶ月前
CONTRIBUTING.md init the repo with base 1ヶ月前
LICENSE init the repo with base 1ヶ月前
NEWS init the repo with base 1ヶ月前
README.md 更新 'README.md' 3週間前
TODO init the repo with base 1ヶ月前

README.md

字段存储和序列化

首先,我们需要将 value 扩展为一个字段数组,每个字段包含字段名和字段值。在 LevelDB 中存储时,可以将这些字段序列化为一个字符串形式(例如使用 key=value 的格式),然后将整个字段数组序列化为一个大的字符串。

不能采用直接在字段间加入转义字符的方法。如果原始字段值中本来就包含了转义字符(例如 \;\=),我们不能简单地通过单纯的转义机制来处理,因为这会导致循环转义的问题。

最终方案如下:

二进制序列化: 如果字段值本身可能包含复杂的字符(包括转义字符、分隔符等),可以考虑使用一种二进制序列化格式。二进制格式通常不会受到字符集和转义的困扰。

使用 C++ 标准库中的 std::ostringstreamstd::istringstream 来序列化和反序列化数据。二进制序列化的格式是:

  1. 字段数量(4字节)
  2. 每个字段的长度和内容:
    • 字段名长度(4字节)
    • 字段名(变长,字节)
    • 字段值长度(4字节)
    • 字段值(变长,字节)

二级索引设计目标和实现思路

设计目标: 本设计旨在为特定字段或属性建立索引,从而提高对该字段查询的效率。在设计中,需要考虑以下几个方面:

  • 索引的创建和删除
  • 索引的维护
  • 原数据和索引数据的隔离存储
  • 数据一致性和同步问题

实现思路: 为了实现这一目标,我们将数据存储和索引存储进行隔离,避免不同操作之间产生耦合问题。具体的实现思路如下:

1. 数据库对象管理

我们会对两种数据创建两个数据库对象:kvDb 用于存储原始数据,indexDb 用于存储索引数据。FieldDb 作为外部接口层,负责管理这两个数据库对象,并提供原本的 LevelDB 接口以及新增加的索引相关功能。

class FieldDb {
    Db *kvDb;               // 原始数据存储
    Db *indexDb;            // 索引数据存储
    std::vector<std::string> fieldWithIndex;  // 存储已经创建索引的字段
    std::queue<std::pair<bool, std::string>> taskQueue;  // 存储创建或删除索引的任务

    // 通过索引字段创建索引
    bool CreateIndexOnField(const std::string& fieldName);
    // 删除索引
    bool DeleteIndex(const std::string& fieldName);
    // 根据索引查询
    std::vector<std::string> QueryByIndex(const std::string& fieldName);
};

2. 数据存储设计

  • kvDb 保持原始数据的一致性,不涉及任何索引相关操作。
  • indexDb 中的 key 由原始 key 和索引字段联合编码而成,value 则为空。通过该设计,我们避免了将索引数据和原数据混合存储。
// 示例:索引数据库的存储形式
// 例如,索引字段为 "author",原始 key 为 "book123" 时,索引的 key 为 "author|book123"。

3. 索引的创建与删除

每当我们需要创建或删除一个索引时,都会将相应的任务加入到 taskQueue 中。任务会在后台顺序执行,使用写锁来确保数据一致性。当一个任务完成后,会唤醒下一个任务继续执行。

  • 创建索引:首先对原数据(kvDb)中指定字段的所有记录进行提取,然后将每一条记录的索引字段与原 key 结合形成新的索引 key,并将该 key 存入 indexDb。完成后,将字段名添加到 fieldWithIndex 中。

  • 删除索引:删除某个字段的索引时,会从 indexDb 中移除相应的索引记录,并从 fieldWithIndex 中移除该字段名。

4. 写入日志

为了保证两个数据库间的一致性,所有的索引操作(包括创建和删除)都会通过 kvDb 的日志模块来管理。日志中包含两种操作类型:

  • kv 写入操作
  • 索引写入操作

在恢复数据时,需要解析日志并将数据恢复到相应的数据库中。通过统一的日志模块,我们确保了数据一致性。

5. 并发与同步

  • 由于数据库的读写操作需要支持高并发,因此我们使用了写锁机制来确保在创建和删除索引时的原子性和数据一致性。
  • 在任务队列中,我们使用了同步机制来控制任务的顺序执行,避免多个任务同时执行导致的数据不一致。

6. 对外接口设计

FieldDb 对外提供了与 LevelDB 类似的接口,同时加入了索引相关功能的操作。这些接口包括:

  • CreateIndexOnField:用于创建索引
  • DeleteIndex:用于删除索引
  • QueryByIndex:用于查询索引

此外,FieldDb 还提供了 put, get, delete 等常规接口,用于操作原始数据。

bool FieldDb::CreateIndexOnField(const std::string& fieldName) {
    // 锁住写锁
    // 从 kvDb 获取该字段的所有记录
    // 对每一条记录生成索引 key,并存入 indexDb
    // 在 fieldWithIndex 中记录已创建索引的字段
    // 释放写锁
}

bool FieldDb::DeleteIndex(const std::string& fieldName) {
    // 锁住写锁
    // 从 indexDb 删除该字段的所有索引记录
    // 从 fieldWithIndex 中删除该字段
    // 释放写锁
}

std::vector<std::string> FieldDb::QueryByIndex(const std::string& fieldName) {
    // 检查该字段是否有索引
    // 从 indexDb 获取索引数据并解码返回
}

7. 总结

通过将原数据和索引数据分别存储在两个独立的数据库中,并使用任务队列来顺序执行索引创建和删除操作,我们能够保证数据的隔离性、并发性和一致性。同时,通过 kvDb 统一管理日志,我们避免了索引操作与原数据操作之间的冲突。在设计上,我们尽量简化了模块之间的耦合,确保了系统的可扩展性和稳定性。