LevelDB源碼解讀——簡介及數據結構

久聞LevelDB大名,由於課程需要,藉助此次機會對levelDB源碼的幾個主要模塊進行解讀,同時加強對c++的理解。

LevelDB簡介

LevelDB是一個google開源的持久型K-V數據存儲引擎,是一個很好的c++學習源碼。LevelDB的主要特點在於其寫性能十分優秀(在犧牲了部分讀性能的前提下),這也是LSM-Tree的主要特性之一。
LevelDB的安裝這裏不再敘述,詳見LevelDB安裝.
安裝完成之後,基本的打卡數據庫,讀寫數據等操作官網上也很詳細,詳見LevelDB基本操作.

LevelDB主要組件

LevelDB的框架圖如下所示。可以看出其主要分成六個部件:MemTable,Immutable Memtable,log,manifest,current,sstable。

  1. MemTable是內存中存儲數據的第一個站點,在數據寫入LevelDB中時,首先存儲在內存MemTable數據結構中,其按照用戶定義的順序存儲數據,同時將寫入記錄在log中,當MemTable大小超過一定量時,則將其變成Immutable Memtable,同時創建一個新的MemTable用於存儲新的寫入數據。
  2. Immutable Memtable 表示在內存中不可更改的數據,後臺進程可以將其持久化到磁盤中的sstable
  3. log是日誌文件,保證數據不易丟失,每當有寫入操作的時候,就會先把操作寫到log中,當數據還在內存中的時候斷電,此時就可以根據日誌文件恢復。
  4. manifest文件,每次有sstable增加或者減少(執行了Compaction操作)都會新增一個版本,這也也可以稱爲不同的level,manifest文件就是記錄每次不同的版本變動。
  5. current文件用來記錄當前的manifest文件名。
  6. sstable是磁盤中主要用來存儲數據的,LevelDB會定期整合sstable,使得sstable在邏輯上分層,level 0表示內存中的數據直接映射到磁盤上,可能會存在數據交集,而level i層表示整合後的數據。
    LevelDB框架圖

LevelDB中的數據結構

本節主要介紹兩種LevelDB中的數據結構:跳錶和LSM-Tree。

SkipList

跳錶是一個基於有序鏈表的數據結構,其最大的優點在於可以實現插入和查找都是O(logn)時間複雜度,優於普通鏈表,同時其比平衡樹實現簡單,因此在Redis和LevelDB中都有應用。

結構以及查找過程

跳錶的結構圖如下所示,可以看到其本質上是一個有序鏈表,只不過其每個節點都有不同的高度,因此每個層次的節點也按序相連:
跳錶結構圖
查找並插入節點25的過程圖,圖中紅線代表查找路徑,當其層數>1的時候,如果下一個節點的值比目標值大,則下降一層,當其層數=1時,則可以直接插入節點,由此可見,其查找過程中跳過了3,6,9的查找,因此具有O(logn)的時間複雜度。插入節點的高度是由一個隨機值計算得出:
插入節點25流程圖

源碼分析

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)加快讀取速度。

參考博客 :

  1. https://github.com/google/leveldb
  2. https://blog.csdn.net/ict2014/article/details/17394259
  3. https://leveldb-handbook.readthedocs.io/zh/latest/basic.html
  4. https://www.zhihu.com/question/19887265
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章