ninja_ken  

跳表

跳表(SkipList)是一种针对查询效率优化过的有序链表,其基本形式是一个多层链表,其中每一层链表的元素都是有序的。最底层的链表包含了所有的数据,而自最底层向上,每一层链表所包含的元素数目递减,如下图所示

在跳表中查找某个元素时,从上图中的左上角开始,逐渐向右下方移动,无须遍历整个链表就可定位到目标元素的位置。

LevelDB中跳表的实现

跳表源码在db/skiplist.h,是用模板实现的,如下所示

template <typename Key, class Comparator>
class SkipList {
 private:
  struct Node;

 public:
  // Create a new SkipList object that will use "cmp" for comparing keys,
  // and will allocate memory using "*arena".  Objects allocated in the arena
  // must remain allocated for the lifetime of the skiplist object.
  explicit SkipList(Comparator cmp, Arena* arena);
 ...
 private:
  enum { kMaxHeight = 12 };
  ...
    // Immutable after construction
  Comparator const compare_;
  Arena* const arena_;  // Arena used for allocations of nodes

  Node* const head_;

  // Modified only by Insert().  Read racily by readers, but stale
  // values are ok.
  std::atomic<int> max_height_;  // Height of the entire list

  // Read/written only by Insert().
  Random rnd_;
};

template模板参数中,Key为跳表中的元素数据类型,Comparator为元素大小比较的函数对象(function object)类型。在SkipList构造函数中,需要显示提供一个comparator用于排序,以及一个Arena对象的指针(Arena是LevelDB源码中的一个辅助类,它用于提高内存的分配和使用效率)。SkipList的数据成员中,head_是跳表头节点(上图中最左边的那个节点),max_height表示当前跳表的最大层数(最大kMaxHeight = 12层),rnd_是一个辅助类实例,用于生成随机数(后面会再提到)。下面我们重点分析下,SkipList的Insert方法。

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

插入的过程,可以理解为向SkipList中插入一列(没错,把上图SkipList中的每一个节点都看成一个列)。Insert函数具体做了下面几件事

  1. 需要找到待插入元素所在的位置(顺带也找到在每一层链表中,前一个节点是谁)。
  2. 确定待插入节点所占据的层数(根据需要可能增大跳表的总层数)
  3. 在待插入节点所占据的每一层,插入该节点(设置prev和next)

关于第一步,由FindGreaterOrEqual函数来实现,它做的基本就是从左上角开始向右下角移动,直至在最底层找到待插入元素的位置;
第二步,由随机数生成器的RandomHeight()实现。如果层数大于当前跳表的最大层数,就修改max_height_,并把超过当前跳表最大层数的那些层的前节点prev,都设置为跳表头节点head_。注意,head_在SkipList的构造函数中,自动已被设置为最大层数12层,也就是说head_头节点默认占据了所有层

第三步插入节点,没有太多需要提的,除了atomic操作memory order的考虑(上面第二步max_height_的修改也涉及了这个,不得不赞叹高性能代码对于细节的考究!如果感兴趣,请留言我们来一起讨论)。

LevelDB中的跳表应用

LevelDB中跳表的应用是Memtable。顾名思义,Memtable是内存中的table,其声明如下

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);
  ...
 private:
  typedef SkipList<const char*, KeyComparator> Table;
  KeyComparator comparator_;
  ...
  Table table_;

在类DBImpl中,Memtable作为其数据成员出现

class DBImpl : public DB {
 ...
 private:
  MemTable* mem_;
  MemTable* imm_ GUARDED_BY(mutex_);  // Memtable being compacted
  std::atomic<bool> has_imm_;         // So bg thread can detect non-null imm_
  WritableFile* logfile_;
  uint64_t logfile_number_ GUARDED_BY(mutex_);
  log::Writer* log_;

其中mem_表示当前内存中未被compact的Memtable,imm_表示正在被compact的Memtable(这个imm_只有发生compact的时候才存在,关于Compact的逻辑,后面会有另一篇文章来专门解析)。用户每次向数据库中插入一个新的键值对时,首先会写入logfile中,之后再写入Memtable(跳表),如下代码所示

Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates) {
...
    {
      mutex_.Unlock();
      status = log_->AddRecord(WriteBatchInternal::Contents(write_batch));
      bool sync_error = false;
      if (status.ok() && options.sync) {
        status = logfile_->Sync();
        if (!status.ok()) {
          sync_error = true;
        }
      }
      if (status.ok()) {
        status = WriteBatchInternal::InsertInto(write_batch, mem_);
      }
      mutex_.Lock();
      if (sync_error) {
        // The state of the log file is indeterminate: the log record we
        // just added may or may not show up when the DB is re-opened.
        // So we force the DB into a mode where all future writes fail.
        RecordBackgroundError(status);
      }
    }

当Memtable的大小达到阈值后(db_impl.cc,mem_->ApproximateMemoryUsage() > options_.write_buffer_size),会触发Compact机制,关于这个我们后面文章里再继续探讨。

posted on 2022-06-10 12:58  ninja_ken  阅读(317)  评论(0)    收藏  举报