MemTable
MemTable的使用
mem_为内存中写入数据的memtable;imm_为CompactMemTable时临时memtable,用于将memtable的内容写到磁盘。在get操作中,如果在mem_中没有找到key,也会去imm_里面查找,即imm_是只读的。imm_是由mem_赋值而来的指向old mem_的指针,在函数DBImpl::MakeRoomForWrite(bool force)中切换,并用has_imm_标识是否存在。
MakeRoomForWrite中的切换:
imm_ = mem_;
has_imm_.store(true, std::memory_order_release);
mem_ = new MemTable(internal_comparator_);
mem_->Ref();
在CompactMemTable()函数中执行释放
// Commit to the new state
imm_->Unref();
imm_ = nullptr;
has_imm_.store(false, std::memory_order_release);
RemoveObsoleteFiles();
跳表定义
MemTable
实现文件:memtable.h/memtable.cc
定义:
class MemTable {
typedef SkipList<const char*, KeyComparator> Table;
KeyComparator comparator_; // Slice(key, value)的比较函数
int refs_; // 引用计数,调用Ref()时加计数,调用Unref()时减引用
Arena arena_; // 用于跳表的内存分配
Table table_; // 跳表
};
其中,定义struct KeyComparator用于Insert/get中key的比较,
int MemTable::KeyComparator::operator()(const char* aptr,
const char* bptr) const {
// Internal keys are encoded as length-prefixed strings.
// 只从aptr和bptr中解码出来key length和key,返回使用key构造的Slice
Slice a = GetLengthPrefixedSlice(aptr);
Slice b = GetLengthPrefixedSlice(bptr);
return comparator.Compare(a, b);
}
由于在memtable中保存的key为由多个变量编码好的一个字符串,这些变量包括
klength varint32
userkey char[klength]
tag uint64
vlength varint32
value char[vlength]
在插入和查找操作中,在对key进行比较的时候,会从整个大Key中解析出klength和userkey
Get函数实现:
// memtable key查找,如果tag是delete返回not found状态码
// 输入:LookupKey key
// 输出: string* value
bool MemTable::Get(const LookupKey& key, std::string* value, Status* s) {
Slice memkey = key.memtable_key(); // length+userkey+tag
Table::Iterator iter(&table_); // 迭代器初始化node_ = null
iter.Seek(memkey.data()); // 调用skiplist的FindGreaterOrEqual函数
if (iter.Valid()) {
// entry format is:
// klength varint32
// userkey char[klength]
// 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); // varint32最大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); // key
switch (static_cast<ValueType>(tag & 0xff)) { // tag
case kTypeValue: {
Slice v = GetLengthPrefixedSlice(key_ptr + key_length);
value->assign(v.data(), v.size()); // value
return true;
}
case kTypeDeletion:
*s = Status::NotFound(Slice()); // 如果找到的纪录是deletion类型,那么这个key没有找到
return true;
}
}
}
return false;
}
SkipTable
跳表结构
类定义
skiplist.h文件没有对应的cc文件,是模板实现,实现了类:
class SkipList;// 内部定义了自己的Iterator类、Node类
SkipList成员定义:
Comparator const compare_; // 构建之后可以改
Arena* const arena_; // 节点内存分配
Node* const head_; // Node定义:Key const key + std::atomic<Node*> next_[1] next_数组个数=height,0是最低层
std::atomic<int> max_height_; // 最大12,只通过Insert()改,读很快,但是会读到过期数据?
Random rnd_; // 一个随机数产生器,只使用Insert()读写
其中,Node定义比较重要,其包含两个数据成员,一个是key,一个是next_[]数组;其中key是该节点保存的数据,而next_数组这里定义元素个数是12,那next_[11]就表示指向第11层该节点的下一个节点;类里只定义了高度为1因为这个指针个数(层)是动态的,通过动态分配内存方式节省内存
node定义
// Implementation details follow
template <typename Key, class Comparator>
struct SkipList<Key, Comparator>::Node {
explicit Node(const Key& k) : key(k) {}
Key const key; //这个节点保存的数据
// Accessors/mutators for links. Wrapped in methods so we can
// add the appropriate barriers as necessary.
// next_数组的第n个元素
Node* Next(int n) { // n表示level
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);
}
private:
// Array of length equal to the node height. next_[0] is lowest level link.
std::atomic<Node*> next_[1]; // next_[0]是最低层的链接(指向height=0的节点)
};
skiptable的线程读写安全在node里面依靠原子变量及memory barrier实现
SkipList的迭代器:
class Iterator {
const SkipList* list_;
Node* node_;
};
分配新Node:
使用arena分配一段内存,然后在已分配内存上new对象
typename SkipList<Key, Comparator>::Node* SkipList<Key, Comparator>::NewNode(
const Key& key, int height) {
// 分配给Node的内存,为node原来的内存+多出来的next_数组元素内存
char* const node_memory = arena_->AllocateAligned(
sizeof(Node) + sizeof(std::atomic<Node*>) * (height - 1));
return new (node_memory) Node(key); // 在node_memory内存上,使用key构造一个node
}
关键函数实现
FindGreaterOrEqual
// 输入:key key
// pre 数组,大小12
// 功能:从最高层(12)开始查找,找到第0层结束
// 找到当前key在每一层的前向节点prev,返回值为第0层的后向节点next(key不大于next)
template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node*
SkipList<Key, Comparator>::FindGreaterOrEqual(const Key& key,
Node** prev) const {
Node* x = head_;
int level = GetMaxHeight() - 1; // max_height_
while (true) {
Node* next = x->Next(level); // x节点的next_指针指向第level层的下一个节点,从最高层开始查找
if (KeyIsAfterNode(key, next)) {
// key比next大的时候为true,继续遍历该层的链表?
// Keep searching in this list
x = next;
} else {
// key不比next大,纪录这一层的node;已经找到0层结束返回;否则继续找level更低的层
if (prev != nullptr) prev[level] = x;
if (level == 0) {
return next; // 找到第0层结束
} else {
// Switch to next list
level--; // 高度更低的方向继续探索
}
}
}
}
Insert函数:
// memtable使用Add方法写入,key类型为char*,包含key-value
template <typename Key, class Comparator>
void SkipList<Key, Comparator>::Insert(const Key& key) {
// TODO(opt): We can use a barrier-free variant of FindGreaterOrEqual()
// here since Insert() is externally synchronized.
Node* prev[kMaxHeight]; // 12
Node* x = FindGreaterOrEqual(key, prev); // 找到当前key在每一层的前向节点prev,返回值为第0层的后向节点next
// Our data structure does not allow duplicate insertion
assert(x == nullptr || !Equal(key, x->key));
// 随机一个height,满足0<height<=12
// 随机一个height,满足0<height<=12.每次以1/4概率从低到高随机到某一层,低层被随机到的概率更高?
int height = RandomHeight();
if (height > GetMaxHeight()) {
// 为什么会大于max_height_?这max_height_纪录的是什么?这个if语句是不是很少发生?
// 是不是max_height_可以小于kMaxHeight?在skiplist早期,level还没有涨到kMaxHeight的时候会走到这个分支
// 随机设置某个节点的最大高度
for (int i = GetMaxHeight(); i < height; i++) {
// max_height_还没有达到最高,高层还没有节点,所以设置更高层的prev为head_
prev[i] = head_;
}
// It is ok to mutate max_height_ without any synchronization
// with concurrent readers. A concurrent reader that observes
// the new value of max_height_ will see either the old value of
// new level pointers from head_ (nullptr), or a new value set in
// the loop below. In the former case the reader will
// immediately drop to the next level since nullptr sorts after all
// keys. In the latter case the reader will use the new node.
max_height_.store(height, std::memory_order_relaxed);
}
// new新节点,并且设置(0~height)各level新节点的后向节点为各level前向节点的next,各个level前向节点的next为新节点
// 这里for循环从低到高层遍历,防止在写入过程,查找高层可以找到但是到了低层却发现没有的情况
x = NewNode(key, height); // 这个 node只需要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)); // x.next_[i] = pre_[i].next_[i]设置第i层前向节点的next为新节点x的next
prev[i]->SetNext(i, x); // pre[i].next_[i].store(x, std::memory_order_release);设置第i层前向节点的next_[i]为x
}
}
跳表结构
从网上找了个图,比较形象地表示了跳表结构和插入操作
图示中为一个max_height_为4(level为3)的skiplist,每个node包含一个key(数字)以及高度为level的next_指针数组,update[i]->forward[i]的连接线即为node中next_[i]
举个例子:key为6的节点,height为4,其next_依次为:next_[0]为key=7的节点(height为1),next_[1]为key=9的节点(height为2),next_[2]为key=25的节点(height为3),next_[3]为nil
插入key=17的节点过程:
- 从height=4(level=3)找起,head的next_[4]为key=6的节点,key=6 < key=17找到key=6的next_[3]为nil,因此level3中node17的前向节点就是key=6的节点
- level=2,从key=6的节点找起,key=6的节点next_[2] = key为25的结点,25 > 17,所以level2中node17的前向节点为
- 依次找到level=1,0层的前向节点分别为key=9的节点,和key=12的节点
- random一个height得到新节点的height为2
- 遍历level(0 ~ 1) 设置新节点的next_[0], next_[1],以及设置前向节点key9.next_[1]=key17, key12.next[0]=key17
关于skiplist的查找效率:
template <typename Key, class Comparator>
int SkipList<Key, Comparator>::RandomHeight() {
// Increase height with probability 1 in kBranching
static const unsigned int kBranching = 4;
int height = 1;
while (height < kMaxHeight && ((rnd_.Next() % kBranching) == 0)) {
height++;
}
assert(height > 0);
assert(height <= kMaxHeight);
return height;
}
以1/4的概率增加height,height2的节点数为height1节点数的1/4,height3节点数为height2节点数的1/4,以此类推
假设height分布是均匀的,总共n个节点,最高层查找区间约为 n* 1/4 * 1/4 * 1/4 ... = n/(4^max_height) ,其他层的查找为常数项4、4、4...,
Arena
主要实现在arena.h/arena.cc中
arena用于跳表的Node以及key字符串的内存分配
class {
......
char* alloc_ptr_; // 内存buffer指针
size_t alloc_bytes_remaining_; // arena总共可以分配的bytes数
std::vector<char*> blocks_; // 保存已分配空间和alloc_ptr_
std::atomic<size_t> memory_usage_; // blocks_中纪录的所有内存+blocks_自身的内存
};
提供一个AllocateAligned方法一个Allocate方法,AllocateAligned考虑按(void*)位数对齐。buffer够用直接使用buffer分配,buffer不够用使用AllocateFallback方法分配,分配规则:
- 对象size大于一个block的1/4直接分配,新分配内存记录到blocks_;
- 否则,分配Block size大小push到blocks_,并将新分配的block指针赋值给alloc_ptr_,由alloc_ptr_给对象分配,当前alloc_ptr_中的内存就丢掉了
对齐分配内存
// 返回分配好的内存块首地址(alloc_ptr_+x),首地址满足字节对齐
char* Arena::AllocateAligned(size_t bytes) {
const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8; // 与系统相关,align最少按8个字节对齐
static_assert((align & (align - 1)) == 0,
"Pointer size should be a power of 2"); // 验证align是2的幂次
// 这里判断alloc_ptr_是不是按系统位数对齐的
// 数据存储是以字节为单位的,地址按位与对齐字节数,完成字节对齐
size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align - 1);
size_t slop = (current_mod == 0 ? 0 : align - current_mod); // 按void(*)对齐分配空间,current_mod == 0是正好对齐不需要填补
size_t needed = bytes + slop; // 需要分配的字节数
char* result;
if (needed <= alloc_bytes_remaining_) {
// alloc_ptr_里面的内存还够直接分配
result = alloc_ptr_ + slop;
alloc_ptr_ += needed;
alloc_bytes_remaining_ -= needed;
} else {
// AllocateFallback always returned aligned memory
result = AllocateFallback(bytes); // 如果直接分配就不需要考虑字节对齐了,因为总是能对齐
}
assert((reinterpret_cast<uintptr_t>(result) & (align - 1)) == 0);
return result;
}
分配新的block
// 内存分配每次分配一个kBlockSize(4096)字节或者按需
// (block_bytes决定分配给谁)
char* Arena::AllocateNewBlock(size_t block_bytes) {
char* result = new char[block_bytes];
blocks_.push_back(result);
// 加一个sizeof(char*)是blocks_的内存
memory_usage_.fetch_add(block_bytes + sizeof(char*),
std::memory_order_relaxed); // atomic
return result;
}
线程安全问题
由定义可以看到,memtable使用引用计数来保证memtable的析构安全,int ref_为引用计数,在Ref()以及UnRef()过程并没有加锁保证安全,需要在外部使用上保证
但是获取到memtable的指针之后,在memtable的操作上,插入/删除并没有加锁,即对skiptable的操作没有加锁,那skiptable是如何保证线程安全的呢?从可以知道,写请求虽然可以被多个线程接受,但是写memtable是通过队列实现的,同一时间只有一个线程在写,所以是一个一写多读的情况
多个读请求之间是没有问题的,skiptable不支持删除元素,那么只需要一个插入元素线程与多个读取元素之间是安全的,跳表实现上使用了很多的atomic原子操作与memory barrier保证插入元素过程读取安全。