MemTable是leveldb的重要组件,写链路的数据都是直接写入MemTable
的,而且查询链路也是首先查找MemTable
的数据,因此MemTable
的读写性能就十分关键了。leveldb中使用了SkipList作为MemTable,本篇文章主要讲解leveldb中MemTable
的数据写入和查询,及其对应的Iterator的实现。
SkipList
跳表是常见的加速查询的数据结构,如图1-1所示展示了一个含有4个节点的跳表:

这个跳表的最大高度是6,即从下往上分为6层,每一层可看作是一个跳表。查找时依次从最高层开始,一旦确定待查找的key不在这一层了之后,马上从下一层开始查找。直到找到最底层为止,返回查找的结果,或者返回不存在。
跳表特性如下:
- 每一层都是一个有序跳表
- 一旦第i层有这个节点,那么其下的每一层也都有这个节点
- 最底层包含跳表的所有元素
SkipList的查找
跳表的查找无论找到与否,都要从最高层一直查到最底层。
在跳表中查找target key所在的节点:
首先在最高层链表中查找,找到最后一个< target_key的节点;然后去往该节点的下一层,继续查找< target_key的节点。直到最底层为止,在最底层找到最后一个< target_key的节点后,如果该节点的next节点 == target_key,那么就返回这个next节点,否则就返回不存在。
以图1-1为例,假如查找的target_key为34
,查找流程如下:
- 在第六层中查找,找到最后一个 < target_key的节点,即
24
- 向下一层到达第5层,找到最后一个 < target_key的节点,依然是
24
- 向下一层到达第4层,找到最后一个 < target_key的节点,依然是
24
- 向下一层到达第3层,找到最后一个 < target_key的节点,依然是
24
- 向下一层到达第2层,找到最后一个 < target_key的节点,找到了
30
- 向下一层到达第1层,找到最后一个 < target_key的节点,依然是
30
。找到第一层中节点30
的next节点——67
,说明target_key34
不存在
跳表能有效第加速查询的性能,这个例子中的节点数较少,效果不明显。当节点数较多的时候,跳表比单层有序链表的查找快多了,效率跟AVL树和红黑树差不多
SkipList的插入
在跳表中插入节点的流程跟跳表的查找过程类似,在插入一个节点的时候它的高度是随机生成的,即跳表有一个最大高度MAX_HEIGHT
,插入的每个节点在MAX_HEIGHT
的范围内随机生成一个高度h
,以插入34
这个节点为例,假设该节点随机生成的高度为3
- 从最高层开始首先查找34,主要目的是找到每一层34节点的prev节点是谁
- 在第六层中查找,找到最后一个 < target_key的节点,即
24
,记下这个节点 - 向下一层到达第5层,找到最后一个 < target_key的节点,依然是
24
,记下这个节点 - 向下一层到达第4层,找到最后一个 < target_key的节点,依然是
24
,记下这个节点 - 向下一层到达第3层,找到最后一个 < target_key的节点,依然是
24
,记下这个节点 - 向下一层到达第2层,找到最后一个 < target_key的节点,即
30
,记下这个节点 - 向下一层到达第1层,找到最后一个 < target_key的节点,依然是
30
,记下这个节点 - 由于待插入的节点高度为3,从第一层开始一次插入这个节点,这就用到了之前记录的每一层中待插入节点的prev节点,第一层的prev节点是
30
,因此在prev节点后插入新节点34
即可 - 依此类推,一直到第3层,在之前所记录的每层的prev节点后插入新节点
可以看到,插入新节点的过程依然是查找的过程,唯一不同的是在每一层的查找中,记录了该层待插入节点的prev节点,即 小于 待插入节点的最后一个节点。然后根据待插入节点的高度,再在相应层中插入这个节点即可。
SkipList源码解析
在简单介绍了跳表原理后,下面来看下跳表在leveldb中的实现
template <typename Key, class Comparator>
class SkipList {
private:
struct Node;
public:
explicit SkipList(Comparator cmp, Arena* arena);
void Insert(const Key& key);
bool Contains(const Key& key) const;
class Iterator;
private:
enum { kMaxHeight = 12 };
Node* NewNode(const Key& key, int height);
int RandomHeight();
bool Equal(const Key& a, const Key& b) const { return (compare_(a, b) == 0); }
// Return whether @key > @Node n
bool KeyIsAfterNode(const Key& key, Node* n) const;
// Return the earliest node that comes at or after key. return null if no such node
Node* FindGreaterOrEqual(const Key& key, Node** prev) const;
// Return the latest node with a key < key. return head_ if no such node
Node* FindLessThan(const Key& key) const;
// return the last node of SkipList
Node* FindLast() const;
Comparator const compare_;
Arena* const arena_; // Arena used for allocations of nodes
Node* const head_;
// Modified only by Insert().
std::atomic<int> max_height_;
};
可以看到leveldb中跳表最大高度为12,插入一个新节点时,该节点高度也在1~12之间随机。SkipList
主要成员就是一个head_
节点和max_height_
. 其核心函数只有2个——FindGreaterOrEqual
和FindLessThan
,前者如同之前所述,用于查找过程和插入的过程。后者主要是给Iterator的Prev
用的,leveldb中跳表都是单向的,每个节点没有记录prev节点,可能是为了代码实现简单吧。
下面来看跳表节点的结构:
template <typename Key, class Comparator>
struct SkipList<Key, Comparator>::Node {
explicit Node(const Key& k) : key(k) {}
// Accessors/mutators for links. Wrapped in methods so we can
// add the appropriate barriers as necessary.
Node* Next(int n) {
assert(n >= 0);
// Use an 'acquire load' so that we observe a fully initialized
// version of the returned Node.
return next_[n].load(std::memory_order_acquire);
}
void SetNext(int n, Node* x) {
assert(n >= 0);
// Use a 'release store' so that anybody who reads through this
// pointer observes a fully initialized version of the inserted node.
next_[n].store(x, std::memory_order_release);
}
// No-barrier variants that can be safely used in a few locations.
Node* NoBarrier_Next(int n) {
assert(n >= 0);
return next_[n].load(std::memory_order_relaxed);
}
void NoBarrier_SetNext(int n, Node* x) {
assert(n >= 0);
next_[n].store(x, std::memory_order_relaxed);
}
public:
Key const key;
private:
// Array of length equal to the node height. next_[0] is lowest level link.
std::atomic<Node*> next_[1];
};
每个节点用一个next_
数组来保存其每一层的next节点,数组长度就是该节点的高度,即从实现来看,每个节点的next_
数组会指向不同层的next节点。
FindGreaterOrEqual
下面重点来看下FindGreaterOrEqual(const Key& key, Node** prev)
函数,该函数主要作用是找出跳表中第一个>= key
的节点:
template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node*
SkipList<Key, Comparator>::FindGreaterOrEqual(const Key& key, Node** prev) const {
Node* current = head_;
int level = GetMaxHeight() - 1;
while (true) {
Node* next = current->Next(level);
if (KeyIsAfterNode(key, next)) {
// Keep searching in this list
current = next;
} else {
if (prev != nullptr) prev[level] = current;
if (level == 0) {
return next;
} else {
// Switch to next list
level--;
}
}
}
}
第4-5行从当前跳表最高层的头节点开始遍历找出<= key的最后一个节点. 第7-8行读取当前层中current
的下一个节点,并与key
作比较,若next < key
,则current
向后移一位,直到next >= key
,说明在当前层中,current
是最后一个小于key
的节点了,然后将current
指针保存在prev[level]
数组中. 若当前level已经是最底层,则next
节点就是>= key
的第一个节点了,否则向下移动一层继续查找。
FindLessThan
再来看FindLessThan(const Key& key)
函数:
template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node*
SkipList<Key, Comparator>::FindLessThan(const Key& key) const {
Node* current = head_;
int level = GetMaxHeight() - 1;
while (true) {
assert(current == head_ || compare_(current->key, key) < 0);
Node* next = x->Next(level);
if (next != nullptr && compare_(next->key, key) < 0) {
current = next;
} else {
if (level == 0) {
return current;
} else {
// Switch to next list
level--;
}
}
}
}
FindLessThan
函数与FindGreaterOrEqual
几乎一模一样,只不过FindLessThan
返回的是current
节点,即当前跳表中< key
的最后一个节点。
Insert
最后来看一下跳表的插入吧:
template <typename Key, class Comparator>
void SkipList<Key, Comparator>::Insert(const Key& key) {
Node* prev[kMaxHeight];
Node* x = FindGreaterOrEqual(key, prev);
// Our data structure does not allow duplicate insertion
assert(x == nullptr || !Equal(key, x->key));
int height = RandomHeight();
if (height > GetMaxHeight()) {
for (int i = GetMaxHeight(); i < height; i++) {
prev[i] = head_;
}
max_height_.store(height, std::memory_order_relaxed);
}
x = NewNode(key, height);
for (int i = 0; i < height; i++) {
// NoBarrier_SetNext() suffices since we will add a barrier when
// we publish a pointer to "x" in prev[i].
x->NoBarrier_SetNext(i, prev[i]->NoBarrier_Next(i));
prev[i]->SetNext(i, x);
}
}
第3行创建了一个大小为kMaxHeight
的Node* prev[kMaxHeight]
数组,只后第4行调用FindGreaterOrEqual
找到每一层中< key
的最后一个节点,填充到prev
数组中。然后第9行随机生成待插入的新节点的高度,如果新节点的高度超过了跳表当前的max_height_
,那么第10~15行将prev
数组中超过的部分都置为head_
节点,然后设置好max_height_
的新值. 第17行生成新的节点,第18~23行就是将新节点插入到每一层的跳表中了.
NewNode
其中第17行NewNode
函数会接收key
和height
来生成一个新节点,通过之前的描述,我们知道一个Node内部包含了key
和一个next_
数组,我们来看下NewNode
主要是怎样创建节点的吧:
template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node* SkipList<Key, Comparator>::NewNode(
const Key& key, int height) {
char* const node_memory = arena_->AllocateAligned(
sizeof(Node) + sizeof(std::atomic<Node*>) * (height - 1));
return new (node_memory) Node(key);
}
首先计算该Node一共需要多大的内存空间,即key的大小 + next_
数组的大小(next_
数组的长度是这个节点的高度),分配好这段内存buffer之后,通过placement new生成这个Node对象.
Contains
跳表判断指定的key
是否存在也就变的显而易见了:
template <typename Key, class Comparator>
bool SkipList<Key, Comparator>::Contains(const Key& key) const {
Node* x = FindGreaterOrEqual(key, nullptr);
if (x != nullptr && Equal(key, x->key)) {
return true;
} else {
return false;
}
}
找到第一个>= key
的节点,然后判断该节点的key是否和待查找的key相等。
查找SkipList
levedb对跳表的查询都是通过创建跳表的Iterator来查询的,其实有了上述的那些函数后,跳表Iterator的实现也就非常简单了。
class Iterator {
public:
// Initialize an iterator over the specified list.
// The returned iterator is not valid.
explicit Iterator(const SkipList* list) {
list_ = list;
node_ = nullptr;
}
// Returns true iff the iterator is positioned at a valid node.
bool Valid() const {
return node_ != nullptr;
}
// Returns the key at the current position.
const Key& key() const {
assert(Valid());
return node_->key;
}
// Advances to the next position.
void Next() {
assert(Valid());
node_ = node_->Next(0);
}
// Advances to the previous position.
void Prev() {
assert(Valid());
node_ = list_->FindLessThan(node_->key);
if (node_ == list_->head_) {
node_ = nullptr;
}
}
// Advance to the first entry with a key >= target
void Seek(const Key& target) {
node_ = list_->FindGreaterOrEqual(target, nullptr);
}
// Position at the first entry in list.
// Final state of iterator is Valid() iff list is not empty.
void SeekToFirst() {
node_ = list_->head_->Next(0);
}
// Position at the last entry in list.
// Final state of iterator is Valid() iff list is not empty.
void SeekToLast();
private:
const SkipList* list_;
Node* node_;
// Intentionally copyable
};
Iterator持有了一个SkipList*
指针和当前所遍历到的Node*
指针,其成员函数的实现主要都依赖于FindGreaterOrEqual
和FindLessThan
.
leveldb SkipList
的实现如上所述,还是比较简单的,leveldb对MemTable的操作不是直接操作SkipList,而是在其之上又封装了一层MemTable
的类
MemTable
leveldb的MemTable就是对SkipList的简单封装,对外提供了写入和查找两个接口,以及数据Iterator的封装.
class MemTable {
public:
// MemTables are reference counted. The initial reference count
// is zero and the caller must call Ref() at least once.
explicit MemTable(const InternalKeyComparator& comparator);
Iterator* NewIterator();
// Add an entry into memtable that maps key to value at the
// specified sequence number and with the specified type.
// Typically value will be empty if type==kTypeDeletion.
void Add(SequenceNumber seq, ValueType type, const Slice& key,
const Slice& value);
// If memtable contains a value for key, store it in *value and return true.
// If memtable contains a deletion for key, store a NotFound() error
// in *status and return true.
// Else, return false.
bool Get(const LookupKey& key, std::string* value, Status* s);
private:
friend class MemTableIterator;
struct KeyComparator {
const InternalKeyComparator comparator;
explicit KeyComparator(const InternalKeyComparator& c) : comparator(c) {}
int operator()(const char* a, const char* b) const;
};
using Table = SkipList<const char*, KeyComparator>;
KeyComparator comparator_;
int refs_;
Arena arena_;
Table table_;
};
MemTable
持有了一个SkipList<const char*, KeyComparator>
对象,其写入数据,读取数据的操作都是借助于SkipList
提供的方法来完成的。这里有必要先介绍一下SkipList
存储的key的结构。
InternalKey
SkipList
不是存用户裸的key,而是存储一个叫作InternalKey
的结构:
class InternalKey {
private:
std::string rep_;
public:
Slice user_key() const { return ExtractUserKey(rep_); }
};
Slice ExtractUserKey(const Slice& internal_key) {
assert(internal_key.size() >= 8);
return Slice(internal_key.data(), internal_key.size() - 8);
}
InternalKey
由3部分组成:user_key + seq_num + value_type
其中seq_num + value_type
合起来是8个bytes(64位),seq_num
占高56位,value_type
在低8位,
value_type
只有如下两种类型:enum ValueType { kTypeDeletion = 0x0, kTypeValue = 0x1 };
InternalKeyComparator
leveldb内部大量使用了InternalKeyComparator
用来比较InternalKey
,其比较逻辑是提取出其中的user_key,先比较user_key,若user_key相同的话,那么再按照desc order比较InternalKey
的后8个bytes(即seq_num+value_type),后8个bytes越大的话,则比较结果越小.
// Compare user_key then seq number
int InternalKeyComparator::Compare(const Slice& akey, const Slice& bkey) const {
// Order by:
// increasing user key (according to user-supplied comparator)
// decreasing sequence number
// decreasing type (though sequence# should be enough to disambiguate)
int r = user_comparator_->Compare(ExtractUserKey(akey), ExtractUserKey(bkey));
if (r == 0) {
const uint64_t anum = DecodeFixed64(akey.data() + akey.size() - 8);
const uint64_t bnum = DecodeFixed64(bkey.data() + bkey.size() - 8);
if (anum > bnum) {
r = -1;
} else if (anum < bnum) {
r = +1;
}
}
return r;
}
但是看到MemTable
的成员中使用的跳表类型是SkipList<const char*, KeyComparator>
,这里就有点纳闷了,跳表key的类型是const char*,那么在比较key的时候怎么比较两个char*的大小呢?const char*里存的又是什么东西呢?
SkipList的数据格式
下面就来揭秘SkipList里存的是什么数据。来看MemTable是怎样往跳表中写入数据的:
void MemTable::Add(SequenceNumber s, ValueType type, const Slice& key,
const Slice& value) {
// Format of an entry is concatenation of:
// key_size : varint32 of internal_key.size()
// key bytes : char[internal_key.size()]
// tag : uint64((sequence << 8) | type)
// value_size : varint32 of value.size()
// value bytes : char[value.size()]
size_t key_size = key.size();
size_t val_size = value.size();
size_t internal_key_size = key_size + 8;
const size_t encoded_len = VarintLength(internal_key_size) +
internal_key_size + VarintLength(val_size) +
val_size;
char* buf = arena_.Allocate(encoded_len);
char* p = EncodeVarint32(buf, internal_key_size);
std::memcpy(p, key.data(), key_size);
p += key_size;
EncodeFixed64(p, (s << 8) | type);
p += 8;
p = EncodeVarint32(p, val_size);
std::memcpy(p, value.data(), val_size);
assert(p + val_size == buf + encoded_len);
table_.Insert(buf);
}
可以看到每次往跳表中写入一个kv-pair的时候,直接申请了一整块buffer,然后序列化key和value的值,一个kv-pair的内存布局如下:

如图2-1所示,在往MemTable
中插入数据的时候,会申请一整块buffer来连续存放InternalKey
和Value
.这一整块buffer存放的key和value都是PrefixedSlice
,即头部是后续数据的大小,然后是后续的数据,在解析的时候先读取并解析头部size的值,然后根据这个size的值就能知道后续的数据有多长了。第24行看到会往跳表中插入一个const char*
的指针,即这一块buffer的头部指针,将这一个char*指针作为跳表的key,那么在跳表内部比较两个key的时候必然会根据char*解析出PrefixedSlice
所代表的InternalKey
,然后比较这两个InternalKey
值的,这个比较逻辑在MemTable::KeyComparator
中:
class MemTable {
private:
struct KeyComparator {
const InternalKeyComparator comparator;
explicit KeyComparator(const InternalKeyComparator& c) : comparator(c) {}
int operator()(const char* a, const char* b) const {
// Internal keys are encoded as length-prefixed strings.
Slice a = GetLengthPrefixedSlice(aptr);
Slice b = GetLengthPrefixedSlice(bptr);
return comparator.Compare(a, b);
}
};
如第8~9行所示,先将PrefixedSlice解析出来,然后再调用InternalKeyComparator::Compare
来比较两个InternalKey
. 不知道leveldb为什么不直接将Slice作为SkipList的key呢,这样更直观,而且也不需要在比较时解出PrefixedSlice了...
最后来看MemTable
的查询逻辑:
leveldb是通过MemTable::Get
这个函数来查找MemTable中的kv-pair的
bool MemTable::Get(const LookupKey& key, std::string* value, Status* s);
MemTable::Get
这个函数的作用就是查找指定的key,然后将这个key对应的value设置在参数std::string* value
中,输入的LookupKey
这个结构其实就是一个PrefixedSlice的封装:
class LookupKey {
public:
// Initialize *this for looking up user_key at a snapshot with
// the specified sequence number.
LookupKey(const Slice& user_key, SequenceNumber sequence);
// Return a key suitable for lookup in a MemTable.
Slice memtable_key() const { return Slice(start_, end_ - start_); }
// Return an internal key (suitable for passing to an internal iterator)
Slice internal_key() const { return Slice(kstart_, end_ - kstart_); }
// Return the user key
Slice user_key() const { return Slice(kstart_, end_ - kstart_ - 8); }
private:
// We construct a char array of the form:
// klength varint32 <-- start_
// userkey char[klength] <-- kstart_
// tag uint64
// <-- end_
// The array is a suitable MemTable key.
// The suffix starting with "userkey" can be used as an InternalKey.
const char* start_;
const char* kstart_;
const char* end_;
char space_[200]; // Avoid allocation for short keys
};
这个结构底层依赖的数据就是一个InternalKey
的PrefixedSlice,存放在start_
指向的一块内存buffer中。分别提供了memtable_key
, internal_key
和user_key
三个方法来返回这块buffer中不同的区域。
bool MemTable::Get(const LookupKey& key, std::string* value, Status* s) {
Slice memkey = key.memtable_key();
Table::Iterator iter(&table_);
iter.Seek(memkey.data());
if (iter.Valid()) {
// entry format is:
// klength varint32
// userkey char[klength - 8]
// tag uint64
// vlength varint32
// value char[vlength]
// Check that it belongs to same user key. We do not check the
// sequence number since the Seek() call above should have skipped
// all entries with overly large sequence numbers.
const char* entry = iter.key();
uint32_t key_length;
const char* key_ptr = GetVarint32Ptr(entry, entry + 5, &key_length);
if (comparator_.comparator.user_comparator()->Compare(
Slice(key_ptr, key_length - 8), key.user_key()) == 0) {
// Correct user key
const uint64_t tag = DecodeFixed64(key_ptr + key_length - 8);
switch (static_cast<ValueType>(tag & 0xff)) {
case kTypeValue: {
Slice v = GetLengthPrefixedSlice(key_ptr + key_length);
value->assign(v.data(), v.size());
return true;
}
case kTypeDeletion:
*s = Status::NotFound(Slice());
return true;
}
}
}
return false;
}
在MemTable::Get
查询的时候会临时构造一个SkipList::Iterator
,然后去Seek
传入的InternalKey
,注:Seek
的结果是找到第一个>= InternalKey
的key. 第15~17行取出iter当前指向的key,将这个key的user_key
部分与待查找的user_key
作比较,如果相等则说明找到了这个key,由于Seek
函数是定位到第一个>= InternalKey
的key,如果此时user_key
相等了,那么InternalKey
的seq_num + type部分一定 >= 待查找的InternalKey
,即此时命中的key中的seq_num一定<=待查找key的seq_num. 最后看命中的key的type是什么,如果是kTypeValue
那么就真的找到了;否则,说明这个key已经在MemTable中被删除了
MemTable迭代器
最后来看下MemTable的迭代器吧,上层DB在查找MemTable的时候一般都是操作迭代器的
class MemTableIterator : public Iterator {
public:
explicit MemTableIterator(MemTable::Table* table) : iter_(table) {}
MemTableIterator(const MemTableIterator&) = delete;
MemTableIterator& operator=(const MemTableIterator&) = delete;
~MemTableIterator() override = default;
bool Valid() const override { return iter_.Valid(); }
void Seek(const Slice& k) override { iter_.Seek(EncodeKey(&tmp_, k)); }
void SeekToFirst() override { iter_.SeekToFirst(); }
void SeekToLast() override { iter_.SeekToLast(); }
void Next() override { iter_.Next(); }
void Prev() override { iter_.Prev(); }
Slice key() const override { return GetLengthPrefixedSlice(iter_.key()); }
Slice value() const override {
Slice key_slice = GetLengthPrefixedSlice(iter_.key());
return GetLengthPrefixedSlice(key_slice.data() + key_slice.size());
}
private:
MemTable::Table::Iterator iter_;
std::string tmp_; // For passing to EncodeKey
};
MemTableIterator
是对SkipList::Iterator
的简单封装,相应的方法都是直接调用SkipList::Iterator
来完成的。特别注意的是SkipList::Iterator
返回的key()
是const char*
,而MemTableIterator
是给上层DB直接使用的,因此上层需要的是InternalKey
的结构,因此MemTableIterator::key()
会将跳表返回的const char*
做一次PrefixedSlice的解析,生成代表InternalKey
的Slice然后返回。上一小节介绍了SkipList中存储的数据格式——紧接着InternalKey
之后就是kv-pair的value部分了,因此MemTableIterator::value()
方法先将InternalKey
解出,然后再将key之后存储的value解出,然后返回。
此外,MemTableIterator::Seek(const Slice& k)
方法传入的是一个InternalKey的Slice,所以需要先将它转化成PrefixedSlice的const char*
然后才能交给跳表去查找,因此第11行的EncodeKey
函数就是将InternalKey转换为PrefixedSlice的:
// Encode a suitable internal key target for "target" and return it.
// Uses *scratch as scratch space, and the returned pointer will point
// into this scratch space.
static const char* EncodeKey(std::string* scratch, const Slice& target) {
scratch->clear();
PutVarint32(scratch, target.size());
scratch->append(target.data(), target.size());
return scratch->data();
}
其实现也一目了然,就是在头部拼上了key的size. 如果leveldb将Slice作为SkipList的key,那么就没这些事了。。同时也能降低查询开销,不然每次Seek的时候都得拼上size前缀,然后拷贝一段buffer数据,有点浪费。不知道leveldb未来会不会做这个优化。
结语
以上就是leveldb MemTable
的全部内容了,主要就是跳表的一些操作,还是比较简单的。基于跳表上层又实现了插入和查找数据的操作,以及MemTableIterator
对SkipList::Iterator
的封装。
这篇文章是leveldb存储篇的完结,至此,leveldb数据存储结构就全部介绍完了。MemTable
的结构加上前2篇SST文件的结构就组成了leveldb所有的数据存储结构,这是leveldb的基石,后续的数据链路都是在操作这些底层的存储结构。这3篇文章是对leveldb底层存储结构和存储格式的全面剖析,基本上对leveldb的存储做了透彻的解析,看完之后能够基本掌握leveldb的存储。
下一篇文章会介绍leveldb的读写路径,敬请期待...
p.s. 开通微信个人公众号啦,会不定期更新一些大数据/数据库技术文章和个人思考等,欢迎关注微信公众号 搜索:一些次要时刻