项目有用到leveldb,打算最近看看源码,整理个系列文章,这是第4篇memtable and log。源码注释地址
代码细节在github fork的仓库注释了,这里主要记录下大概的流程。从put开始。
put
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value) { WriteBatch batch; batch.Put(key, value); return Write(opt, &batch); }
void WriteBatch::Put(const Slice& key, const Slice& value) { WriteBatchInternal::SetCount(this, WriteBatchInternal::Count(this) + 1); rep_.push_back(static_cast<char>(kTypeValue)); PutLengthPrefixedSlice(&rep_, key); PutLengthPrefixedSlice(&rep_, value); }
|
1 2 3 4 5 6 7 8 9 10 11
| Status DBImpl::Write(...) { MakeRoomForWrite(...) log_->AddRecord(WriteBatchInternal::Contents(write_batch)); status = WriteBatchInternal::InsertInto(write_batch, mem_); }
|
log
AddRecord(…)
这块比较麻烦,代码里面我都注释了,这块只画个图描述下。日志是按block来处理的,代码里面一个block是32768字节,block由若干个record组成。每次put一个kv就是若干个record, 因为这里分段存储的,先看一个record的格式:
如果一个put的kv可以在block的avail剩余空间内存储,则type=kFullType, 只需要一个record:
如果block遗留空间不够,则要拆分为多个block, 可能是一个begin(1个) + middle(0-n个) + end(1个),如图:
另外需要注意的,如果一个block的剩余空间小于一个header(7个字节),则去下一个block存储,也就是说data会被拆分到多个block, 但一个header是不能被拆分的。还有就是每次拼成一个record都会被flush刷盘。record里面的data就是rep_ ,没有再转换。
memtable
1 2 3 4 5 6 7
| WriteBatchInternal::InsertInto(write_batch, mem_) Iterate(&inserter) remove rep_前面13个字节 handler->Put(key, value) mem_->Add(sequence_, kTypeValue, key, value) handler->Delete(key) mem_->Add(sequence_, kTypeDeletion, key, Slice())
|
memtable先将数据存在heap, 然后把地址放到skiplist, 这里有一些数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| MemTable::Add(...) { const size_t encoded_len = VarintLength(internal_key_size) + internal_key_size + VarintLength(val_size) + val_size; arena_.Allocate(encoded_len); EncodeVarint32(buf, internal_key_size); memcpy(p, key.data(), key_size); EncodeFixed64(p, (s << 8) | type); EncodeVarint32(p, val_size); memcpy(p, value.data(), val_size);
table_.Insert(buf); }
|
这里申请了一块内存,保存了key和value的值,大小是:
1 2 3 4
| encoded_len (key-len + key-data) + (seq) + (val-len + val-data) -> (varint32 + char*) + (int64) + (varint32 + char*) -> (1-5 + n) + 8 + (1-5 + n)bytes
|
然后把这块内存地址传给skiplist, 建立索引,这块我画个图记录下。skiplist初始化后是new了一块空间,用head_指过去,如图:
插入节点后:
1 2 3 4 5
| Insert FindGreaterOrEqual RandomHeight NewNode SetNext
|
reference
https://web.archive.org/web/20120131105110/http://rdc.taobao.com/blog/cs/wp-content/plugins/leveldb%E5%AE%9E%E7%8E%B0%E8%A7%A3%E6%9E%90.pdf