跳表
跳表(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函数具体做了下面几件事
- 需要找到待插入元素所在的位置(顺带也找到在每一层链表中,前一个节点是谁)。
- 确定待插入节点所占据的层数(根据需要可能增大跳表的总层数)
- 在待插入节点所占据的每一层,插入该节点(设置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机制,关于这个我们后面文章里再继续探讨。

浙公网安备 33010602011771号