关于KV数据库leveldb的介绍,网上已经太多了,这里只是本身再学习源码过程当中,整理的笔记,磁盘存储和内存存储的结构用了伪代码表示出来了,首先是内存中存储结构,而后是log文件存储结构和磁盘数据sst文件存储结构。mysql
MemTable底层是用skiplist(跳跃表)进行存储, 数据所有存储在内存中, 具体结构设计以下:sql
class MemTable { enum ValueType { kTypeDeletion = 0x0, /*正常标记*/ kTypeValue = 0x1 /*已删除标记*/ }; /*跳跃表中存储的实体信息*/ struct Entity { /*key长度*/ key_size; /*key数据*/ key_bytes; /*标识是否删除ValueType中一个*/ type; /*value长度*/ value_size; /*value数据*/ value_bytes; }; SkipList<Entity*, KeyComparator> table_; }
Log文件存储在磁盘上, 用于数据恢复使用, 写入数据前先写入log文件, 与mysql方式相似, 写入的实体格式Entity以下, 写入块以32KB为单位, 若是一个块空间有足够空间容纳新写入的Entity, 则直接写入, 并将记录类型type置为KFullType; 若是没法完整写入, 则写入Entity开始部分的块类型为kFirstType, 写入中间部分块类型为kMiddleType, 写入最后部分块类型为kLastType. 一个块内可能写入多个Entity, 一个Entity可能写入多个块中,块方式写入以后,在读取日志进行恢复数据时, 变得很方便, 直接按块大小读取, 加快访问速度.数据库
Entity实体结构示意图 | HEADER | key, value对 | |--------------|------------|----------|------------|-----------|-------------|------------|-------------|......| | checksum | length | type | val_type | key_size | key_bytes | val_size | val_bytes |......|
struct Entity { /*主要标识一个Entity是否在当前块中的*/ enum RecordType { kZeroType = 0, kFullType = 1, kFirstType = 2, kMiddleType = 3, kLastType = 4 }; struct Header { /*32位crc校验码, 对写入数据校验*/ int4 checksum; /*日志块长度*/ int2 length; /*RecordType中一种*/ int1 type; }; /*键值对能够批量写入, 所以一次可能有N个键值对*/ struct KeyValuePair { /*标识值被删除,仍是正常状态*/ val_type; /*键长度*/ key_size; /*键内容*/ key_bytes; /*key对应的值长度*/ val_size; /*key对应的值内容*/ val_bytes; }[N]; };
SST文件存储最终落入磁盘的数据, 数据是只读的, 数据默认是压缩存储. 下面是伪代码的存储数据结构, 文件依次存储数据块, 数据块索引, 过滤器,文件尾。数组
1.key共享存储:block中存储的一条条记录, 每条记录中一个KV对, 假如存储key为user1, user2, user3, 则首先存入user1, shared_size值为0,non_shared_size为0,后面依次存入value长度和值,存入user2时,因为user1和user2是共享user部分,所以user2中shared_size为4,non_shared_size为1,后面依次存入value长度,non_shared值也就是1,value值,后面存入user3时,同理,shared_size为4,non_shared_size为1,后面依次存入value长度,non_shared值也就是3,value值,因为SST中存储的key值都是有序的,key若是类似的,这种存储能够节省不少空间。缓存
2.重启点:block最后存储了一个重启点数组,默认间隔16条记录插入一个重启点,插入重启点位置的key是一个完整的key,没有共享字段,插入重启点是为了加快block中查找key的速度,block中进行查找时,咱们首先在重启点数组中利用二分查找,找到距离查找小于key最近的重启点,而后顺着重启点依次查找,直到找到key,或者没有找到。数据结构
3.过滤器BlockMeta:为了减小操做磁盘次数,leveldb加入了过滤器,建立db的时候能够指定过滤器,leveldb实现了布隆过滤器供使用。BlcokMeta中每条记录对应一个BlockData的过滤器,查找时,若是过滤器中没有找到则直接返回,不然在BlockData中进行查找。性能
4.块索引BlcokIndex:存储块对应的索引,其中key为前一个块中最后一个key和后一个块中第一个key之间的一个值,好比block1中最后key为user1, block2中最小key为user5,索引的key值为user2;若是block2中最小的key为user2,则索引的key只能为user1。学习
/*M个数据块, 存储具体数据*/ struct Block { /*每一个数据块中存储N条记录*/ struct Record { /*Key中共享字段长度*/ size_t shared_size; /*Key中独有字段长度*/ size_t non_shared_size; /*Key对应的Value字段长度*/ size_t value_size; /*Key中独有字段内容*/ byte non_shared_bytes[non_shared_size]; /*Key对应Value字段内容*/ byte value_bytes[value_size]; }[N]; /*重启点数组方式保存, 长度和重启点都已固定大小存储,值表示重启点距离block开始位置的偏移量*/ uint32 restarts[restart_num]; /*重启点个数*/ uint32 restart_num; /*标识是否进行压缩*/ byte type; /*数据校验码, 若是压缩数据, 则校验码是数据压缩以后的校验码, 校验数据的完整性*/ uint32 crc; }; class table { /*存放数据的数据块*/ Block BlocKData[N]; /*存放Data数据块对应的索引, 每一个记录对应一个Block, 其中value存储的是块相对于文件头的偏移量*/ Block BlockIndex; /*存储过滤规则,默认没有,通常使用布隆过滤器,可能为空,里面每条记录对应一个block生成的过滤器*/ Block BlcokMeta; struct Footer { /*过滤器数据相对于文件头的偏移量*/ uint64 metaindex_offset; /*过滤器数据长度*/ uint64 metaindex_size; /*BlockIndex数据相对于文件头的偏移量*/ uint64 blockindex_offset; /*BlockIndex数据长度*/ uint64 blockindex_size; /*文件尾部填充的魔数*/ uint64 magic_number; } };
leveldb中读性能比不了内存数据库,因为分层存储,为了尽可能减小磁盘操做,实现了一套缓存机制,缓存以查找的key做为hash,对应值为key所在的table指针。缓存作了两级,外层是是固定大小为16的hash表,hash表中每条记录中对应一个随元素数量增加的hash表, 两层hash一方面能够减小hash碰撞次数, 另外一方面rehash时减小copy内存的长度, 内层的缓存操做是须要加锁的, 分层以后减小锁的竞争次数.ui
leveldb磁盘存储的文件分为level-0到level-6, 每一层中有若干个文件, 全部文件长度和最大限制以下, 默认存储总量10TB左右. 其中level-0中默认最大文件个数限制为4设计
level-0 10M
level-1 100M
level-2 1000M
level-3 10000M
level-4 100000M
level-5 1000000M
level-6 10000000M
leveldb数据存储分为两部份内存中MemTable和磁盘上Table文件, 合并的过程就是将内存数据合并入磁盘中, 磁盘中低层数据向高层合并.向数据库中写入一个key时, 首先将Key和Value值写入log文件中, 而后检查MemTable中数据大小, 若是大于临界值(默认4M), 则从新建立MemTable, 将Key插入, 原来的MemTable则保存在Imm中, 只用于查询使用, 检查是否须要进行合并操做, 流程以下.
1.判断Imm是否为空, Imm非空先遍历Imm中数据依次写入sst文件中, 而后挑选合适的level进行合并, 从level-0开始遍历到level-6, 挑选过程以下, 挑选结束后直接将生成的sst文件添加进挑选的level.
a) 因为level-0不一样文件中存在重叠key, 所以单独判断Imm中key和level0中key是否重叠, 重叠则直接将Imm中数据合并入level-0中, 不然继续向下;
b) 假设遍历到level-1层发现key和Imm中key有重叠, 则直接将Imm合并入level-0层; 不然继续向下.
c) 假如遍历到level-1层发现key和Imm中key没有重叠, 可是level-2层中key与Imm中key重叠文件长度大于kMaxGrandParentOverlapBytes(默认20M), 则直接合并入level-0层, 避免level-1和level-2层重叠太多,后面产生过多的合并操做. 不然level+1后继续步骤b进行遍历.
2.Imm为空时, 则须要合并磁盘中的数据是否须要合并, 每次修改VersionSet集合中的文件时,都会对每层数据评估得出一个score, 评估出下次最合适合并的level,
level-0层 : score=文件个数/文件最大总数.
level-1~6层, score=文件总长度/本层文件最大长度.
根据获取的score值, 得出本次最须要合并的level, 若是level中文件在level+1中key没有重叠, 则直接将level中文件移除, 并添加到level+1中; level和level+1中key存在重叠, 则须要使用合并迭代器, 包含了level和level+1层须要合并的文件迭代器(可能包含多个文件), 每次合并迭代器迭代一次, 选择两层中最小的key, 插入到新的输出文件, 若是当前遍历的key已经被删除或者不是最新的, 则直接忽略. 最终生成一个新的文件, 插入到level+1层.
查找元素过程, 首先在MemTable中查找, 找到则返回, 不然在Imm中查找, 找到则返回, 不然继续开始在level0~6中进行查找, 首先在每一层中使用二分查找key所在的文件, 文件找到以后, 经过快索引二分查找key所在的块, 经过块中的过滤器(通常是布隆过滤), 匹配key值是否存在, 不存在直接返回查找不到, 不然经过重启点二分查找key所在的记录, 从而定位key是否存在, 存在返回key对应的value, 不然返回查找不到.
leveldb添加元素,只须要将元素添加进MemTable中便可, 添加元素时会生成一个内部key, 包含是否删除元素标志和惟一的序列号, 经过删除标志肯定是否为删除元素, 经过序列号能够肯定元素是否为最新元素, 进行合并操做时能够判断元素状态. leveldb删除元素时并不会对原来的元素进行修改移除, 只是插入一个设置删除标志位的新元素, 合并时会移除原来的元素, 更新操做操做同样, 一样插入一个新元素, 合并时经过序列号肯定元素是否为最新的, 从而移除老的元素.
leveldb中文件版本信息和数据库的信息都写入在MANIFEST-xxxxx文件中, 文件及其重要, 包含每一层的全部文件的描述, 日志文件序号, 插入key的序列号等信息, 丢失以后数据库基本废掉. VersionSet版本集合操做版本信息, VersionEdit保存了Version的修改信息, 以追加的方式添加在MANIFEST-xxxxx文件中, 所以MANIFEST-xxxxx文件中还保存有历史版本信息, 每次数据库重启都须要从新读取MANIFEST-xxxxx文件并将全部的版本信息读出, 并执行相应的VersionEdit, 生成当前版本Version. 每次进行合并操做都会生成一个VersionEdit, 追加到VersionSet中, 并写入MANIFEST-xxxxx文件中.