在LevelDb简介这篇博客中我们知道使用levelDB时,主要涉及到初始化数据库、写入一条数据、删除一条数据、读取一条数据等操作,下面我们分别介绍各个操作的流程。
1. 初始化数据库
leveldb::DB* db;
leveldb::Options opts;
opts.create_if_missing = true;
leveldb::Status status = leveldb::DB::Open(opts, "./testdb", &db);
打开并初始化一个LevelDb数据库的流程为:
(1)首先需要创建一个数据库的类对象DBimpl,该类继承了抽象类DB,并且重写了DB的所有接口,包括Put()、Get()、Delete()等接口,后续的所有数据库操作都由该类进行实现
(2)接着需要调用impl->Recover() 函数,从文件中恢复数据库的状态:
1)根据数据库名dbname创建数据库目录;
2)根据数据库名dbname对当前数据库加文件锁,整个对数据库的操作过程中都会加这个锁,直接释放数据库对象DBimpl;
3)如果数据库的Current文件不存在,则创建一个Manifest文件,并且生成一个VersionEdit对象,把该对象Encode成一条record写入manifest文件中,最后创建Current文件,Current文件的内容为manifest文件文件名;
4)接着读取Current文件内容,得到manifest文件名,然后打开manifest文件,从manifest中循环读取所有的record并Decode到VersionEdit中,接着把VersionEdit保存到builder的中间状态,由于这里读取了manifest文件中所有从开始到结束的所有VersionEdit的记录,因此将所有记录读取完成之后,此时builder中所保存的中间状态即为数据库的当前状态。接着再从中间状态恢复到Version中,这个Version即为上次数据库关闭时的状态。最后再预先计算好Version中下一次需要compact的level,然后把Version添加到VersionSet中;
5)查找数据库目录下的所有日志文件,找出那些从比manifest文件中恢复的og_number_还新的日志文件,然后从这些日志中恢复键值对到MemTable和VersionEdit中;
(3)如果内存表为空,根据数据库名dbname创建日志文件,保存到impl->logfile_,这里的日志文件命名时后生成一个日志文件号,使得日志文件名唯一。然后根据数据库名dbname创建内存表(MemTable)的数据结构,保存到impl->mem_;
(4)如果不能重用manifest文件,则需要新建一个manifest文件,把版本信息VersionEdit对象作为一条record 添加到manifest文件中,并且设置Current文件指向manifest文件。接着把VersionEdit保存到builder的中间状态,再从中间状态恢复到Version中,计算Version中下一次需要compact的level,然后把Version添加到VersionSet中,保存到impl->versions_;
(5)遍历所有当前目录下的所有文件,根据logNumber删除那些已经废弃的无用文件;
(6)如果满足条件的话,后台调度执行Compaction操作,把只读的内存表(Immutable Memtable)impl->imm_进行压缩并写入SSTable文件中;
2. 写入一条数据
status = db->Put(leveldb::WriteOptions(), "name", "jinhelin");
对于一个插入操作Put(Key,Value)来说,完成插入操作包含两个具体步骤:
(1)首先是将这条KV记录以顺序写的方式追加到之前介绍过的log文件末尾,因为尽管这是一个磁盘读写操作,但是文件的顺序追加写入效率是很高的,所以并不会导致写入速度的降低;
(2)第二个步骤是: 如果写入log文件成功,那么将这条KV记录插入内存中的Memtable中,前面介绍过,Memtable只是一层封装,其内部其实是一个Key有序的SkipList列表,插入一条新记录的过程也很简单,即先查找合适的插入位置,然后修改相应的链接指针将新记录插入即可。完成这一步,写入记录就算完成了,所以一个插入记录操作涉及一次磁盘文件追加写和内存SkipList插入操作,这是为何levelDb写入速度如此高效的根本原因。
LevelDb的接口没有直接支持更新操作的接口,如果需要更新某个Key的Value,你可以选择直接生猛地插入新的KV,保持Key相同,这样系统内的key对应的value就会被更新;或者你可以先删除旧的KV, 之后再插入新的KV,这样比较委婉地完成KV的更新操作。整个写入过程的代码流程图如下:
leveldb支持多线程访问db对象,这里采用生产者-消费者模式进行同步访问,为了提升性能,leveldb采用尽可能多在同一个线程内处理多个WriteBatch,所以会尝试将多个WriteBatch进行合并,根据合并后的WriteBatch分别写入到.log和MemTable中,最后通知阻塞到条件变量的生产者线程进行下一次写入。
3. 删除一条数据
status = db->Delete(leveldb::WriteOptions(), "name");
对于levelDb来说,并不存在立即删除的操作,而是与插入操作相同的,区别是,插入操作插入的是Key:Value 值,而删除操作插入的是“Key:删除标记”,并不真正去删除记录,而是后台Compaction的时候才去做真正的删除操作。levelDb的插入和删除操作就是如此简单。真正的麻烦在后面将要介绍的读取操作中。
4. 读取一条数据
std::string val;
status = db->Get(leveldb::ReadOptions(), "name", &val);
读取一条key:value的流程如下:
(1)LevelDb首先会去查看内存中的Memtable,如果Memtable中包含key及其对应的value,则返回value值即可;
(2)如果在Memtable没有读到key,则接下来到同样处于内存中的Immutable Memtable中去读取,类似地,如果读到就返回;
(3)若是在Immutable Memtable没有读到,那么只能从磁盘中的大量SSTable文件中查找。因为SSTable数量较多,而且分成多个Level,所以首先会读取manifest文件,获取所有SSTable的信息。总的读取原则是这样的:首先从属于level 0的文件中查找,如果找到则返回对应的value值,如果没有找到那么到level 1中的文件中去找,如此循环往复,直到在某层SSTable文件中找到这个key对应的value为止(或者查到最高level,查找失败,说明整个系统中不存在这个Key)。
- 问题1: 为什么是从Memtable到Immutable Memtable,再从Immutable Memtable到文件,而文件中为何是从低level到高level这么一个查询路径呢?
之所以选择这么个查询路径,是因为从信息的更新时间来说,很明显Memtable存储的是最新鲜的KV对;Immutable Memtable中存储的KV数据对的新鲜程度次之;而所有SSTable文件中的KV数据新鲜程度一定不如内存中的Memtable和Immutable Memtable的。对于SSTable文件来说,如果同时在level L和Level L+1找到同一个key,level L的信息一定比level L+1的要新。也就是说,上面列出的查找路径就是按照数据新鲜程度排列出来的,越新鲜的越先查找。
- 问题2 : 为啥要优先查找新鲜的数据呢?
这个道理不言而喻,举个例子。比如我们先往levelDb里面插入一条数据 {key="www.samecity.com" value="我们"},过了几天,samecity网站改名为:69同城,此时我们插入数据{key="www.samecity.com" value="69同城"},同样的key,不同的value;逻辑上理解好像levelDb中只有一个存储记录,即第二个记录,但是在levelDb中很可能存在两条记录,即上面的两个记录都在levelDb中存储了,此时如果用户查询key="www.samecity.com",我们当然希望找到最新的更新记录,也就是第二个记录返回,这就是为何要优先查找新鲜数据的原因。
- 问题3 : 如果同时在level L和Level L+1找到同一个key,level L的信息为什么一定比level L+1的要新?
因为Level L+1的数据是从Level L 经过Compaction后得到的,也就是说,现在的Level L+1层的SSTable数据是从原来的Level L中来的,现在的Level L比原来的Level L数据要新鲜,所以可证,现在的Level L比现在的Level L+1的数据要新鲜。
- 问题4 : SSTable文件很多,如何快速地找到key对应的value值?
在LevelDb中,level 0一直都爱搞特殊化,在level 0和其它level中查找某个key的过程是不一样的。因为level 0下的不同文件可能key的范围有重叠,某个要查询的key有可能多个文件都包含,这样的话LevelDb的策略是先找出level 0中哪些文件包含这个key(manifest文件中记载了level和对应的文件及文件里key的范围信息,LevelDb在内存中保留这种映射表), 之后按照文件的新鲜程度排序,新的文件排在前面,之后依次查找,读出key对应的value。而如果是非level 0的话,因为这个level的文件之间key是不重叠的,所以只从一个文件就可以找到key对应的value。
- 问题5 : 如果给定一个要查询的key和某个key range包含这个key的SSTable文件,那么levelDb是如何进行具体查找过程的呢?
levelDb一般会先在内存中的Cache中查找是否包含这个文件的缓存记录,如果包含,则从缓存中读取;如果不包含,则打开SSTable文件,同时将这个文件的索引部分加载到内存中并放入Cache中。 这样Cache里面就有了这个SSTable的缓存项,但是只有索引部分在内存中,之后levelDb根据索引可以定位到哪个内容Block会包含这条key,从文件中读出这个Block的内容,在根据记录一一比较,如果找到则返回结果,如果没有找到,那么说明这个level的SSTable文件并不包含这个key,所以到下一级别的SSTable中去查找。
总的来说,相对写操作,读操作处理起来要复杂很多,所以写的速度必然要远远高于读数据的速度,也就是说,LevelDb比较适合写操作多于读操作的应用场合。而如果应用是很多读操作类型的,那么顺序读取效率会比较高,因为这样大部分内容都会在缓存中找到,尽可能避免大量的随机读取操作。
参考:https://www.cnblogs.com/haippy/archive/2011/12/04/2276064.html