Linux上leveldb的安装和使用中我们写了这么一段测试代码,内容以及输出结果如下:

#include <iostream>
#include <string>
#include <assert.h>
#include "leveldb/db.h" using namespace std; int main(void)
{ leveldb::DB *db;
leveldb::Options options;
options.create_if_missing = true; // open
leveldb::Status status = leveldb::DB::Open(options,"/tmp/testdb", &db);
assert(status.ok()); string key = "name";
string value = "chenqi"; // write
status = db->Put(leveldb::WriteOptions(), key, value);
assert(status.ok()); // read
status = db->Get(leveldb::ReadOptions(), key, &value);
assert(status.ok()); cout<<value<<endl; // delete
status = db->Delete(leveldb::WriteOptions(), key);
assert(status.ok()); status = db->Get(leveldb::ReadOptions(),key, &value);
if(!status.ok()) {
cerr<<key<<" "<<status.ToString()<<endl;
} else {
cout<<key<<"==="<<value<<endl;
} // close
delete db; return 0;
}
chenqi
name NotFound:

Leveldb的读数据入口为db文件夹下db_impl.cc文件中的DBImpl::Get函数,首先获取当前的版本号,然后依次在三个数据源memtable,immutable table,和sst表中进行查找,返回之前再判断一下是否需要启动Compact任务.

Status DBImpl::Get(const ReadOptions& options,
const Slice& key,
std::string* value) {
Status s;
MutexLock l(&mutex_);
SequenceNumber snapshot;
if (options.snapshot != NULL) {
snapshot = reinterpret_cast<const SnapshotImpl*>(options.snapshot)->number_;
} else {
//获取版本号
snapshot = versions_->LastSequence();
}
//三个查找源,memtable,immutable,sstable
MemTable* mem = mem_;
MemTable* imm = imm_;
Version* current = versions_->current();
mem->Ref();
if (imm != NULL) imm->Ref();
current->Ref(); bool have_stat_update = false;
Version::GetStats stats; // Unlock while reading from files and memtables
{
mutex_.Unlock();
// First look in the memtable, then in the immutable memtable (if any).
LookupKey lkey(key, snapshot);
if (mem->Get(lkey, value, &s)) { //在memtable中查找
// Done
} else if (imm != NULL && imm->Get(lkey, value, &s)) {//在imutable中查找
// Done
} else {
s = current->Get(options, lkey, value, &stats); //在磁盘文件中查找,当前Version
have_stat_update = true;
}
mutex_.Lock();
}
//判断是否需要调度Compact
if (have_stat_update && current->UpdateStats(stats)) {
MaybeScheduleCompaction();
}
mem->Unref();
if (imm != NULL) imm->Unref();
current->Unref();
return s;
}

首先,按照leveldb代码的惯例线上锁,然后生成一个SequenceNumber作为标记, 后续不管线程会不会被切出去, 结果都要相当于在这个时间点瞬间完成,memtable、immemtable以及Version都由于采用了引用计数, 因此要Ref().快照建立完了, 接下来的操作只会有单纯的读, 可以把锁暂时释放.查询的顺序是先找memtable, 再immemtable, 最后是SSTable.这里调用了db文件夹下dbformat.cc中的LookupKey::LookupKey, 源码内容如下:

LookupKey::LookupKey(const Slice& user_key, SequenceNumber s) {
size_t usize = user_key.size();
size_t needed = usize + ; // A conservative estimate
char* dst;
if (needed <= sizeof(space_)) {
dst = space_;
} else {
dst = new char[needed];
}
start_ = dst;
dst = EncodeVarint32(dst, usize + );
kstart_ = dst;
memcpy(dst, user_key.data(), usize);
dst += usize;
EncodeFixed64(dst, PackSequenceAndType(s, kValueTypeForSeek));
dst += ;
end_ = dst;
}

这个类主要的功能是把输入的key转换成用于查询的key. 比如key是"Sherry", 实际在数据库中的表达可能会是"6Sherry", 6是长度. 这样比对key是否相等时速度会更快.LookupKey格式 = 长度 + key + SequenceNumber + type,这里用了两个tricks:

1.在栈上分配一个200长度的数组, 如果运行时发现长度不够用再从堆上new一个, 可以极大避免内存分配

2.黑科技函数"EncodeVarint32", 一般key的长度不可能用满32bit. 大量很短的Key却要用32bit来描述长度无疑是很浪费的. 这个函数让小数值用更少的空间, 代价是最糟要多花一字节(8bit).EncodeVarint32的代码出现在util文件夹下的coding.cc文件里,源码内容如下:

char* EncodeVarint32(char* dst, uint32_t v) {
// Operate on characters as unsigneds
unsigned char* ptr = reinterpret_cast<unsigned char*>(dst);
static const int B = ;
if (v < (<<)) {
*(ptr++) = v;
} else if (v < (<<)) {
*(ptr++) = v | B;
*(ptr++) = v>>;
} else if (v < (<<)) {
*(ptr++) = v | B;
*(ptr++) = (v>>) | B;
*(ptr++) = v>>;
} else if (v < (<<)) {
*(ptr++) = v | B;
*(ptr++) = (v>>) | B;
*(ptr++) = (v>>) | B;
*(ptr++) = v>>;
} else {
*(ptr++) = v | B;
*(ptr++) = (v>>) | B;
*(ptr++) = (v>>) | B;
*(ptr++) = (v>>) | B;
*(ptr++) = v>>;
}
return reinterpret_cast<char*>(ptr);
}

现在回到DBImpl::Get函数,memtable和immutable table都是通过内存中的skiplist进行的,磁盘文件的查找是通过db文件夹下version_set.cc中Version::Get来进行的.源码内容如下:

Status Version::Get(const ReadOptions& options,
const LookupKey& k,
std::string* value,
GetStats* stats) {
Slice ikey = k.internal_key();
Slice user_key = k.user_key();
const Comparator* ucmp = vset_->icmp_.user_comparator();
Status s; stats->seek_file = NULL;
stats->seek_file_level = -;
FileMetaData* last_file_read = NULL;
int last_file_read_level = -; // We can search level-by-level since entries never hop across
// levels. Therefore we are guaranteed that if we find data
// in an smaller level, later levels are irrelevant.
//查找用户提供的key可能在的文件,通过各个level的文件的最小值,最大值来判断
//按层查找
std::vector<FileMetaData*> tmp;
FileMetaData* tmp2;
for (int level = ; level < config::kNumLevels; level++) {
size_t num_files = files_[level].size();
if (num_files == ) continue; // Get the list of files to search in this level
FileMetaData* const* files = &files_[level][];
if (level == ) {
// Level-0 files may overlap each other. Find all files that
// overlap user_key and process them in order from newest to oldest.
tmp.reserve(num_files);
for (uint32_t i = ; i < num_files; i++) {
FileMetaData* f = files[i];
if (ucmp->Compare(user_key, f->smallest.user_key()) >= &&
ucmp->Compare(user_key, f->largest.user_key()) <= ) {
tmp.push_back(f);
}
}
if (tmp.empty()) continue; std::sort(tmp.begin(), tmp.end(), NewestFirst);
files = &tmp[];
num_files = tmp.size();
} else {
// Binary search to find earliest index whose largest key >= ikey.
//查找用户提供的key可能在的文件
uint32_t index = FindFile(vset_->icmp_, files_[level], ikey);
if (index >= num_files) {
files = NULL;
num_files = ;
} else {
tmp2 = files[index];
if (ucmp->Compare(user_key, tmp2->smallest.user_key()) < ) {
// All of "tmp2" is past any data for user_key
files = NULL;
num_files = ;
} else {
files = &tmp2;
num_files = ;
}
}
} for (uint32_t i = ; i < num_files; ++i) {
if (last_file_read != NULL && stats->seek_file == NULL) {
// We have had more than one seek for this read. Charge the 1st file.
stats->seek_file = last_file_read;
stats->seek_file_level = last_file_read_level;
} FileMetaData* f = files[i];
last_file_read = f;
last_file_read_level = level; Saver saver;
saver.state = kNotFound;
saver.ucmp = ucmp;
saver.user_key = user_key;
saver.value = value;
//在table_cache中查找key对应的value
s = vset_->table_cache_->Get(options, f->number, f->file_size,
ikey, &saver, SaveValue);
if (!s.ok()) {
return s;
}
switch (saver.state) {
case kNotFound:
break; // Keep searching in other files
case kFound:
return s;
case kDeleted:
s = Status::NotFound(Slice()); // Use empty error message for speed
return s;
case kCorrupt:
s = Status::Corruption("corrupted key for ", user_key);
return s;
}
}
} return Status::NotFound(Slice()); // Use an empty error message for speed
}

FindFile源码内容如下:

int FindFile(const InternalKeyComparator& icmp,
const std::vector<FileMetaData*>& files,
const Slice& key) {
uint32_t left = ;
uint32_t right = files.size();
while (left < right) {
uint32_t mid = (left + right) / ;
const FileMetaData* f = files[mid];
if (icmp.InternalKeyComparator::Compare(f->largest.Encode(), key) < ) {
// Key at "mid.largest" is < "target". Therefore all
// files at or before "mid" are uninteresting.
left = mid + ;
} else {
// Key at "mid.largest" is >= "target". Therefore all files
// after "mid" are uninteresting.
right = mid;
}
}
return right;
}

Version::Get函数首先查找key可能存在的sst表,然后调用table_cache->Get进行查找。即对SSTable的查询就是对table_cache_的查询, 这个cache是不可取消的, 解决了什么问题呢?

LevelDB的数据库"文件"是一个文件夹, 里面包含大量的文件. 这是把复杂度甩锅给操作系统的做法, 但很多系统资源是有限的. 比如, file handle(文件句柄). 一个程序如果开了1W个file handle会浪费大量资源. 这里做个LRU cache, 只有常用的SSTable才会开一个活跃的file handle.

另外就是索引的问题. LSMT是没有主索引的, 只有在各个SSTable内有微缩版索引. 所以, 最最优的情况下也需要2次硬盘读写. 第一张SSTable就存着key, 先读微型索引, 然后二分法找到具体位置, 再读value.

TableCache把热点SSTable的微型索引预先放在内存里, 这样只要1次硬盘读取就能取到key. 这个优化对于LSMT的数据库来说尤为重要, 因为很可能会不止查询一张SSTable. 情况会劣化非常快.

总结, TableCache既承担管理资源(file handle)的作用, 又加速索引的读取.

TableCache的实现在db文件夹下table_cache.cc中,源码内容如下:

Status TableCache::Get(const ReadOptions& options,
uint64_t file_number,
uint64_t file_size,
const Slice& k,
void* arg,
void (*saver)(void*, const Slice&, const Slice&)) {
Cache::Handle* handle = NULL;
Status s = FindTable(file_number, file_size, &handle);//查找table,没有则新建table结构并插入table_cache
if (s.ok()) {
Table* t = reinterpret_cast<TableAndFile*>(cache_->Value(handle))->table;
s = t->InternalGet(options, k, arg, saver); //在table中查找
cache_->Release(handle);
}
return s;
}

该函数流程很简单,先从table_cache中获取Table结构,没有则新建Table结构加入table_cache,然后调用Table::InternalGet在具体的sst表中查找.需要注意的是,在util文件夹下查看cache.cc可以看到

virtual Handle* Insert(const Slice& key, void* value, size_t charge,
void (*deleter)(const Slice& key, void* value)) {
const uint32_t hash = HashSlice(key);
return shard_[Shard(hash)].Insert(key, hash, value, charge, deleter);
}
virtual Handle* Lookup(const Slice& key) {
const uint32_t hash = HashSlice(key);
return shard_[Shard(hash)].Lookup(key, hash);
}

这个hash table做了两遍hash, 先把key分片一遍, 然后再扔给真正的hash table cache(有锁)去lookup.这么做的逻辑是可以减少锁的使用率和提升并发.

进一步查看table_cache.cc中的Table::InternalGet函数:

Status Table::InternalGet(const ReadOptions& options, const Slice& k,
void* arg,
void (*saver)(void*, const Slice&, const Slice&)) {
Status s;
Iterator* iiter = rep_->index_block->NewIterator(rep_->options.comparator);
iiter->Seek(k);//在索引中找,是否存在某个块可能包含这个key
if (iiter->Valid()) {
Slice handle_value = iiter->value();
FilterBlockReader* filter = rep_->filter;
BlockHandle handle;
if (filter != NULL &&
handle.DecodeFrom(&handle_value).ok() &&
!filter->KeyMayMatch(handle.offset(), k)) {
// Not found
} else {
//在具体的block中找
Iterator* block_iter = BlockReader(this, options, iiter->value());
block_iter->Seek(k);
if (block_iter->Valid()) {
(*saver)(arg, block_iter->key(), block_iter->value());
}
s = block_iter->status();
delete block_iter;
}
}
if (s.ok()) {
s = iiter->status();
}
delete iiter;
return s;
}

Table::Get函数先在table的indexblock中查找该key所处的block,然后利用Bloom Filter来过滤,最后在具体的block中查找。在查找过程中使用了Iterator机制。

总体来说,leveldb中Get操作的流程可以用下图来说明: 

参考文献:

1.http://blog.csdn.net/joeyon1985/article/details/47154249

2.http://masutangu.com/2017/06/leveldb_1/

3.https://zhuanlan.zhihu.com/jimderestaurant?topic=LevelDB

LevelDB的源码阅读(三) Get操作的更多相关文章

  1. 25 BasicUsageEnvironment0基本使用环境基类——Live555源码阅读(三)UsageEnvironment

    25 BasicUsageEnvironment0基本使用环境基类——Live555源码阅读(三)UsageEnvironment 25 BasicUsageEnvironment0基本使用环境基类— ...

  2. 24 UsageEnvironment使用环境抽象基类——Live555源码阅读(三)UsageEnvironment

    24 UsageEnvironment使用环境抽象基类——Live555源码阅读(三)UsageEnvironment 24 UsageEnvironment使用环境抽象基类——Live555源码阅读 ...

  3. 26 BasicUsageEnvironment基本使用环境——Live555源码阅读(三)UsageEnvironment

    26 BasicUsageEnvironment基本使用环境--Live555源码阅读(三)UsageEnvironment 26 BasicUsageEnvironment基本使用环境--Live5 ...

  4. LevelDB的源码阅读(三) Put操作

    在Linux上leveldb的安装和使用中我们写了这么一段测试代码,内容以及输出结果如下: #include <iostream> #include <string> #inc ...

  5. LevelDB的源码阅读(四) Compaction操作

    leveldb的数据存储采用LSM的思想,将随机写入变为顺序写入,记录写入操作日志,一旦日志被以追加写的形式写入硬盘,就返回写入成功,由后台线程将写入日志作用于原有的磁盘文件生成新的磁盘数据.Leve ...

  6. LevelDB的源码阅读(二) Open操作

    在Linux上leveldb的安装和使用中我们写了一个测试代码,内容如下: #include "leveldb/db.h" #include <cassert> #in ...

  7. LevelDB的源码阅读(一)

    源码下载 git clone https://github.com/google/leveldb.git 项目结构 db/, 数据库逻辑 doc/, MD文档 helpers/, LevelDB内存版 ...

  8. SparkSQL(源码阅读三)

    额,没忍住,想完全了解sparksql,毕竟一直在用嘛,想一次性搞清楚它,所以今天再多看点好了~ 曾几何时,有一个叫做shark的东西,它改了hive的源码...突然有一天,spark Sql突然出现 ...

  9. SpringMVC源码阅读(三)

    先理一下Bean的初始化路线 org.springframework.beans.factory.support.AbstractBeanDefinitionReader public int loa ...

随机推荐

  1. php mqtt client

    <?php /* phpMQTT */ class phpMQTT { private $socket; /* holds the socket */ private $msgid = 1; / ...

  2. [转帖]InfiniBand, RDMA, iWARP, RoCE , CNA, FCoE, TOE, RDMA, iWARP, iSCSI等概念

    InfiniBand, RDMA, iWARP, RoCE , CNA, FCoE, TOE, RDMA, iWARP, iSCSI等概念 2017-12-15 15:37:00 jhzh951753 ...

  3. Java基础笔试练习(三)

    1.下列InputStream类中哪个方法可以用于关闭流? A.skip() B.close() C.mark() D.reset() 答案: B 解析: inputstream的close方法用来关 ...

  4. v8 引擎的内存

    一.nodejs查看内容使用情况: process.memoryUsage() 单位为 Btye 转化函数: var format = function(bytes) { return (bytes/ ...

  5. YIii2.0-学习笔记之服务器安装

    最近项目需要,学习了一下yii2.0框架 第一步:  安装composer--------作为一个phper  composer不会用可能有点不合适 下载composer.phar文件 #curl - ...

  6. zabbix 数据库分表操作

    近期zabbix数据库占用的io高,在页面查看图形很慢,而且数据表已经很大,将采用把数据库的数据目录移到新的磁盘,将几个大表进行分表操作 一.数据迁移: 1.数据同步到新的磁盘上,先停止mysql(不 ...

  7. Spring/Spring Boot整合Weblogic JMS实战

    本文主要介绍weblogic jms的配置,包括JMS 服务器和JMS 模块(连接工厂.队列.远程 SAF 上下文.SAF 导入目的地.SAF 错误处理)的配置:并在Spring/Spring Boo ...

  8. python之爬取小说

    继上一篇爬取小说一念之间的第一章,这里将进一步展示如何爬取整篇小说 # -*- coding: utf- -*- import urllib.request import bs4 import re ...

  9. UI5-技术篇-SEGW DEEP ENTITY测试

    参考:http://blog.sina.com.cn/s/blog_a7d67d810102xgms.html 一般调用后端的OData服务需要同时传输多个记录表对业务逻辑进行处理(例如BAPI),今 ...

  10. BPM业务流程管理系统_K2受邀出席QAD客户日活动,赋能企业云端智造_工作流引擎

    10月17日,K2受邀参加由厦门易维主办的以“走进QAD云ERP,深耕智能制造”为主题的QAD客户日活动.本次大会是以工业4.0背景下传统制造业面临巨大压力和挑战为导向,旨在探讨如何助力企业迅速适应业 ...