leveldb设计分析之memtable

在Leveldb中,所有内存中的KV数据都存储在Memtable中,物理disk则存储在SSTable中。在系统运行过程中,如果Memtable中的数据占用内存到达指定值(Options.write_buffer_size),则Leveldb就自动将Memtable转换为Memtable,并自动生成新的Memtable。

在leveldb中存在的5种key

ParsedInternalKey

InternalKey

internalKey的内部组成格式为:

| User key (string) | sequence number (7 bytes) | value type (1 byte) |

ParseInternalKey函数解析InternalKey,AppendInternalKey生成一个internalkey。所以在ParsedInternalKey结构体中的前三个成员就是拆分的InternalKey,一目了然:


struct ParsedInternalKey {

  Slice user_key;

  SequenceNumber sequence;

  ValueType type;

  ...

}

User Key

其实就是一个字符串

LookupKey

Memtable Key

LookupKey生成一个key,它内部的构造:

| Size (int32变长)| User key (string) | sequence number (7 bytes) | value type (1 byte) |

Memtable Key用字符串(slice类型)的形式返回这个值。

有三个函数导出各个key:


  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); }

Comparator

在comparator中有两个方法需要注意:


FindShortestSeparator(std::string* start, const Slice& limit);

FindShortSuccessor(std::string* key)

从实现上来看,leveldb中一共有两个具体实现:


class BytewiseComparatorImpl : public Comparator { ... }

class InternalKeyComparator : public Comparator { ... }

其中BytewiseComparatorImpl关于上述两个函数的实现很简单,没有什么值得讨论的地方。由于InternalKey的内部结构,InternalKeyComparator类中,特意添加了一个user_comparator_的成员用来专门对user key做比较。


class InternalKeyComparator : public Comparator {

 private:

  const Comparator* user_comparator_;

...

}

比较的顺序为:

  1. 首先根据user key按升序排列

  2. 然后根据sequence number按降序排列

  3. 最后根据value type按降序排列

memtable的操作

add

主要就是按照| VarInt(Internal Key size) len | internal key |VarInt(value) len |value|的格式,pack到一块内存中,然后调用:


table_.Insert(buf)

来插入到memtable中去。

get

核心逻辑是一个Seek函数,根据传入的LookupKey得到在memtable中存储的key,然后调用Skip list::Iterator的Seek函数查找。Seek直接调用Skip list的FindGreaterOrEqual(key)接口,返回大于等于key的Iterator。然后取出user key判断时候和传入的user key相同,如果相同则取出value,如果记录的Value Type为kTypeDeletion,返回Status::NotFound(Slice())。

memtable中的跳表

首先在skiplist的构造函数里,会通过NewNode(0, kMaxHeight)初始化head_,然后将node中的next_数组元素初始化为0。注意next_数组元素是class AtomicPointer类型,其实就是为了安全的操作rep_,防止data race造成bug。max_height_被初始化为1


class skiplist {

// skiplist class private data

private:

  Node* const head_;

  port::AtomicPointer max_height_; 

}

struct Node {

private:

// node struct private data

port::AtomicPointer next_[1];

}

上面的第一个class,就是所谓的skiplist,在它的private成员里,head_对应下图HEAD,而下图中的TAIL其实在leveldb的中是用的NULL。

max_heght_在本示例图中是3,即level0-level3。

第二类,也就是struct Node。其实代表一个实节点,那么怎样的节点叫实际节点?就是每个字符对应的节点。我们从level0上看,我们实际有

a-i等9个节点,那么意味着有9个node。那level1和level2算什么?这就是skiplist的特色了。在下面马上就会讨论到。


 HEAD                                                        TAIL

   |----------------------------->e-----------------------------|  (level 2)
   |                              |                             |
   |----------------->c---------->e---------->g-----------------|  (level 1)
   |                  |           |           |                 |
   |<---->a<--->b<--->c<--->d<--->e<--->f<--->g<--->h<--->i<--->|  (level 0)

RandomHeight

随机生成一个1到kMaxHeight之间的高度值。这个值就是level1和level2的生成谜底。每个新生成的node在进行insert时,需要预先进行一个RandomHeight的动作,算出当前的node有占几层,也就是level为多少。除了c,e,g其他的node之所以只有一层,就是因为RandomHeight算出的level为1。

FindGreaterOrEqual

在insert时,leveldb使用的是升序。所以需要首先调用FindGreaterOrEqual来找到合适的位置,”Greater Or Equal“的含义很明显体现了升序的意思,跳过less的,找第一个大于等于自己的node为目标。在具体查找过程中,首先从最顶层开始查找,在没找到合适的位置时,会逐级向下递减层级,当在某一层中找打了合适的插入位置,prev就记录下当前level在当前level中插入节点,所需要的前驱位置。由于leveldb中的skiplist本质上每层level是一个单链表,所以要做插入动作,必须要知道前驱节点,而后继节点在前驱节点的next里。

FindLessThan

原理同FindGreaterOrEqual。

Insert

通过FindGreaterOrEqual找到了合适的位置,然后就需要计算自己的level,这里假设level 小于当前的max_height_。当然如果大于的话,需要对head_做一定的处理,很简单不做讨论。插入的操作很简单:


  x = NewNode(key, height);

  for (int i = 0; i < height; i++) { x->NoBarrier_SetNext(i, prev[i]->NoBarrier_Next(i)); prev[i]->SetNext(i, x); }

这几行代码里,首先需要解释prev是做什么的。它实际是在调用FindGreaterOrEqual时生成的。我们回到看上面的图,演绎一下e节点的插入过程。在FindGreaterOrEqual执行过程,prev[i]指向了c节点位于level1的一个位置,同时prev[i]->NoBarrier_Next指向的是g在level1中的位置。那么e在level1中插入一个占位,然后执行经典的单链表插入指针修改动作。for循环的两句其实就是下面的原理,只不过数据结构复杂一点罢了:


x -> next = p->next;

p->next = x;

你可能感兴趣的:(存储,leveldb)