10225501448 李度 10225101546 陈胤遒 10215501422 高宇菲
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 

7.0 KiB

1. 项目概述

leveldb中的存储原本只支持简单的字节序列,在这个项目中我们对其功能进行拓展,使其可以包含多个字段,并通过这些字段实现类似数据库列查询的功能。但如果仅通过字段查找数据,需要对整个数据库的遍历,不够高效,因此还要新增二级索引,提高对特定字段的查询效率。

本文档涵盖的设计内容只是最初的设想,实现过程中大概率会进行调整甚至重构。各部分也将在项目落实的过程中进行补充完善。

2. 功能设计

2.1 字段设计

设计目标:对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中。

2.2 二级索引

设计目标:对某个字段或属性建立索引,提高对该字段的查询效率。需考虑索引的创建、删除、维护。

实现思路: 这一部分的难点主要在于,索引数据与原数据的存储需要进行隔离,不同操作之间存在同步与异步问题,还需要考虑两种数据间的一致性。为了使设计简洁化,避免不同模块耦合带来潜在的问题,我们的设计如下:

  1. 总体上,我们对两种数据分别创建一个db类的对象kvDb, indexDb。对外的接口类FieldDb包含了这两个对象,提供原先的leveldb各种接口,以及新功能,并在这一层完成两个对象的管理。
  2. kvDb与原先的存储一致,indexDb中key是原key与索引字段的联合编码,value是空(详见数据结构设计),并且lab1中新增的接口也不会被调用到。
  3. 在open新的fieldDb时,会创建这两个对象并open他们。fieldDb会维护一个字符串数组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得到编码后的数据信息
        //解码返回
    }
}
  1. 一个创建/删除索引任务开始时,首先锁住写锁,再对kvDb调用FindKvsByField(类似lab1的FindKeysByField但也要返回field对应的值),对返回的数组中每个元素一一与字段名编码成新key,合并在一个writebatch中写入日志,再调用indexDb中的put或delete。当indexdb把所有操作执行完后,在fieldWithIndex中添加/删除这个fieldName,使得用户可以针对这个字段进行索引读,最后唤醒下一个任务,没有则释放写锁。
  2. fieldDb也提供了put, get,getFields, putFields,delete接口。对前三者,简单调用kvDb中的对应接口(不涉及索引)。
    putFields,先判断是否有fieldWithIndex中有的字段,如果有,并对kvDb调用一个(多个)put但在写日志时一并加上索引日志写入
    delete逻辑一致。
  3. 针对索引的日志:为了保证两个数据库间的一致性,由kvDb的日志模块统一管理。这其中包含了两种chunk(kv写入和索引写入),在恢复时需要分别解析,决定往哪一个数据库中写入。索引写入的时机在4、5中的加粗部分,如何编码还有待设计。也就是说,indexDb本身的日志模块不再起到作用,项目后期可以修改关闭这一部分。
  4. 对两个数据库的其他部分,理论上每个数据库内部的其他模块不会互相影响。

3. 数据结构设计

具体设计模块化,实现时再具体考虑。
indexDb的kv编码:暂时考虑助教文档那种
区分日志中kv部分和index部分:思路是在writebatch中某个地方加个标识区分,每一类的编码与各自的key编码类似细节有待完成

4. 接口/函数设计

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前先写日志
}

类内部实现的功能函数,具体实现过程中再抽象。

5. 功能测试

  1. 基本的每个接口函数调用。
  2. 创建/删除索引时的并发读写、并发创/删。
  3. 数据库的恢复检查。
  4. 性能测试。

6. 可能的挑战与解决方案

已想到的部分在之前已阐述,其余待发现。
mark一些原代码(db中)的修改点:recover时日志解析修改,write时的日志写入可能要合并索引的写入。

7. 分工与进度安排

功能 完成日期 分工
value序列化 11.19 李度
fieldDb接口 11.25 陈胤遒
lab1整体+测试 11.30 高宇菲
fieldDb功能实现 12.10 李度
kvdb功能实现与原代码修改 12.10 陈胤遒
整体系统整合+测试 12.20 李度、陈胤遒、高宇菲
性能测试 12.30 高宇菲