leveldb源码笔记
2015-12-17 22:18
281 查看
关于KV数据库leveldb的介绍,网上已经太多了,这里只是自己再学习源码过程中,整理的笔记,磁盘存储和内存存储的结构用了伪代码表示出来了,首先是内存中存储结构,然后是log文件存储结构和磁盘数据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。
level-0 10M
level-1 100M
level-2 1000M
level-3 10000M
level-4 100000M
level-5 1000000M
level-6 10000000M
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存储格式
MemTable底层是用skiplist(跳跃表)进行存储, 数据全部存储在内存中, 具体结构设计如下: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文件存储在磁盘上, 用于数据恢复使用, 写入数据前先写入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; } ; };
SST文件存储格式
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]; } ; /*重启点数组方式保存, 长度和重启点都已固定大小存储,值表示重启点距离block开始位置的偏移量*/ uint32 restarts[restart_num]; /*重启点个数*/ uint32 restart_num; /*标识是否进行压缩*/ byte type; /*数据校验码, 如果压缩数据, 则校验码是数据压缩之后的校验码, 校验数据的完整性*/ uint32 crc; }; class table { /*存放数据的数据块*/ Block BlocKData ; /*存放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; } };
LRU缓存
leveldb中读性能比不了内存数据库,由于分层存储,为了尽量减少磁盘操作,实现了一套缓存机制,缓存以查找的key作为hash,对应值为key所在的table指针。缓存做了两级,外层是是固定大小为16的hash表,hash表中每条记录中对应一个随元素数量增长的hash表, 两层hash一方面可以减少hash碰撞次数, 另一方面rehash时减少copy内存的长度, 内层的缓存操作是需要加锁的, 分层之后减少锁的竞争次数.分层
leveldb磁盘存储的文件分为level-0到level-6, 每一层中有若干个文件, 所有文件长度和最大限制如下, 默认存储总量10TB左右. 其中level-0中默认最大文件个数限制为4level-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删除元素时并不会对原来的元素进行修改移除, 只是插入一个设置删除标志位的新元素, 合并时会移除原来的元素, 更新操作操作一样, 同样插入一个新元素, 合并时通过序列号确定元素是否为最新的, 从而移除老的元素.Version管理
leveldb中文件版本信息和数据库的信息都写入在MANIFEST-xxxxx文件中, 文件及其重要, 包含每一层的所有文件的描述, 日志文件序号, 插入key的序列号等信息, 丢失之后数据库基本废掉. VersionSet版本集合操作版本信息, VersionEdit保存了Version的修改信息, 以追加的方式添加在MANIFEST-xxxxx文件中, 因此MANIFEST-xxxxx文件中还保存有历史版本信息, 每次数据库重启都需要重新读取MANIFEST-xxxxx文件并将所有的版本信息读出, 并执行相应的VersionEdit, 生成当前版本Version. 每次进行合并操作都会生成一个VersionEdit, 追加到VersionSet中, 并写入MANIFEST-xxxxx文件中.相关文章推荐
- pkusc2015
- B-树的基本操作
- Linux下VMware虚拟机网卡不能运行在混杂模式解决办法
- 卸载3dMax2012流程
- 心上莲花次第开系列:家世渊源
- P05 (*) 反向列表中元素
- P05 (*) 反向列表中元素
- Python 2.7 学习笔记
- poj2031
- 汉诺塔 hanoi 递归
- Linux简单命令之三
- .NET CurrentCulture 区域设置代码列表
- Spring自定义属性编辑器
- Freescale IMX6 Android (7): Android启动动画死循环 Home界面不出来与pid XXX exit 可能的原因汇总
- java代码统计工具
- 一个组件注册失败引发的惨案
- android事件分发的研究
- MATLAB中匿名函数与符号函数的转换
- 第一阶段,第二阶段,第三阶段团队github更新项目地址
- 心上莲花次第开系列:流失的幸福