leveldb中的存储原本只支持简单的字节序列,在这个项目中我们对其功能进行拓展,使其可以包含多个字段,并通过这些字段实现类似数据库列查询的功能。但如果仅通过字段查找数据,需要对整个数据库的遍历,不够高效,因此还要新增二级索引,提高对特定字段的查询效率。
本文档涵盖的设计内容只是最初的设想,实现过程中大概率会进行调整甚至重构。各部分也将在项目落实的过程中进行补充完善。
设计目标:对value存储读取时进行序列化编码,使其支持字段。
实现思路:设计之初有考虑增加一些元数据(例如过滤器、字段偏移支持二分)来加速查询。但考虑到在数据库中kv的数量是十分庞大的,新加数据结构会带来巨大的空间开销。因此我们决定在这里牺牲时间换取空间,而将时间的加速放在索引中。
在这一基础上,我们对序列化进行了简单的优化:将字段名排序后,一一调用leveldb中原本的编码方法PutLengthPrefixedSlice
存入value。这样不会有额外的空间开销,而好处在于遍历一个value的字段时,如果得到的字段名比目标大,就可以提前结束遍历。
std::string SerializeValue(const FieldArray& fields){
FieldArray sortFields = fields;
std::sort(sortFields.begin(), sortFields.end(), compareByFirst);
std::string result;
for (const Field& pairs : sortFields) {
PutLengthPrefixedSlice(&result, pairs.first);
PutLengthPrefixedSlice(&result, pairs.second);
}
return result;
}
最终db类提供了新接口putFields
, getFields
,分别对传入的字段序列化后调用原来的put
, get
接口。
FindKeysByField
调用NewIterator
遍历所有数据,field名和值符合则加入返回的key中。
设计目标:对某个字段或属性建立索引,提高对该字段的查询效率。需考虑索引的创建、删除、维护。
实现思路: 这一部分的难点主要在于,索引数据与原数据的存储需要进行隔离,不同操作之间存在同步与异步问题,还需要考虑两种数据间的一致性。为了使设计简洁化,避免不同模块耦合带来潜在的问题,我们的设计如下:
kvDb, indexDb
。对外的接口类FieldDb
包含了这两个对象,提供原先的leveldb各种接口,以及新功能,并在这一层完成两个对象的管理。kvDb
与原先的存储一致,indexDb
中key是原key与索引字段的联合编码,value是空(详见数据结构设计),并且lab1中新增的接口也不会被调用到。fieldWithIndex
,记录了当前哪些字段名拥有索引。一个pair<indexStatus, fieldName>
的队列taskQueue
, 维护了创建或者删除索引的任务请求。创建或删除索引时,把任务加入队尾并休眠。一个任务完成后唤醒队首继续下一个任务(类似write机制)class FieldDb {
Db *kvDb;
Db *indexDb;
string[] fieldWithIndex;
queue<pair<bool indexStatus, std::string fieldName>> taskQueue;
// 0建 or 1删,或把pair抽象成一个类
//原db所有对外接口,下面没提就调用kvDb相应函数
bool CreateIndexOnField(const std::string& fieldName){、
//加入taskQueue并休眠/拿写锁
//kvdb->FindKvsByField得到数据信息
//编码
//kvdb->writeIndexLog写索引日志
//indexDb->put存储索引
//fieldWithIndex.insert(fieldName)
//唤醒taskQueue队首/释放写锁
}
bool DeleteIndex(std::string &fieldName); //同上
std::vector<std::string> QueryByIndex(Field &field){
//判断fieldWithIndex是否有
//indexDb->iterator得到编码后的数据信息
//解码返回
}
}
kvDb
调用FindKvsByField
(类似lab1的FindKeysByField
但也要返回field对应的值),对返回的数组中每个元素一一与字段名编码成新key,合并在一个writebatch中写入日志,再调用indexDb
中的put或delete。当indexdb
把所有操作执行完后,在fieldWithIndex
中添加/删除这个fieldName
,使得用户可以针对这个字段进行索引读,最后唤醒下一个任务,没有则释放写锁。put
, get
,getFields
, putFields
,delete
接口。对前三者,简单调用kvDb
中的对应接口(不涉及索引)。putFields
,先判断是否有fieldWithIndex
中有的字段,如果有,并对kvDb
调用一个(多个)put
,但在写日志时一并加上索引日志写入。delete
逻辑一致。kvDb
的日志模块统一管理。这其中包含了两种chunk(kv写入和索引写入),在恢复时需要分别解析,决定往哪一个数据库中写入。索引写入的时机在4、5中的加粗部分,如何编码还有待设计。也就是说,indexDb
本身的日志模块不再起到作用,项目后期可以修改关闭这一部分。具体设计模块化,实现时再具体考虑。
indexDb
的kv编码:暂时考虑助教文档那种
区分日志中kv部分和index部分:思路是在writebatch中某个地方加个标识区分,每一类的编码与各自的key编码类似细节有待完成
FieldDb
的对外接口函数之前已展示,这里补充一些子数据库需提供给FieldDb
的抽象功能(暂时想到的):
class Db{
//原有部分和lab1部分
//kvdb需要:
pair<slice key, string fieldVal> FindKvsByField(string fieldName); //搜集索引需要的数据信息
status writeIndexLog(string fieldName, pair<slice key, string fieldVal>); //向indexDb put前先写日志
}
类内部实现的功能函数,具体实现过程中再抽象。
已想到的部分在之前已阐述,其余待发现。
mark一些原代码(db中)的修改点:recover时日志解析修改,write时的日志写入可能要合并索引的写入。
功能 | 完成日期 | 分工 |
---|---|---|
value序列化 | 11.19 | 李度 |
fieldDb接口 | 11.25 | 陈胤遒 |
lab1整体+测试 | 11.30 | 高宇菲 |
fieldDb功能实现 | 12.10 | 李度 |
kvdb功能实现与原代码修改 | 12.10 | 陈胤遒 |
整体系统整合+测试 | 12.20 | 李度、陈胤遒、高宇菲 |
性能测试 | 12.30 | 高宇菲 |