关于KV数据库leveldb的介绍,网上已经太多了,这里只是自己再学习源码过程中,整理的笔记,磁盘存储和内存存储的结构用了伪代码表示出来了,首先是内存中存储结构,然后是log文件存储结构和磁盘数据sst文件存储结构。

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;
}[N];
};

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];
}[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;
}
};

LRU缓存

  leveldb中读性能比不了内存数据库,由于分层存储,为了尽量减少磁盘操作,实现了一套缓存机制,缓存以查找的key作为hash,对应值为key所在的table指针。缓存做了两级,外层是是固定大小为16的hash表,hash表中每条记录中对应一个随元素数量增长的hash表, 两层hash一方面可以减少hash碰撞次数, 另一方面rehash时减少copy内存的长度, 内层的缓存操作是需要加锁的, 分层之后减少锁的竞争次数.

分层

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删除元素时并不会对原来的元素进行修改移除, 只是插入一个设置删除标志位的新元素, 合并时会移除原来的元素, 更新操作操作一样, 同样插入一个新元素, 合并时通过序列号确定元素是否为最新的, 从而移除老的元素.

Version管理

  leveldb中文件版本信息和数据库的信息都写入在MANIFEST-xxxxx文件中, 文件及其重要, 包含每一层的所有文件的描述, 日志文件序号, 插入key的序列号等信息, 丢失之后数据库基本废掉. VersionSet版本集合操作版本信息, VersionEdit保存了Version的修改信息, 以追加的方式添加在MANIFEST-xxxxx文件中, 因此MANIFEST-xxxxx文件中还保存有历史版本信息, 每次数据库重启都需要重新读取MANIFEST-xxxxx文件并将所有的版本信息读出, 并执行相应的VersionEdit, 生成当前版本Version. 每次进行合并操作都会生成一个VersionEdit, 追加到VersionSet中, 并写入MANIFEST-xxxxx文件中.

leveldb源码笔记的更多相关文章

  1. leveldb源码学习系列

    楼主从2014年7月份开始学习<>,由于书籍比较抽象,为了加深思考,同时开始了Google leveldb的源码学习,主要是想学习leveldb的设计思想和Google的C++编程规范.目 ...

  2. LevelDB源码剖析

    LevelDB的公共部件并不复杂,但为了更好的理解其各个核心模块的实现,此处挑几个关键的部件先行备忘. Arena(内存领地) Arena类用于内存管理,其存在的价值在于: 提高程序性能,减少Heap ...

  3. Zepto源码笔记(一)

    最近在研究Zepto的源码,这是第一篇分析,欢迎大家继续关注,第一次写源码笔记,希望大家多指点指点,第一篇文章由于首次分析原因不会有太多干货,希望后面的文章能成为各位大大心目中的干货. Zepto是一 ...

  4. redis源码笔记(一) —— 从redis的启动到command的分发

    本作品采用知识共享署名 4.0 国际许可协议进行许可.转载联系作者并保留声明头部与原文链接https://luzeshu.com/blog/redis1 本博客同步在http://www.cnblog ...

  5. AsyncTask源码笔记

    AsyncTask源码笔记 AsyncTask在注释中建议只用来做短时间的异步操作,也就是只有几秒的操作:如果是长时间的操作,建议还是使用java.util.concurrent包中的工具类,例如Ex ...

  6. Java Arrays 源码 笔记

    Arrays.java是Java中用来操作数组的类.使用这个工具类可以减少平常很多的工作量.了解其实现,可以避免一些错误的用法. 它提供的操作包括: 排序 sort 查找 binarySearch() ...

  7. Tomcat8源码笔记(八)明白Tomcat怎么部署webapps下项目

    以前没想过这么个问题:Tomcat怎么处理webapps下项目,并且我访问浏览器ip: port/项目名/请求路径,以SSM为例,Tomcat怎么就能将请求找到项目呢,项目还是个文件夹类型的? Tom ...

  8. Tomcat8源码笔记(七)组件启动Server Service Engine Host启动

    一.Tomcat启动的入口 Tomcat初始化简单流程前面博客介绍了一遍,组件除了StandardHost都有博客,欢迎大家指文中错误.Tomcat启动类是Bootstrap,而启动容器启动入口位于 ...

  9. Tomcat8源码笔记(六)连接器Connector分析

    根据 Tomcat8源码笔记(五)组件Container分析 前文分析,StandardService的初始化重心由 StandardEngine转移到了Connector的初始化,本篇记录下Conn ...

随机推荐

  1. POJ2965The Pilots Brothers' refrigerator

    http://poj.org/problem?id=2965 这个题的话,一开始也不会做,旁边的人说用BFS,后来去网上看了众大神的思路,瞬间觉得用BFS挺简单易:因为要让一个“+”变为“-”,只要将 ...

  2. 恢复mdf文件到数据库方法

    CREATE DATABASE crm_testdb1 ON (FILENAME = N'C:\e527051\crm_testdb\crm_testdb_20121104.mdf')FOR ATTA ...

  3. 心情记录&考试总结 3.30

    并不知道现在要干什么,本人像是一只大颓狗 Em..怎么说呢,今天考完了一场奇怪的试 准确的说,画风很不正常的试 第一题集体爆零 第二题暴力20分 第三题暴力40分,乱搞有加成 改题的话, 第一题有奇怪 ...

  4. Python中Lambda, filter, reduce and map 的区别

    Lambda, filter, reduce and map Lambda Operator Some like it, others hate it and many are afraid of t ...

  5. lintcode:Remove Element 删除元素

    题目: 删除元素 给定一个数组和一个值,在原地删除与值相同的数字,返回新数组的长度. 元素的顺序可以改变,并且对新的数组不会有影响.  样例 给出一个数组 [0,4,4,0,0,2,4,4],和值 4 ...

  6. mysql字段的适当冗余有利于提高查询速度

    CREATE TABLE `comment` (  `c_id` int(11) NOT NULL auto_increment COMMENT '评论ID',  `u_id` int(11) NOT ...

  7. iOS开发--数组

    1.sortedArrayUsingSelector (按Key值大小对NSDictionary排序) NSMutableArray *array = [NSMutableArray arrayWit ...

  8. MFC、WTL、WPF、wxWidgets、Qt、GTK、Cocoa、VCL 各有什么特点?

    WTL都算不上什么Framework,就是利用泛型特性对Win API做了层封装,设计思路也没摆脱MFC的影响,实际上用泛型做UI Framework也只能算是一次行为艺术,这个思路下继续发展就会变得 ...

  9. 让Windows蓝屏死机

    ssdt 随便一个函数入口改90就蓝了 ------------------------------------------------- program Project2; uses Windows ...

  10. UNIX相关知识

    UNIX UNIX的设计目标是小而美:希望能在任何小系统上执行,而核心只提供必不可少的一些功能,其他的则根据需要加上去.这已经成为操作系统的一种设计哲学. The Open Group持有UNIX商标 ...