读流程
当用户调用Get方法从leveldb中读取一个key的value时,leveldb按照memtable,imm,sst文件的顺序,依次寻找key。其中,在sst文件中搜索是通过在当前version中Get来实现的,version是sst文件的一个snapshot,可以保证版本一致,如果没有version机制,在get过程中,此key所在sst文件被合并到了其他文件中,则可能会get失败。
关键代码
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* 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);
//依次从mem,imm,current version中get对应的value
if (mem->Get(lkey, value, &s)) {
// Done
} else if (imm != NULL && imm->Get(lkey, value, &s)) {
// Done
} else {
s = current->Get(options, lkey, value, &stats);
have_stat_update = true;
}
mutex_.Lock();
}
if (have_stat_update && current->UpdateStats(stats)) {
MaybeScheduleCompaction();
}
mem->Unref();
if (imm != NULL) imm->Unref();
current->Unref();
return s;
}
需要注意的点
用户通过user-key来查询,在leveldb中首先会通过user-key和sequence num组成lookupkey,在三处查询,均使用lookupkey
Get时会首先获取锁,在临界区获取mem,imm,current version,当前seq等信息,而从三处查询时,不需要持有锁,因为对以上三处是只读
Get也可能会触发compaction,每个get会导致对sst文件seek,leveldb会记录对每个sst文件的seek次数,到达某个值之后,会将此文件compaction,其中的道理是,假设用户查询key是随机的,那文件越大,其被seek的次数应该越多,leveldb假设16k对应一次查询,比如文件大小为160k,那么它最多被seek10次,seek次数超过了10,就说明它的range太大了,应该被compaction(存疑?)
对sst文件的搜索,其实是按照level0,level1…的顺序进行的,但是这些细节都被封装在version->Get函数中,查看version->Get函数可以看到,首先根据key找到对应的file meta信息(version中保存了各个level的file meta信息),然后根据file meta信息(file number)到cache中找获取对应的TableAndFile(如果没有,则RandomAccess方式打开对应的文件),然后通过Table中的index block,filter block等信息来具体定位key在文件中的位置,期间,可能使用block cache。
MaybeSchedualCompaction函数会将一次compaction的判断和compaction放到后台任务队列中,后台会有一个bg线程消费此队列
写流程
大致上, 写入一条k-v的流程为
- 将k-v包装成为一个writer结构
- 放到一个writers 队列中,等待,直到被唤醒(自己的任务被执行或者自己的任务在队列头)
- 如果线程发现自己的任务在队列头,则将队列中目前所有的writer组合起来,先写log,再写入mem
- 如果线程发现自己的任务已经被执行,则返回
类比如下场景
一群人要去一个窗口交表, 先排队,如果发现自己不在队首,就睡觉,如果自己在队首,就把当前排队的人群中所有的表都收上来,代为递交,然后再挨个通知排队的人他的表已经交上去了,可以回家了。
需要注意的是
- 系统中任意时刻, 只有一个线程在执行写mem的操作,所以写入mem不需要加锁
- 每个线程都可能成为执行写入mem的线程
还没有想通的问题
为什么这样会提高效率?