LevelDB學習筆記 (3): 長文解析memtable、跳錶和記憶體池Arena
1. MemTable的基本資訊
我們前面說過leveldb的所有資料都會先寫入memtable中,在leveldb中每個 LevelDB 例項最多會維護兩個 MemTable: mem_ 和 imm_。mem_ 可以讀寫,imm_ 只讀。分別對應了memtable和immutable table。
1.1 首先去看一下db/memtable.h
下面是基本的構造資訊
class MemTable {
public:
// 必須顯示呼叫
explicit MemTable(const InternalKeyComparator& comparator);
// 禁止拷貝構造和拷貝賦值
MemTable(const MemTable&) = delete;
MemTable& operator=(const MemTable&) = delete;
// 增加引用計數
void Ref() { ++refs_; }
//引用計數減一,當引用計數為0時,銷燬當前物件。
void Unref() {
--refs_;
assert(refs_ >= 0);
if (refs_ <= 0) {
delete this;
}
}
下面是memtable的一些基本操作和需要的基本資料結構
有對於memtable的資料寫入和讀取。分別為add和get
以及定義需要儲存資料的跳錶結構和對應的key的比較器
這裡可以發現對應key的add操作是需要對應的seqnumber
這個就相當於對於每次更新的config number、version number的意思。這樣我們可以一直獲取的是最新的資料。。
// add 和 get操作
void Add(SequenceNumber seq, ValueType type, const Slice& key,
const Slice& value);
bool Get(const LookupKey& key, std::string* value, Status* s);
private:
friend class MemTableIterator;
friend class MemTableBackwardIterator;
// 針對於key的比較操作
struct KeyComparator {
const InternalKeyComparator comparator;
explicit KeyComparator(const InternalKeyComparator& c) : comparator(c) {}
int operator()(const char* a, const char* b) const;
};
// 跳錶用於儲存資料
typedef SkipList<const char*, KeyComparator> Table;
~MemTable(); // Private since only Unref() should be used to delete it
KeyComparator comparator_;
int refs_;
// 記憶體分配器
Arena arena_;
Table table_;
};
} // namespace leveldb
1.2 跳錶SkipList
由於memtable中的資料都是放到跳錶中的。所以在深入理解memtable之前我們必須要先來看一下跳錶這個資料結構,看過redis原始碼解析的同學應該對跳錶這個資料結構很熟悉了。而且在演算法導論中也有學到過這個資料結構。跳錶於1990 年,由William Pugh 發表其論文:Skip Lists: A Probabilistic Alternative to Balanced Trees。
說是跳錶其實就是多個單連結串列連線起來,對於一個單連結串列我們的search操作的時間複雜度是\(0(n)\) 。但是如果有一些輔助連結串列的幫助就可以幫我們減少這一時間複雜度
經過下面無聊的證明,跳錶對於查詢的時間複雜度可以變成\(O(logn)\)
SkipList查詢流程
對於查詢72的流程如下
- 首先在第一層遍歷14、79發現72大於14小於79因此進入第二層從14繼續搜尋
- 第二層發現50 < 72 < 79所以進入第三層從50開始繼續搜尋
- 第三層發現66 < 72 < 79所以進入第四層從66開始繼續搜尋
- 在第四層就會找到72.這就是整個的搜尋過程
1.3 LevelDB的SkipList
記憶體順序學習
首先leveldb中是通過了原子操作和記憶體順序做到了無鎖併發。這裡先學習一下記憶體順序(Memory Order)
學習部落格?c++11多執行緒初探
知乎討論?如何理解 C++11 的六種 memory order?
https://github.com/apache/incubator-brpc/blob/master/docs/cn/atomic_instructions.md#memory-fence
在 C11/C++11 中,引入了六種不同的 memory order,可以讓程式設計師在併發程式設計中根據自己需求儘可能降低同步的粒度,以獲得更好的程式效能。這六種 order 分別是:
relaxed, acquire, release, consume, acq_rel, seq_cst
其中memory_order_relaxed
編譯器僅保證load和stroe是原子操作,除此之外並不提供任何跨執行緒的同步。 如果某個操作只要求是原子操作,除此之外,不需要其它同步的保障,就可以使用 Relaxed ordering
。程式計數器是一種典型的應用場景:
#include <cassert>
#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> cnt = {0};
void f()
{
for (int n = 0; n < 1000; ++n) {
cnt.fetch_add(1, std::memory_order_relaxed);
}
}
int main()
{
std::vector<std::thread> v;
for (int n = 0; n < 10; ++n) {
v.emplace_back(f);
}
for (auto& t : v) {
t.join();
}
assert(cnt == 10000); // never failed
return 0;
}
而對於Release-Acquire ordering
這種模型store()
使用memory_order_release
,而load()
使用memory_order_acquire
。這種模型有兩種效果,第一種是可以限制 CPU 指令的重排:
- 在
store()
之前的所有讀寫操作,不允許被移動到這個store()
的後面。 - 在
load()
之後的所有讀寫操作,不允許被移動到這個load()
的前面。
除此之外,還有另一種效果:假設 Thread-1 store()
的那個值,成功被 Thread-2 load()
到了,那麼 Thread-1 在store()
之前對記憶體的所有寫入操作,此時對 Thread-2 來說,都是可見的。
比如下面的例子
std::atomic<bool> ready{ false };
int data = 0;
//thread 1
void producer()
{
data = 100; // A
ready.store(true, std::memory_order_release); // B
}
//thread 2
void consumer()
{
while (!ready.load(std::memory_order_acquire)) // C
;
assert(data == 100); // never failed // D
}
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
-
這個執行流程是對於程式1他會保證A不被移到B的後面
-
對於程式2而言C也不會移到D的後面
-
而當程式2讀到ready為true的時候,就表示程式1中對於data的寫入對於程式2是可見的因此對於D的執行則永遠不會failed
1. 跳錶的定義
- 有指定的最大層數
- 為節點分配空間的
arena
- 指向表頭的指標
head
max_height_
表示目前跳錶最高多少層rnd_
用來生成每個節點層數的隨機數
private:
enum { kMaxHeight = 12 };
inline int GetMaxHeight() const {
return max_height_.load(std::memory_order_relaxed);
}
Node* NewNode(const Key& key, int height);
// Immutable after construction
Comparator const compare_;
Arena* const arena_; // Arena used for allocations of nodes
Node* const head_;
std::atomic<int> max_height_; // Height of the entire list
// Read/written only by Insert().
Random rnd_;
};
跳錶的建構函式
這裡通過傳入的比較器和對應的分配器進行構造
但是這裡有一些需要注意的點。
template <typename Key, class Comparator>
SkipList<Key, Comparator>::SkipList(Comparator cmp, Arena* arena)
: compare_(cmp),
arena_(arena),
head_(NewNode(0 /* any key will do */, kMaxHeight)),
max_height_(1),
rnd_(0xdeadbeef) {
for (int i = 0; i < kMaxHeight; i++) {
head_->SetNext(i, nullptr);
}
}
1. NewNode函式
另外就是給將head_初始化,分配一個空節點,初始化跳錶高度為1,初始化隨機函式(分配一個種子)。
最後for迴圈裡,將頭結點的每一層都初始化。
這裡注意NewNode
利用了一個叫做placement New的操作。來實現對於Node的構造
實際上每個Node都所擁有的大小是會很高度掛鉤,因為這涉及到了next陣列的大小。
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);
}
2. Node函式
Node函式中有節點需要用到的一些幫助函式
比如Next和SetNext。這裡就用到了上面所說的Release-Acquire ordering
在無所鎖的情況下實現原子性的讀寫操作
並且保證當執行緒1寫入next[n]的時候執行緒2會線上程1寫完才會讀到這個值
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.
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);
}
private:
// 記錄了該節點在每一層的next節點
// Array of length equal to the node height. next_[0] is lowest level link.
std::atomic<Node*> next_[1];
};
2. 跳錶實現之-初始化
在db_impl找到了對於mem的初始化。也就是會有對於skiplist的初始化
當我們執行
leveldb::DB *db; leveldb::Options opts; opts.create_if_missing = true; leveldb::Status status = leveldb::DB::Open(opts, "../tmp/testdb1", &db);
這裡的DB:open會執行下面這一行來初始化MemTable
而在memtable.cc
中的MemTable的建構函式中通過initial list實現了對於skipList的構造
3. 跳錶實現之-插入操作
插入整體的思路還是比較簡單的,但是有一些小細節需要注意
1. next_[1]的使用
這裡用next_[1]這樣的char陣列來替代了char*,但本質還是一個單連結串列。
如下面這樣的結構就是
b->c->d->g
2. 插入之前要先找到當前key對應於每一層的所有前驅節點
- 首先從最高層開始尋找,並且讓x等於頭節點。同時獲取頭節點在最高層的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;
while (true) {
Node* next = x->Next(level);
if (KeyIsAfterNode(key, next)) {
// Keep searching in this list
x = next;
} else {
if (prev != nullptr) prev[level] = x;
if (level == 0) {
return next;
} else {
// Switch to next list
level--;
}
}
}
}
3. 邏輯上儲存和物理上儲存的區別
整體區別如上圖。leveldb基本上是完全引用的形式實現了多層的skiplist,這樣是非常節省記憶體的。他這樣的實現得益於在newNode時候的一些小技巧
char* const node_memory = arena_->AllocateAligned(
sizeof(Node) + sizeof(std::atomic<Node*>) * (height - 1));
return new (node_memory) Node(key);
這裡以上圖為例子對於14這個節點他會被分別一個Node + 多一個next_的地址
因為他需要14->next_[1]
這個來儲存42這個節點所在的地址。
4. 整個的insert函式
這個時候再來看insert就比較簡單了,配合註釋應該可以看懂的
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];
//這個函式的作用有(2)
// 1. 找到第一個大於等於key的節點
// 2. 就是初始化prev陣列,讓它分別儲存要插入的key在不同level中的前驅
Node* x = FindGreaterOrEqual(key, prev);
// Our data structure does not allow duplicate insertion
// 要麼x為空 要麼x >key
assert(x == nullptr || !Equal(key, x->key));
int height = RandomHeight();
// 如果想要插入的隨機高度 > 當前的最大高度
// 則更新最大高度
if (height > GetMaxHeight()) {
for (int i = GetMaxHeight(); i < height; i++) {
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);
}
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);
}
這個函式最後還是有一些小技巧
這裡就是單連結串列的插入但是有兩個要注意的地方
- 這裡對於設定x的next指標並沒有用barrier操作,也就是說它的順序是任意的。
- 但是第二個對於prev[i]->next = x 這一操作這裡用了
memory_order_release
也就是說對於上一個操作必須在這個操作之前,這也是為什麼上一個操作可以不用barrier
的原因
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);
}
4. 跳錶實現之-迭代器與查詢
對於跳錶的遍歷leveldb專門定義了迭代器
SkipList<key,Comparator>::Iterator iter(&list)
下面是SkipList迭代器對應的一些函式
class Iterator {
public:
// Initialize an iterator over the specified list.
// The returned iterator is not valid.
explicit Iterator(const SkipList* list);
// 確保當前node不為空
bool Valid() const;
// 返回當前位置的key
const Key& key() const;
// node_ = node_.Next(0)
void Next();
// node _ = node的前驅
void Prev();
// 查詢第一個 key >= target的node
void Seek(const Key& target);
// Position at the first entry in list.
// Final state of iterator is Valid() iff list is not empty.
void SeekToFirst();
// 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
};
1. Prev和FindLessThan
Prev函式是讓node = node->prev也就是把node置為它的前驅。由於我們是單連結串列,所以這裡要找前驅實際上需要一個遍歷。
這裡我們用到了FindLessThan
這個函式
- 還是從最高層出發
- 找到第一個next節點大於等於key的節點
- 返回這個節點即為所求
template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node*
SkipList<Key, Comparator>::FindLessThan(const Key& key) const {
Node* x = head_;
int level = GetMaxHeight() - 1;
while (true) {
assert(x == head_ || compare_(x->key, key) < 0);
Node* next = x->Next(level);
if (next == nullptr || compare_(next->key, key) >= 0) {
if (level == 0) {
return x;
} else {
// Switch to next list
level--;
}
} else {
x = next;
}
}
}
所以Prev
就非常容易的實現了
inline void SkipList<Key, Comparator>::Iterator::Prev() {
// Instead of using explicit "prev" links, we just search for the
// last node that falls before key.
assert(Valid());
node_ = list_->FindLessThan(node_->key);
if (node_ == list_->head_) {
node_ = nullptr;
}
}
2. 利用FindGreaterOrEqual實現Seek函式
template <typename Key, class Comparator>
inline void SkipList<Key, Comparator>::Iterator::Seek(const Key& target) {
node_ = list_->FindGreaterOrEqual(target, nullptr);
}
因為FindGreaterOrEqual函式就是返回大於等於target的第一個節點。這就是seek函式所需要的
5. 跳錶實現之-雜七雜八
1. 隨機高度函式
rnd_.Next()
隨機生成一個數- 然後對4取餘,也就是有1/4的概率高都增長
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;
}
2. Contains函式
contains函式的功能是判斷當前的skiplist中是否有key值存在
- 利用了之前說過的
FindGreaterOrEqual
可以說是封裝的非常好了
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;
}
}
2. MemTable的Add操作
在看Add之前要先看一下,對於每一條資料是以什麼樣的格式放入table中的
1. MemTable的內部編碼引用自
MemTable 中儲存的資料是 key 和 value 編碼成的一個字串,由四個部分組成:
- klength: 變長的 32 位整數(varint 的編碼),表示 internal key 的長度。
- internal key: 長度為 klength 的字串。
- vlength: 變長的 32 位整數(varint 的編碼),表示 value 的長度。
- value: 長度為 vlength 的字串。
對應在程式碼裡就是
這裡的編碼主要有兩種
- 第一種是對klength和vlength的
EncodeVarint32
編碼 - 第二種是對sequcenceNumber + type的
EncodeFixed64
編碼 - 對於key和value的data部分則直接儲存
size_t key_size = key.size();
size_t val_size = value.size();
size_t internal_key_size = key_size + 8; // +8 = (sequence 7 bytes + 1 bytes type)
const size_t encoded_len = VarintLength(internal_key_size) +
internal_key_size + VarintLength(val_size) +
val_size;
char* buf = arena_.Allocate(encoded_len); // 分配記憶體 大小為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);
EncoderVarint32函式
- 首先把字元陣列轉為uint8_t型別的陣列,這樣我們接下來才能表示數字。可以看到,在判斷語句中對數字所需要的位數進行了判斷,如果可以用7個位來表示,那就直接賦值,如果可以用14個位來表示(剩下兩位要用來表示數字是否結束),那就把數字的前7位放到dst[0]中,在把剩餘的7位右移,放到dst[1]中去。如果可以用21個位來表示,那麼前七個位放到dst[0],中間七位放到dst[1],最後七位放到dst[2]。
- 舉例說來,假設我們的dst是一個長度位5的字元陣列,value等於129。那麼dst在記憶體空間中是這樣表示的dst[4] dst[3] dst[2] dst[1] dst[0],每個佔據8位,value的二進位制表示為0000 0000 1000 0001。ptr首先指向dst[0],現在我們通過(ptr++) = 128 | value把最後七位0000 0001放到dst[0]中去,此處發生了截斷,因為ptr是uint8_t型別,而value是uint32_t,所以只會把value的最後七位放到dst[0]中去,放完之後dst[0]等於1000 0001,注意此處最高位的1並不是數字,而是代表數字沒有結束,要繼續操作。接下來我們把中間七位00 0000 1放到dst[1]中去,首先右移七位得到0000 0001,然後直接賦值即可。
版權宣告:本文為CSDN博主「饅頭2870」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處連結及本宣告。
原文連結:https://blog.csdn.net/sjc2870/article/details/112342573
char* EncodeVarint32(char* dst, uint32_t v) {
// Operate on characters as unsigneds
uint8_t* ptr = reinterpret_cast<uint8_t*>(dst);
static const int B = 128;
if (v < (1 << 7)) {
*(ptr++) = v;
} else if (v < (1 << 14)) {
*(ptr++) = v | B;
*(ptr++) = v >> 7;
} else if (v < (1 << 21)) {
*(ptr++) = v | B;
*(ptr++) = (v >> 7) | B;
*(ptr++) = v >> 14;
} else if (v < (1 << 28)) {
*(ptr++) = v | B;
*(ptr++) = (v >> 7) | B;
*(ptr++) = (v >> 14) | B;
*(ptr++) = v >> 21;
} else {
*(ptr++) = v | B;
*(ptr++) = (v >> 7) | B;
*(ptr++) = (v >> 14) | B;
*(ptr++) = (v >> 21) | B;
*(ptr++) = v >> 28;
}
return reinterpret_cast<char*>(ptr);
}
EncodeFixed64函式
這個對於64位的固定編碼就比較通俗易懂了。
依次取低8位放到buffer[i]中
inline void EncodeFixed64(char* dst, uint64_t value) {
uint8_t* const buffer = reinterpret_cast<uint8_t*>(dst);
// Recent clang and gcc optimize this to a single mov / str instruction.
buffer[0] = static_cast<uint8_t>(value);
buffer[1] = static_cast<uint8_t>(value >> 8);
buffer[2] = static_cast<uint8_t>(value >> 16);
buffer[3] = static_cast<uint8_t>(value >> 24);
buffer[4] = static_cast<uint8_t>(value >> 32);
buffer[5] = static_cast<uint8_t>(value >> 40);
buffer[6] = static_cast<uint8_t>(value >> 48);
buffer[7] = static_cast<uint8_t>(value >> 56);
}
EncodeVarint64
emmm這個程式碼等價於上面的EncoderVarint32
函式只不過比上面的更簡潔更大佬
char* EncodeVarint64(char* dst, uint64_t v) {
static const int B = 128;
uint8_t* ptr = reinterpret_cast<uint8_t*>(dst);
while (v >= B) {
*(ptr++) = v | B;
v >>= 7;
}
*(ptr++) = static_cast<uint8_t>(v);
return reinterpret_cast<char*>(ptr);
}
2. Add
隨後的add就是把我們編碼之後的資料insert到skiplist中就可以了
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()]
// 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);
}
這裡有要注意的就是SequenceNumber
了
快照就是根據SequenceNumber
的值來實現的,有了這個數字,就可以保證,讀到的值就是一定的。
key value sequencenumber
a 1 3
a 2 4
a 3 5
如果指定了sequencenumber=4,那麼讀到a的值就是2,而不是3;
3. MemTable的Get操作
1. db_impl.cc中的Get操作
MemTable
的get操作是在整個leveldb讀操作中的一種,因為會先去看MemTable
中是否有資料,這裡先從整個leveldb的get操作入手。來分析整體的呼叫過程
- 先忽略掉這裡的快照操作,如果在options沒有指定快照的話,那麼就回去獲取最新的sequence number
- 獲取互斥鎖
- 獲取本次讀操作的 Sequence Number:如果 ReadOptions 引數的 snapshot 不為空,則使用這個 snapshot 的 sequence number;否則,預設使用 LastSequence(LastSequence 會在每次寫操作後更新)。
- 對於MemTable, Immutable Memtable 和 Current Version(SSTable) 增加引用計數,避免在讀取過程中被後臺執行緒進行 compaction 時“垃圾回收”了。
- 釋放互斥鎖,不同執行緒的請求可以併發執行讀取操作,上面加鎖是因為要修改引用次數,是一個寫操作。
- 構造 LookupKey
- 從 MemTable 查詢
Status DBImpl::Get(const ReadOptions& options, const Slice& key,
std::string* value) {
Status s;
MutexLock l(&mutex_);
SequenceNumber snapshot;
if (options.snapshot != nullptr) {
snapshot =
static_cast<const SnapshotImpl*>(options.snapshot)->sequence_number();
} else {
snapshot = versions_->LastSequence();
}
MemTable* mem = mem_;
MemTable* imm = imm_;
Version* current = versions_->current();
mem->Ref();
if (imm != nullptr) imm->Ref();
current->Ref();
bool have_stat_update = false;
Version::GetStats stats;
// Unlock while reading from files and memtables
{
mutex_.Unlock();
// First look in the memtable, then in the immutable memtable (if any).
LookupKey lkey(key, snapshot);
if (mem->Get(lkey, value, &s)) {
// Done
}
mutex_.Lock();
}
if (have_stat_update && current->UpdateStats(stats)) {
MaybeScheduleCompaction();
}
mem->Unref();
if (imm != nullptr) imm->Unref();
current->Unref();
return s;
}
2.LookupKey的實現
通過user_key和sequence構造
leveldb:LookupKey
用於LevelDB內部查詢的介面
- klength 的型別是 varint32,儲存的是
user_key + tag
的長度 - userkey 就是 Get 介面傳入的 key 引數。
- tag 是 7 位元組 sequence number 和 1 位元組 value type。
- 一個 varint32 最多需要 5 個位元組,tag 需要 8 位元組。所以一個 LookupKey 最多需要分配
[ usize + 13 位元組]
記憶體(usize 是 userkey 的 size)。
LookupKey::LookupKey(const Slice& user_key, SequenceNumber s) {
size_t usize = user_key.size();
size_t needed = usize + 13; // A conservative estimate
char* dst;
if (needed <= sizeof(space_)) {
dst = space_;
} else {
dst = new char[needed];
}
start_ = dst;
dst = EncodeVarint32(dst, usize + 8);
kstart_ = dst;
std::memcpy(dst, user_key.data(), usize);
dst += usize;
EncodeFixed64(dst, PackSequenceAndType(s, kValueTypeForSeek));
dst += 8;
end_ = dst;
}
這裡除了上面提到過的兩種編碼方式還多了一個PackSequenceAndType
其實就是對sequenceNumber << 8 | Type
的一個封裝
static uint64_t PackSequenceAndType(uint64_t seq, ValueType t) {
assert(seq <= kMaxSequenceNumber);
assert(t <= kValueTypeForSeek);
return (seq << 8) | t;
}
3.對Memtable的get操作
最後再來看memtable
中的get操作
- 這裡其實就是利用迭代器在skipList中找到對應的key
- 然後就是對編碼資訊的解碼過程
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()) {
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;
}
程式碼過程分析
-
key.memtable_key()
實際上就是把剛才建立的lookupkey返回
// Return a key suitable for lookup in a MemTable. Slice memtable_key() const { return Slice(start_, end_ - start_); }
-
GetVarint32Ptr
這裡的解碼就像編碼一樣有兩種情況。
- 對應了要解碼的數字小於等於127 也就是
(result & 128) == 0)
的時候直接返回
inline const char* GetVarint32Ptr(const char* p, const char* limit, uint32_t* value) { if (p < limit) { uint32_t result = *(reinterpret_cast<const uint8_t*>(p)); if ((result & 128) == 0) { *value = result; return p + 1; } } return GetVarint32PtrFallback(p, limit, value); }
- 否則的話呼叫
GetVarint32PtrFallback
以129為例子
129會這樣來儲存 |0000 0001| 1000 0001|
剛開始 *p = 10000001
所以 *p & 128 == 1隨後就會執行result |= ((byte & 127) << shift); result = 1
p++之後 *p = 0000 0100
隨後byte無法進入if中而進入esle中
result |= (byte << shift) = 129
隨後返回即可
const char* GetVarint32PtrFallback(const char* p, const char* limit, uint32_t* value) { uint32_t result = 0; for (uint32_t shift = 0; shift <= 28 && p < limit; shift += 7) { uint32_t byte = *(reinterpret_cast<const uint8_t*>(p)); p++; if (byte & 128) { // More bytes are present result |= ((byte & 127) << shift); } else { result |= (byte << shift); *value = result; return reinterpret_cast<const char*>(p); } } return nullptr;
- 對應了要解碼的數字小於等於127 也就是
-
value type
如果對應的型別是
kTypeDeletion
,表示該 key/value 已經從 Memtable 中刪除,這時候將NotFound
儲存在狀態碼中,並且返回。
3. MemTable的刪除操作
追蹤一次刪除操作的發生。
DB:delete -> writeBatch::Delete()
這裡的刪除並不是真的刪除元素,而是新增一條刪除記錄來實現的。真正的刪除操作會在後臺程式中完成,也是為了減少io增大效率。
void WriteBatch::Delete(const Slice& key) {
WriteBatchInternal::SetCount(this, WriteBatchInternal::Count(this) + 1);
rep_.push_back(static_cast<char>(kTypeDeletion));
PutLengthPrefixedSlice(&rep_, key);
}
1. 首先這裡會新增一個kTypeDeletion + key
的字首
然後在WriteBatch::Iterate
中
Slice input(rep_);
if (input.size() < kHeader) {
return Status::Corruption("malformed WriteBatch (too small)");
}
input.remove_prefix(kHeader);
Slice key, value;
int found = 0;
while (!input.empty()) {
found++;
char tag = input[0];
input.remove_prefix(1);
switch (tag) {
//.......
//檢測到了key的字首有kTypeDeletion隨後呼叫
// handler->Delete去skipList中標記key為刪除
case kTypeDeletion:
if (GetLengthPrefixedSlice(&input, &key)) {
handler->Delete(key);
2. 在 handler->Delete
中
實際上這個函式呼叫了mem_->Add
函式.指定其中的keyType = KTypeDeletion
void Delete(const Slice& key) override {
mem_->Add(sequence_, kTypeDeletion, key, Slice());
sequence_++;
}
這樣當下一次獲取當前key的資料的時候,最新的sequenceNumber已經標記它為刪除
4. MemTable-->ImmuTable的轉變
找了半天發現是在這裡實現的MemTable -> ImmuTable
的轉變。具體的邏輯會在後面進行分析,這裡的內容已經足夠多了。
但還是配圖,是在這裡實現了轉變
imm_ = mem_ ; // 把原來的memtable -> immutable
mem_ = new MemTable() // 建立一個新的memTable
5. 記憶體池Arena
最後花點時間在MemTable
用到的記憶體池上。
首先看Arena
類的四個私有變數
// Allocation state
char* alloc_ptr_;
size_t alloc_bytes_remaining_;
// Array of new[] allocated memory blocks
std::vector<char*> blocks_;
// Total memory usage of the arena.
//
// TODO(costan): This member is accessed via atomics, but the others are
// accessed without any locking. Is this OK?
std::atomic<size_t> memory_usage_;
其中如上圖所示,blocks_
是一個包含指向各個塊的char *
陣列,而alloc_ptr_
則表示了當前塊的未使用記憶體區域的起始地址。同時用alloc_bytes_remaining_
來表示當前塊還剩多少記憶體是沒有被使用的
而我們整個的分配過程是從Allocate(size_t bytes)
函式開始的
- 如果要分配的記憶體大小 < 當前塊剩餘記憶體大小的話則直接分配
- 否則要呼叫
AllocateFallback
inline char* Arena::Allocate(size_t bytes) {
// The semantics of what to return are a bit messy if we allow
// 0-byte allocations, so we disallow them here (we don't need
// them for our internal use).
assert(bytes > 0);
if (bytes <= alloc_bytes_remaining_) {
char* result = alloc_ptr_;
alloc_ptr_ += bytes;
alloc_bytes_remaining_ -= bytes;
return result;
}
return AllocateFallback(bytes);
}
AllocateFallback(bytes)
如果當前塊的剩餘記憶體區域不足以進行分配的話則需要呼叫這個函式
static const int kBlockSize = 4096;
- 如果需要分配的大小 > 1KB的話則單獨申請一個
bytes
的快分配給它 - 否則就是申請一個正常大小的塊
4kb
然後在新的塊分配他
char* Arena::AllocateFallback(size_t bytes) {
if (bytes > kBlockSize / 4) {
// Object is more than a quarter of our block size. Allocate it separately
// to avoid wasting too much space in leftover bytes.
char* result = AllocateNewBlock(bytes);
return result;
}
// We waste the remaining space in the current block.
alloc_ptr_ = AllocateNewBlock(kBlockSize);
alloc_bytes_remaining_ = kBlockSize;
char* result = alloc_ptr_;
alloc_ptr_ += bytes;
alloc_bytes_remaining_ -= bytes;
return result;
}
AllocateNewBlock(block_bytes)函式
- 分配一個制定大小的新塊,給它加入
blocks_
陣列中 - 在總的使用記憶體變數中加入新分配的記憶體
char* Arena::AllocateNewBlock(size_t block_bytes) {
char* result = new char[block_bytes];
blocks_.push_back(result);
memory_usage_.fetch_add(block_bytes + sizeof(char*),
std::memory_order_relaxed);
return result;
最後看AllocateAligned(size_t bytes)
函式
這個是類allocate
函式只不過制定了對奇要求。
- 第一行是獲取當前系統的對其規格。一般來講x86-64機器的(void*)大小就是8bytes;
- 唯一的區別就是
needed = bytes + slop
slop是為了對奇所需要多加的bytes
char* Arena::AllocateAligned(size_t bytes) {
const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;
static_assert((align & (align - 1)) == 0,
"Pointer size should be a power of 2");
size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align - 1);
size_t slop = (current_mod == 0 ? 0 : align - current_mod);
size_t needed = bytes + slop;
char* result;
if (needed <= alloc_bytes_remaining_) {
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;
}
關於Arena的記憶體釋放
可以發現整個Arena中沒有類free
釋放,它的整個釋放都在整個物件的解構函式完成
Arena::~Arena() {
for (size_t i = 0; i < blocks_.size(); i++) {
delete[] blocks_[i];
}
而整個Arena
的解構函式會在memTable
物件被釋放的時候進行釋放。