|
|
@ -1,171 +1,123 @@ |
|
|
|
### **实验计划说明报告:基于 `embedded_secondary-index` 的 `LevelDB` 实现及实验** |
|
|
|
### **字段存储和序列化** |
|
|
|
|
|
|
|
* PDF Link : |
|
|
|
首先,我们需要将 `value` 扩展为一个字段数组,每个字段包含字段名和字段值。在 `LevelDB` 中存储时,可以将这些字段序列化为一个字符串形式(例如使用 `key=value` 的格式),然后将整个字段数组序列化为一个大的字符串。 |
|
|
|
|
|
|
|
https://gitea.shuishan.net.cn/building_data_management_systems.Xuanzhou.2024Fall.DaSE/levelDB_se_index/src/branch/main/Report/Scheme.pdf |
|
|
|
不能采用直接在字段间加入转义字符的方法。如果原始字段值中本来就包含了转义字符(例如 `\;` 或 `\=`),我们不能简单地通过单纯的转义机制来处理,因为这会导致循环转义的问题。 |
|
|
|
|
|
|
|
------ |
|
|
|
|
|
|
|
#### **1. 实验背景** |
|
|
|
|
|
|
|
LevelDB 是一个高性能的持久化键值存储引擎,提供简单的 `API` 用于高效的读写操作。然而,传统 `LevelDB` 仅支持基于主键的快速查询,而无法直接支持对二级属性的查询需求。在许多场景(如搜索系统或复杂索引系统)中,需要支持高效的二级索引查询。 |
|
|
|
最终方案如下: |
|
|
|
|
|
|
|
本实验计划基于 `embedded_secondary-index` 的设计扩展了 `LevelDB`,支持通过嵌入式布隆过滤器实现的二级索引查询,并引入了 Top-K 查询功能以提升二级属性查询的实用性和效率。 |
|
|
|
**二进制序列化:** 如果字段值本身可能包含复杂的字符(包括转义字符、分隔符等),可以考虑使用一种二进制序列化格式。二进制格式通常不会受到字符集和转义的困扰。 |
|
|
|
|
|
|
|
------ |
|
|
|
|
|
|
|
#### **2. 实验目标** |
|
|
|
|
|
|
|
- 实现一个支持二级索引查询的 `LevelDB` 扩展版本。 |
|
|
|
- 验证嵌入式二级索引的设计在读写性能和查询效率上的优越性。 |
|
|
|
- 测试支持二级索引查询的数据库在 Top-K 查询功能上的性能表现。 |
|
|
|
使用 C++ 标准库中的 `std::ostringstream` 和 `std::istringstream` 来序列化和反序列化数据。二进制序列化的格式是: |
|
|
|
|
|
|
|
------ |
|
|
|
1. **字段数量(4字节)** |
|
|
|
2. 每个字段的长度和内容: |
|
|
|
- **字段名长度(4字节)** |
|
|
|
- **字段名(变长,字节)** |
|
|
|
- **字段值长度(4字节)** |
|
|
|
- **字段值(变长,字节)** |
|
|
|
|
|
|
|
#### **3. 系统设计** |
|
|
|
|
|
|
|
本实验采用 **`embedded_secondary-index`** 的实现方式,将二级索引嵌入到 `LevelDB` 的原有数据结构中。以下是系统的核心设计: |
|
|
|
|
|
|
|
##### **3.1 数据结构设计** |
|
|
|
### 二级索引设计目标和实现思路 |
|
|
|
|
|
|
|
1. **`MemTable`**: |
|
|
|
- 在内存中维护主键与二级属性的数据映射关系。 |
|
|
|
- 对二级属性构建布隆过滤器以提高查询效率。 |
|
|
|
2. **`SSTable`**: |
|
|
|
- 每个 `SSTable` 包含多个数据块(存储键值对)、元数据块(记录索引信息)和布隆过滤器块(分别用于主键和二级属性的快速过滤)。 |
|
|
|
- 数据写入磁盘时,布隆过滤器被嵌入到 `SSTable` 中,避免额外的索引文件。 |
|
|
|
3. **布隆过滤器**: |
|
|
|
- 对每个数据块的二级属性计算布隆过滤器位串。 |
|
|
|
- 通过内存中加载的布隆过滤器快速筛选可能包含目标数据的块,减少磁盘 IO。 |
|
|
|
**设计目标**: |
|
|
|
本设计旨在为特定字段或属性建立索引,从而提高对该字段查询的效率。在设计中,需要考虑以下几个方面: |
|
|
|
- 索引的创建和删除 |
|
|
|
- 索引的维护 |
|
|
|
- 原数据和索引数据的隔离存储 |
|
|
|
- 数据一致性和同步问题 |
|
|
|
|
|
|
|
##### **3.2 查询算法设计** |
|
|
|
**实现思路**: |
|
|
|
为了实现这一目标,我们将数据存储和索引存储进行隔离,避免不同操作之间产生耦合问题。具体的实现思路如下: |
|
|
|
|
|
|
|
1. **Top-K 查询**: |
|
|
|
- 查询时,先通过布隆过滤器筛选出可能的 `SSTable` 和数据块。 |
|
|
|
- 使用小顶堆保存查询结果,根据 `sequence_number`(插入顺序)排序,最终返回最近的 K 条记录。 |
|
|
|
2. **层次化查询流程**: |
|
|
|
- 优先从 `MemTable` 查询; |
|
|
|
- 若未命中,则逐层遍历 `SSTable`。 |
|
|
|
### 1. **数据库对象管理** |
|
|
|
|
|
|
|
------ |
|
|
|
我们会对两种数据创建两个数据库对象:`kvDb` 用于存储原始数据,`indexDb` 用于存储索引数据。`FieldDb` 作为外部接口层,负责管理这两个数据库对象,并提供原本的 LevelDB 接口以及新增加的索引相关功能。 |
|
|
|
|
|
|
|
#### **4. 实验步骤** |
|
|
|
```cpp |
|
|
|
class FieldDb { |
|
|
|
Db *kvDb; // 原始数据存储 |
|
|
|
Db *indexDb; // 索引数据存储 |
|
|
|
std::vector<std::string> fieldWithIndex; // 存储已经创建索引的字段 |
|
|
|
std::queue<std::pair<bool, std::string>> taskQueue; // 存储创建或删除索引的任务 |
|
|
|
|
|
|
|
##### **4.1 系统实现** |
|
|
|
// 通过索引字段创建索引 |
|
|
|
bool CreateIndexOnField(const std::string& fieldName); |
|
|
|
// 删除索引 |
|
|
|
bool DeleteIndex(const std::string& fieldName); |
|
|
|
// 根据索引查询 |
|
|
|
std::vector<std::string> QueryByIndex(const std::string& fieldName); |
|
|
|
}; |
|
|
|
``` |
|
|
|
|
|
|
|
1. 修改 `LevelDB` 的源码以支持二级索引嵌入: |
|
|
|
- 更新 `SSTable` 数据块结构,增加布隆过滤器支持; |
|
|
|
- 修改 `Write` 和 `Flush` 流程,嵌入二级索引信息。 |
|
|
|
2. 扩展数据库的 `API`: |
|
|
|
- 实现二级索引的查询接口(`RangeLookUp` 和 `Top-K LookUp`)。 |
|
|
|
3. 使用 Google Test 编写单元测试,验证功能正确性。 |
|
|
|
### 2. **数据存储设计** |
|
|
|
|
|
|
|
##### **4.2 计划性能测试** |
|
|
|
- `kvDb` 保持原始数据的一致性,不涉及任何索引相关操作。 |
|
|
|
- `indexDb` 中的 key 由原始 key 和索引字段联合编码而成,value 则为空。通过该设计,我们避免了将索引数据和原数据混合存储。 |
|
|
|
|
|
|
|
1. **数据准备**: |
|
|
|
```cpp |
|
|
|
// 示例:索引数据库的存储形式 |
|
|
|
// 例如,索引字段为 "author",原始 key 为 "book123" 时,索引的 key 为 "author|book123"。 |
|
|
|
``` |
|
|
|
|
|
|
|
- 生成包含主键和二级属性的模拟数据集。 |
|
|
|
### 3. **索引的创建与删除** |
|
|
|
|
|
|
|
- 数据格式示例: |
|
|
|
每当我们需要创建或删除一个索引时,都会将相应的任务加入到 `taskQueue` 中。任务会在后台顺序执行,使用写锁来确保数据一致性。当一个任务完成后,会唤醒下一个任务继续执行。 |
|
|
|
|
|
|
|
```json |
|
|
|
{ |
|
|
|
"primary_key": "id12345", |
|
|
|
"secondary_key": "tag123", |
|
|
|
"value": "This is a test record." |
|
|
|
} |
|
|
|
``` |
|
|
|
- **创建索引**:首先对原数据(`kvDb`)中指定字段的所有记录进行提取,然后将每一条记录的索引字段与原 key 结合形成新的索引 key,并将该 key 存入 `indexDb`。完成后,将字段名添加到 `fieldWithIndex` 中。 |
|
|
|
|
|
|
|
2. **测试指标**: |
|
|
|
- **删除索引**:删除某个字段的索引时,会从 `indexDb` 中移除相应的索引记录,并从 `fieldWithIndex` 中移除该字段名。 |
|
|
|
|
|
|
|
- 数据写入性能(`QPS`)。 |
|
|
|
- 基于二级属性的查询性能: |
|
|
|
- 单次查询耗时; |
|
|
|
- 不同 Top-K 参数下的查询性能; |
|
|
|
- 对比嵌入式二级索引与传统外部索引在查询性能上的表现。 |
|
|
|
### 4. **写入日志** |
|
|
|
|
|
|
|
3. **测试工具**: |
|
|
|
计划使用 Benchmark 工具测量数据库的吞吐量与延迟。 |
|
|
|
为了保证两个数据库间的一致性,所有的索引操作(包括创建和删除)都会通过 `kvDb` 的日志模块来管理。日志中包含两种操作类型: |
|
|
|
- kv 写入操作 |
|
|
|
- 索引写入操作 |
|
|
|
|
|
|
|
在恢复数据时,需要解析日志并将数据恢复到相应的数据库中。通过统一的日志模块,我们确保了数据一致性。 |
|
|
|
|
|
|
|
### 5. **并发与同步** |
|
|
|
|
|
|
|
------ |
|
|
|
- 由于数据库的读写操作需要支持高并发,因此我们使用了写锁机制来确保在创建和删除索引时的原子性和数据一致性。 |
|
|
|
- 在任务队列中,我们使用了同步机制来控制任务的顺序执行,避免多个任务同时执行导致的数据不一致。 |
|
|
|
|
|
|
|
#### **5. 附录:系统结构图** |
|
|
|
### 6. **对外接口设计** |
|
|
|
|
|
|
|
1. 下面提供一些建议的结构图,可以清晰说明基于 **`embedded_secondary-index`** 的设计和实现,适合配合实验报告使用: |
|
|
|
`FieldDb` 对外提供了与 LevelDB 类似的接口,同时加入了索引相关功能的操作。这些接口包括: |
|
|
|
- `CreateIndexOnField`:用于创建索引 |
|
|
|
- `DeleteIndex`:用于删除索引 |
|
|
|
- `QueryByIndex`:用于查询索引 |
|
|
|
|
|
|
|
------ |
|
|
|
此外,`FieldDb` 还提供了 `put`, `get`, `delete` 等常规接口,用于操作原始数据。 |
|
|
|
|
|
|
|
### **1. 系统整体架构图** |
|
|
|
```cpp |
|
|
|
bool FieldDb::CreateIndexOnField(const std::string& fieldName) { |
|
|
|
// 锁住写锁 |
|
|
|
// 从 kvDb 获取该字段的所有记录 |
|
|
|
// 对每一条记录生成索引 key,并存入 indexDb |
|
|
|
// 在 fieldWithIndex 中记录已创建索引的字段 |
|
|
|
// 释放写锁 |
|
|
|
} |
|
|
|
|
|
|
|
**图示内容** |
|
|
|
展示 `embedded_secondary-index` 的整体设计,包括主键、二级属性的存储方式,以及布隆过滤器与 `SSTable` 的嵌入关系。 |
|
|
|
bool FieldDb::DeleteIndex(const std::string& fieldName) { |
|
|
|
// 锁住写锁 |
|
|
|
// 从 indexDb 删除该字段的所有索引记录 |
|
|
|
// 从 fieldWithIndex 中删除该字段 |
|
|
|
// 释放写锁 |
|
|
|
} |
|
|
|
|
|
|
|
**图示结构** |
|
|
|
std::vector<std::string> FieldDb::QueryByIndex(const std::string& fieldName) { |
|
|
|
// 检查该字段是否有索引 |
|
|
|
// 从 indexDb 获取索引数据并解码返回 |
|
|
|
} |
|
|
|
``` |
|
|
|
|
|
|
|
![error](./Report/png/Structure1.svg) |
|
|
|
### 7. **总结** |
|
|
|
|
|
|
|
- 要点说明: |
|
|
|
1. 二级索引与布隆过滤器紧密嵌入 `SSTable` 的元数据块中,避免外部索引文件的开销。 |
|
|
|
2. 查询时,通过布隆过滤器快速过滤非相关 `SSTable`,只访问可能的匹配块。 |
|
|
|
|
|
|
|
------ |
|
|
|
|
|
|
|
### **2. 数据写入流程图** |
|
|
|
|
|
|
|
**图示内容** |
|
|
|
描述写入数据时如何解析主键和二级属性,并更新布隆过滤器和 `SSTable` 的流程。 |
|
|
|
|
|
|
|
**图示结构** |
|
|
|
|
|
|
|
![error](./Report/png/Structure2.svg) |
|
|
|
|
|
|
|
- **要点说明**: |
|
|
|
写入过程中,自动解析主键和二级属性,实时更新布隆过滤器,确保写入操作高效完成。 |
|
|
|
|
|
|
|
------ |
|
|
|
|
|
|
|
### **3. 数据查询流程图** |
|
|
|
|
|
|
|
**图示内容** |
|
|
|
展示基于二级属性查询的具体步骤,包括布隆过滤器筛选、块访问和结果返回。 |
|
|
|
|
|
|
|
**图示结构** |
|
|
|
|
|
|
|
![error](./Report/png/Structure3.svg) |
|
|
|
|
|
|
|
- **要点说明**: |
|
|
|
布隆过滤器用于筛选目标 `SSTable`,通过小顶堆实现 Top-K 的排序与记录收集,保证查询的效率。 |
|
|
|
|
|
|
|
------ |
|
|
|
|
|
|
|
### **4. `SSTable` 布局示意图** |
|
|
|
|
|
|
|
**图示内容** |
|
|
|
展示 `SSTable` 内部如何组织主键、二级属性和布隆过滤器的布局。 |
|
|
|
|
|
|
|
**图示结构** |
|
|
|
|
|
|
|
![error](./Report/png/Structure4.svg) |
|
|
|
|
|
|
|
- **要点说明:** |
|
|
|
1. 每个 `SSTable` 包含数据块(Data Blocks)、元数据块(Meta Block)和布隆过滤器块(Bloom Filter Blocks)。 |
|
|
|
2. 二级属性的布隆过滤器和主键布隆过滤器分别存储,提供不同维度的快速索引。 |
|
|
|
|
|
|
|
------ |
|
|
|
|
|
|
|
### **5. Top-K 查询堆排序示意图** |
|
|
|
|
|
|
|
**图示内容** |
|
|
|
以小顶堆为核心,说明查询结果如何按照时间顺序(`sequence_number`)进行排序。 |
|
|
|
|
|
|
|
**图示结构** |
|
|
|
|
|
|
|
![error](./Report/png/Structure5.svg) |
|
|
|
|
|
|
|
- **要点说明**: |
|
|
|
查询过程中,维护一个固定大小的小顶堆,仅保留最近的 K 条记录,大幅提高排序效率。 |
|
|
|
|
|
|
|
------ |
|
|
|
通过将原数据和索引数据分别存储在两个独立的数据库中,并使用任务队列来顺序执行索引创建和删除操作,我们能够保证数据的隔离性、并发性和一致性。同时,通过 `kvDb` 统一管理日志,我们避免了索引操作与原数据操作之间的冲突。在设计上,我们尽量简化了模块之间的耦合,确保了系统的可扩展性和稳定性。 |
|
|
|
|