leveldb源码分析--插入删除流程
由于网络上对leveldb的分析文章都比较丰富,一些基础概念和模型都介绍得比较多,所以本人就不再对这些概念以专门的篇幅进行介绍,本文主要以代码流程注释的方式。
首先我们从db的插入和删除开始以对整个体系有一个感性的认识,首先看插入:
Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value) { WriteBatch batch; //leveldb中不管单个插入还是多个插入都是以WriteBatch的方式进行的 batch.Put(key, value); return Write(opt, &batch); }
Delete也类似,只是调用了WriteBatch 的 Delete(key), 这样再内部会以不同的形式编码传递至下一步进行处理。具体的WriteBatch的实现和编码方式在稍后的文章中进行介绍。Delete和Put都调用了Write,,这里的Write是在DBImpl::Write中通过虚函数的形式实现对其调用的,我们接着看Write的流程
Status DBImpl::Write(const WriteOptions& options, WriteBatch* my_batch) { Writer w(&mutex_); w.batch = my_batch; w.sync = options.sync; w.done = false; /*产生一个Writer对象,然后保存必要的锁、batch、和同步写的相关信息
*/ MutexLock l(&mutex_); writers_.push_back(&w); // 上锁,然后放入待写的队列中 while (!w.done && &w != writers_.front()) {
/* 这里设计比较特别,需要跟后面的 BuildBatchGroup结合起来看,这里大致的意思是
一直等待到这次写完成或者这次写被放在队列的最前面,BuildBatchGroup会将队列
里所有sync设置相同的写请求组成一个WriteBatch进行写入,这里的写请求有可能在
别的线程完成写操作了,而是否在队列首的判断是有可能此刻没有其他线程在写循环中,
或者本次写请求和前面的写请求的同步设置不一致,那么这种情况就需要自己进入该线
程完成写的操作。
*/ w.cv.Wait(); } if (w.done) { return w.status; } // 这个函数的主要作用是清理内存表和外存(磁盘)的表使内存表腾出空间插入新的数据
// 这里的设计比较复杂设计到leveldb 的很多核心设计,我们这里先大致了解其功能 Status status = MakeRoomForWrite(my_batch == NULL); uint64_t last_sequence = versions_->LastSequence(); Writer* last_writer = &w; if (status.ok() && my_batch != NULL) { // NULL batch is for compactions WriteBatch* updates = BuildBatchGroup(&last_writer); WriteBatchInternal::SetSequence(updates, last_sequence + 1); last_sequence += WriteBatchInternal::Count(updates); { mutex_.Unlock();BuildBatchGroup
// 这里讲组装好的batch内容写入log,并根据同步设置判断是否同步到磁盘 status = log_->AddRecord(WriteBatchInternal::Contents(updates)); bool sync_error = false; if (status.ok() && options.sync) { status = logfile_->Sync(); if (!status.ok()) { sync_error = true; } } if (status.ok()) {
// 写入内存表,这里采用了一个遍历WriteBatch完成插入的方式,稍后分析 status = WriteBatchInternal::InsertInto(updates, mem_); } mutex_.Lock(); if (sync_error) { // 如果同步错误则记录相应错误信息. RecordBackgroundError(status); } }
// 删除在BuildBatch里面设置的零时Batch的内容
if (updates == tmp_batch_) tmp_batch_->Clear(); versions_->SetLastSequence(last_sequence); } while (true) {
// 唤醒所有等待写入的线程 Writer* ready = writers_.front(); writers_.pop_front(); if (ready != &w) { ready->status = status; ready->done = true; ready->cv.Signal(); } if (ready == last_writer) break; } // Notify new head of write queue,因为可能请求时不在队首而进入了等待状态,
// 这样唤醒他使其成为新的队首写线程,进行MakeRoomForWrite等一系列操作 if (!writers_.empty()) { writers_.front()->cv.Signal(); } return status; }
所以从流程可以清晰的看到插入删除的流程主要为:
1. 将这条KV记录以顺序写的方式追加到log文件末尾;
2. 将这条KV记录插入内存中的Memtable中,在插入过程中如果刚好后台进程在compaction会短暂停顿以为后台进程compaction腾出时间及cpu
这里涉及到一次磁盘读写操作和内存SkipList的插入操作,但是这里的磁盘写时文件的顺序追加写入效率是很高的,所以并不会导致写入速度的降低;
而且从流程分析我们知道,在插入(删除)过程中如果多线程同时进行,那么这些操作将会将操作的同步设置相同的相邻的操作合并为一个批插入,这样可以使整个系统的总吞吐量更大。所以一次插入记录操作只会等待一次磁盘文件追加写和内存SkipList插入操作,这是为何leveldb写入速度如此高效的根本原因。
我们这里讲插入和删除以等同的方式进行了介绍,可能有的朋友会觉得奇怪,删除不是需要查找到插入的原始记录的么?而leveldb进行了一个巧妙的将随机读写,转换为顺序读写的方式,那就是其并不存在立即删除的操作,而是与插入操作相同将插入操作插入的是Key:Value值改为删除操作插入的是“Key:删除标记”,并不真正立即去删除记录,而是后台Compaction的时候才去做对应的真正的删除操作,这又极大的提高了leveldb的效率。