久聞LevelDB大名,由於課程需要,藉助此次機會對levelDB源碼的幾個主要模塊進行解讀,同時加強對c++的理解。
LevelDB簡介
LevelDB是一個google開源的持久型K-V數據存儲引擎,是一個很好的c++學習源碼。LevelDB的主要特點在於其寫性能十分優秀(在犧牲了部分讀性能的前提下),這也是LSM-Tree的主要特性之一。
LevelDB的安裝這裏不再敘述,詳見LevelDB安裝.
安裝完成之後,基本的打卡數據庫,讀寫數據等操作官網上也很詳細,詳見LevelDB基本操作.
LevelDB主要組件
LevelDB的框架圖如下所示。可以看出其主要分成六個部件:MemTable,Immutable Memtable,log,manifest,current,sstable。
- MemTable是內存中存儲數據的第一個站點,在數據寫入LevelDB中時,首先存儲在內存MemTable數據結構中,其按照用戶定義的順序存儲數據,同時將寫入記錄在log中,當MemTable大小超過一定量時,則將其變成Immutable Memtable,同時創建一個新的MemTable用於存儲新的寫入數據。
- Immutable Memtable 表示在內存中不可更改的數據,後臺進程可以將其持久化到磁盤中的sstable
- log是日誌文件,保證數據不易丟失,每當有寫入操作的時候,就會先把操作寫到log中,當數據還在內存中的時候斷電,此時就可以根據日誌文件恢復。
- manifest文件,每次有sstable增加或者減少(執行了Compaction操作)都會新增一個版本,這也也可以稱爲不同的level,manifest文件就是記錄每次不同的版本變動。
- current文件用來記錄當前的manifest文件名。
- sstable是磁盤中主要用來存儲數據的,LevelDB會定期整合sstable,使得sstable在邏輯上分層,level 0表示內存中的數據直接映射到磁盤上,可能會存在數據交集,而level i層表示整合後的數據。
LevelDB中的數據結構
本節主要介紹兩種LevelDB中的數據結構:跳錶和LSM-Tree。
SkipList
跳錶是一個基於有序鏈表的數據結構,其最大的優點在於可以實現插入和查找都是O(logn)時間複雜度,優於普通鏈表,同時其比平衡樹實現簡單,因此在Redis和LevelDB中都有應用。
結構以及查找過程
跳錶的結構圖如下所示,可以看到其本質上是一個有序鏈表,只不過其每個節點都有不同的高度,因此每個層次的節點也按序相連:
查找並插入節點25的過程圖,圖中紅線代表查找路徑,當其層數>1的時候,如果下一個節點的值比目標值大,則下降一層,當其層數=1時,則可以直接插入節點,由此可見,其查找過程中跳過了3,6,9的查找,因此具有O(logn)的時間複雜度。插入節點的高度是由一個隨機值計算得出:
源碼分析
LevelDB的源碼位於db/skiplist.h。首先看看其節點的定義,Node包含了節點值key,取出和設置next指針的函數,next表示下一個節點的首地址,只有一個元素。
template <typename Key, class Comparator>
struct SkipList<Key, Comparator>::Node {
explicit Node(const Key& k) : key(k) {}
// 每個節點的值
Key const key;
Node* Next(int n) {
assert(n >= 0);
// 取出atomic的值
return next_[n].load(std::memory_order_acquire);
}
void SetNext(int n, Node* x) {
assert(n >= 0);
// 存儲atomic的值
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];
};
下面看SkipList的定義。首先看看成員變量,compare_用於表示節點之間的比較關係,arena_用於給跳錶和節點申請內存,head_是頭結點,max_height_整個跳錶的最大高度,rnd_用於隨機生成節點高度。除此之外,還有在SkipList類內有一個Iterator內置類。成員函數的作用可以由其名可知。
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);
SkipList(const SkipList&) = delete;
SkipList& operator=(const SkipList&) = delete;
// Insert key into the list.
// REQUIRES: nothing that compares equal to key is currently in the list.
void Insert(const Key& key);
// Returns true iff an entry that compares equal to key is in the list.
bool Contains(const Key& key) const;
// Iteration over the contents of a skip list
class Iterator {
public:
// Initialize an iterator over the specified list.
// The returned iterator is not valid.
explicit Iterator(const SkipList* list);
// Returns true iff the iterator is positioned at a valid node.
bool Valid() const;
// Returns the key at the current position.
// REQUIRES: Valid()
const Key& key() const;
// Advances to the next position.
// REQUIRES: Valid()
void Next();
// Advances to the previous position.
// REQUIRES: Valid()
void Prev();
// Advance to the first entry with a key >= target
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
};
private:
enum { kMaxHeight = 12 };
inline int GetMaxHeight() const {
return max_height_.load(std::memory_order_relaxed);
}
Node* NewNode(const Key& key, int height);
int RandomHeight();
bool Equal(const Key& a, const Key& b) const { return (compare_(a, b) == 0); }
// Return true if key is greater than the data stored in "n"
bool KeyIsAfterNode(const Key& key, Node* n) const;
// Return the earliest node that comes at or after key.
// Return nullptr if there is no such node.
//
// If prev is non-null, fills prev[level] with pointer to previous
// node at "level" for every level in [0..max_height_-1].
Node* FindGreaterOrEqual(const Key& key, Node** prev) const;
// Return the latest node with a key < key.
// Return head_ if there is no such node.
Node* FindLessThan(const Key& key) const;
// Return the last node in the list.
// Return head_ if list is empty.
Node* FindLast() const;
// 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_;
};
LSM-Tree
LSM-Tree(Log Structured-Merge Tree)是一個插入性能極佳的結構,傳統的關係型數據庫的數據庫存儲引擎(如mysql的Innodb)都是採用B+樹的形式,而B+樹的好處在於其是一個索引樹,只有葉子節點存儲數據,這樣可以同時兼顧讀寫性能,同時查詢性能更加穩定。而LSM-Tree的優勢在於其能提高寫操作的吞吐量,在一些寫操作頻率>讀操作頻率的場景十分有效。
LSM-Tree原理
首先要清楚一個道理:磁盤的隨機讀寫慢,順序讀寫快。這個其實很好理解,隨機讀寫會將數據存放在不同的磁盤扇區中,這樣數據的讀寫操作就會訪問多個磁盤扇區,而順序讀寫就會將數據儘量放在同一扇區下,這樣使得數據量相同的情況下,順序讀寫訪問的磁盤扇區更少,因此速度更快。一個很好的方法就是將數據存儲在文件中,文件中的數據都是有序的。
LSM-Tree的原理很容易理解,轉子知乎:
將之前使用一個大的查找結構(造成隨機讀寫,影響寫性能),變換爲將寫操作順序的保存到一些相似的有序文件(也就是sstable)中。所以每個文件包含短時間內的一些改動。因爲文件是有序的,所以之後查找也會很快。文件是不可修改的,他們永遠不會被更新,新的更新操作只會寫到新的文件中。讀操作檢查最新的文件。通過週期性的合併這些文件來減少文件個數。
本質上LSM-Tree是利用了將對數據的操作保持在內存中,然後批量將這些操作flush到磁盤上,這樣就犧牲了部分的讀取性能,因此讀取操作要先後去讀內存中的最新數據MemTable,然後讀取不可修改的內存數據Immutable MemTable,如果還是沒有再去磁盤上讀取sstable,這樣讀取性能就會降低很多,因此LevelDB中有設置頁緩存機制(配合LRU)加快讀取速度。
參考博客 :
- https://github.com/google/leveldb
- https://blog.csdn.net/ict2014/article/details/17394259
- https://leveldb-handbook.readthedocs.io/zh/latest/basic.html
- https://www.zhihu.com/question/19887265