1. 程式人生 > >資料結構-常用樹總結

資料結構-常用樹總結

資料結構-常用樹總結

0x01 摘要

本文會簡單說下常用的樹形結構如AVL樹、紅黑樹、B樹、B+樹的一些知識點,從時間複雜度、使用場景等作對比。

0x02 對比

名稱 簡介 旋轉規則 插入複雜度 刪除複雜度 查詢複雜度 使用場景
AVL樹 高度平衡二叉查詢樹,左右子樹高度差不超過1 不滿足高度平衡就旋轉直到平衡 - 最壞旋轉logN次 O(logN) 查詢多,資料變動少
紅黑樹 根黑葉黑,紅帶兩黑。非葉到葉黑樹相等,且路徑相差不會超過1倍,非高度平衡 旋轉次數較AVL少 較AVL差一些(非高度平衡) 較AVL好,最多旋轉兩次 較AVL差一些(非高度平衡) 資料變動多。Java TreeSet ConcurrentHashMap
B樹 多路查詢樹,分支多、樹矮,磁碟IO少。n個子樹節點包含n+1個關鍵字 - - - - B樹相較B+樹可把HotData放在Non-leaf以快速查詢
B+樹 多路查詢樹,分支多、樹矮,磁碟IO少。n個子樹節點包含n個索引關鍵字,資料存葉節點 - - - - 快速遍歷資料(二分查詢)。檔案系統、索引
Trie樹 字典樹,不同字串的相同字首共享儲存一份 - - - - 字首匹配,統計和排序大量的字串

0x03 二叉搜尋樹(BST)

Binary Search Tree
BST

3.1 簡介

利用左右子樹的規則,來減少查詢時遞迴深度

3.1 規則

  1. 左子樹上所有結點的值均小於或等於它的根結點的值
  2. 右子樹上所有結點的值均大於或等於它的根結點的值
  3. 左、右子樹也分別為BST

3.3 思想

二分查詢

3.4 時間複雜度

O(logN),連結串列時最差O(N),如下圖
連結串列狀的BST

0x04 AVL樹

AVL樹

4.1 簡介

還是一顆BST,但是高度有平衡要求,所以叫平衡二叉搜尋樹

4.2 規則

  • 左右子樹的高度差不能超過1
  • 每個子樹也是一顆AVL

4.3 思想

要求平衡,減少查詢時遞迴的深度

4.4 時間複雜度

注意,以下C為常數

  • 查詢
    O(logN)
  • 插入
    O(logN)查詢+O©旋轉
  • 刪除
    O(logN)查詢+非O©旋轉。最壞情況下需要從被刪節點到根節點這條路徑上所有節點的平衡性,此時旋轉O(logN)

4.5 樹調整規則

當樹結構改動導致不再平衡時,需要旋轉最小失衡子樹。我們先看兩個概念:

  1. 平衡因子:左子樹的高度減去右子樹的高度。AVL樹的平衡因子的取值只可能為[0, 1, -1]
  2. 最小失衡子樹:在新插入的節點向上查詢,以第一個平衡因子的絕對值超過1的節點為根的子樹稱為最小失衡子樹。也就是說,一棵失衡的樹,是有可能有多棵子樹同時失衡的。而這個時候,我們只要調整最小的不平衡子樹,就能夠將不平衡的樹調整為平衡的樹。

下面講一個例子:
AVL樹旋轉例子
上面圖1的[1, 2, 3, 4]本來是一顆AVL,但由於加入了節點5,造成不平衡。此時節點4的平衡因子為-1,節點3為-2。也就是說節點3就是我們要找的最小失衡子樹,需要對其做左旋調整。最後結果如上圖2,是一棵新的AVL樹。

4.6 旋轉

4.6.1 左旋轉

左旋轉
左旋轉即將父節點X下移成為右孩子Y的左孩子,Y成為新的父節點,之前Y的左孩子b成為X的右孩子。旋轉之後仍然符合BST規則。

4.6.2 右旋轉

右旋轉
右旋轉即將父節點X下移成為左孩子Y的右孩子,Y成為新的父節點,之前Y的右孩子b成為X的左孩子。旋轉之後仍然符合BST規則。

0x05 紅黑樹

紅黑樹

5.1 簡介

不再是AVL那樣的高度平衡樹,而是有一套自己的自平衡邏輯。

5.2 規則

  • 節點是紅、黑兩色之一
  • 根節點一定是黑色
  • 葉子節點(是指為空(NIL或NULL)的葉子節點)一定是黑色
  • 如果一個節點是紅色,那麼它的兩個子節點都一定是黑色
  • 從任一節點到其每個後代葉節點的所有簡單路徑都包含相同數目的黑色節點
  • 由上面的一點結論可以推匯出紅黑樹從根到葉子的最長路徑不會超過最短路徑長度的兩倍

5.3 思想

還是一顆BST,但是高度有平衡要求,所以叫平衡二叉搜尋樹

5.4 時間複雜度

注意,以下C, D為常數

  • 查詢
    O(logN)
  • 插入
    O(logN)查詢+O©旋轉+O(D)染色
  • 刪除
    O(logN)查詢+O©旋轉+O(D)染色
    刪除時最多旋轉3次
    這一點是紅黑樹比AVL樹在資料增刪較多的場景下更優秀的重要原因。

5.5 調整規則

請參考教你初步瞭解紅黑樹

5.5.1 插入

紅黑樹的插入規則如下:

  • 在紅黑樹中插入節點時,節點的初始顏色必須是紅色。因為規則5規定每個節點到葉節點必須包含相同數目黑色節點,如果插入的節點是黑色,那就肯定會打破規則5必須調整了。所以設為紅色可以在插入過程中儘量避免對樹的結構進行調整。
  • 初始插入按照二叉查詢樹的性質插入,即找到合適大小的節點,在其左邊或右邊插入子節點。
  • 如果插入節點的父節點為紅,則規則4(紅點孩子都為黑)違背,需要以插入的節點為中心進行旋轉或重塗色。在這步操作後可能仍不滿足規則,則需要將當前節點變換回溯到其父節點或祖父節點,以父節點或祖父節點為中心繼續旋轉或塗色,如此向根節點方向迴圈操作直到滿足紅黑樹的性質。

恢復紅黑樹規則具體策略:

  • 違反規則的節點上移
  • 旋轉或塗色,有五種情況:
    1. 空樹中插入根節點
      此時違反了根節點為黑規則,塗色為紅即可
    2. 插入節點的父節點是黑色
      無需調整
    3. 當前節點的父節點是紅色,且叔叔節點(祖父節點的另一個子節點)也是紅色
      此時違反了紅節點孩子必為黑規則。
      父節點和叔叔節點調整為黑色,祖父節點調整為紅色。如果還是不符合紅黑樹規則,就以祖父節點為焦點進行新的調整。
    4. 當前節點的父節點是紅色,叔叔節點是黑色,當前節點是右子節點
      此時違反了紅節點孩子必為黑規則。
      將父節點作為焦點,進行左旋轉。如果還是不符合紅黑樹規則,就以該新焦點即原父節點做為焦點進行新的調整。
    5. 當前節點的父節點是紅色,叔叔節點是黑色,當前節點是左子節點
      此時違反了紅節點孩子必為黑規則。
      將當前節點的父節點改變為黑色,祖父節點改變為紅色,然後再以祖父節點作為新焦點,做右旋操作。如果還是不滿足規則,就以新焦點按上述策略繼續調整,直到滿足為止。

5.6 應用

插入刪除資料較多場景。

Java中TreeSet HashMap等用了紅黑樹。

0x06 B樹

B樹

6.1 簡介

B樹是一棵多路平衡搜尋樹,他的特點是矮而寬。

適合海量資料無法放入記憶體的情況。試想如果海量資料放在硬碟上,採用上述的AVL樹或紅黑樹,因為他們是二叉樹,所以會導致樹的深度特別深,造成IO次數過多。我們都知道磁碟IO速度是很慢的動作,所以這種情況適合用B樹。

每次IO都能獲得大量的資料,B樹的資料結構很適合內外存資料互動。

6.2 規則

一個 m 階的B樹滿足以下條件:

  • 非葉根結點至少擁有2個子節點
  • 每個節點最多擁有m個子節點
  • 每個節點最多擁有m-1個key
  • 每一個非葉非葉節點至少有 ⌈m/2⌉ 個子節點
  • 有 k 個子節點的非葉子節點擁有 k − 1 個key
  • 所有的葉結點都在同一層

6.3 思想

特別寬,深度淺,減少磁碟IO次數

6.4 時間複雜度

  • 查詢

  • 插入

  • 刪除

0x07 B+樹

下面是一棵3階B+樹
B+樹

7.1 簡介

B+樹是變數變體,但不同的地方是B+樹將真正的資料存放到了葉節點,非葉節點只存放其葉子節點的最小值作為索引關鍵字和指向孩子節點的指標。這樣的好處是,降低樹的高度,可用更少的節點能容納更多的資料。

而且葉節點之間還有指標順序連線,可形成雙向連結串列,便於快速遍歷、範圍查詢。這一點很重要,而B樹在範圍查詢時必須要做中序遍歷,十分低效。

B+ 樹的特點:

  • 所有關鍵字都有序地放置在葉子結點的連結串列中(稠密索引)
  • 非葉子結點相當於是葉子結點的索引(稀疏索引),葉子結點相當於是儲存(關鍵字)資料的資料層;
  • 每個關鍵字的查詢路徑相同,所以查詢相率很穩定
  • 比B樹還寬,深度更淺,進一步減少磁碟IO次數。
  • 葉節點之間有指標相連,形成有序連結串列,範圍查詢效率高

7.2 規則

一個m階樹為例:

  • 根節點子樹[2,m]
  • 非根節點最少包含 ⌈m/2⌉個元素,最多包含m個元素
  • 所有葉子結點都在同一層,且按關鍵字從小到大連線
  • m個關鍵字的節點同時有m個指標指向子樹
  • 所有非葉子節點的關鍵字可以看成是索引部分,這些索引等於其子樹(根結點)中的最大(或最小)關鍵字。例如一個非葉子節點包含資訊: (n,A0,K0, A1,K1,……,Kn,An),其中Ki為關鍵字,Ai為指向子樹根結點的指標,n表示關鍵字個數。即Ai所指子樹中的關鍵字均小於或等於Ki,而Ai+1所指的關鍵字均大於Ki(i=1,2,……,n)。
    B+樹2

7.3 思想

  • 比B樹還寬,深度更淺,進一步減少磁碟IO次數。
  • 可把更多的關鍵字放在同一個內部節點,存放在同一盤塊中。每次能讀入更多的關鍵字到記憶體,IO次數隨之降低。
  • 區域性性原理,一個葉節點就是一個磁碟頁,每次讀入一頁就是多個相連的記錄
  • 葉子節點雙向連結串列,範圍遍歷查詢更快速

7.4 時間複雜度

擁有穩定的時間複雜度,每次都必須從根節點到葉子節點。

  • 查詢
    穩定,跟樹高相關

  • 插入

  • 刪除

7.5 基本操作

7.5.1 查詢

查詢類似於二叉查詢樹,起始於根節點,自頂向下遍歷樹,選擇其分離值在要查詢值的任意一邊的子指標。在節點內部典型的使用是二分查詢來確定這個位置。

7.5.2 插入

  1. 先查詢到需要插入的葉子節點
  2. 如果該葉子節點小於規定個數,直接插入
  3. 否則對該葉節點進行分裂操作。這個過程與B樹不同,B樹的葉節點分裂是將該節點的中間關鍵字上移至其父親節點,剩下兩部分關鍵字分別組成新的左右孩子葉節點;而B+樹分裂葉節點的過程是在上移中間關鍵字後,還在左葉節點儲存這個關鍵字。
  4. 然後將上升到的節點作為焦點,按步驟2-4迴圈操作直到滿足B+樹要求。
    B+樹插入

下面來一個3階B+樹插入例項,注意該樹存的是子節點中最大的值作為關鍵字:

  1. 3階B+樹
    B+樹插入例子1

  2. 插入20
    此時會從根節點自頂向下查詢,直到21,37,44那個葉節點,插入
    B+樹插入例子2

  3. 葉子節點分裂,並上移中間關鍵字,原節點保留該上移的關鍵字
    因為此時葉節點變為20,21,37,44,超出3階B+樹限制,所以該節點分裂。這裡就形成了20,21, 37,44兩個新的左右葉子節點,並將21上移到父節點:
    B+樹插入例子3

  4. 此時父節點變為15,21,44,59,作為新的焦點考慮。他也不滿足3階B+樹要求,所以我們分裂他,並將中間數21上移,得到以下結果:
    B+樹插入例子4

  5. 此時,已經到了根節點,最新值是21,59,97,滿足3階B+樹要求,插入資料完畢。

7.5.3 刪除

刪除操作相對於插入過程更復雜,考慮的情況更多,可以參考圖解B樹和B+樹的插入和刪除操作

7.6 B樹和B+樹的區別

以一個m階樹為例:

  • 關鍵字的數量不同:
    B+樹中非根節點有m個關鍵字,其子結點也有m個,其關鍵字只是起到了一個索引的作用(不存資料,只有索引);而B樹雖然也有m個子樹,但是其只擁有m-1個關鍵字(還存了資料)。
  • 儲存的位置不同:
    B+樹中的資料都儲存在葉子結點上,也就是其所有葉子結點的資料組合起來就是完整的資料;而B樹的資料儲存在所有節點。
  • 非根節點的構造不同:
    B+樹的非根節點僅僅儲存著關鍵字資訊和孩子的指標(這裡的指標指的是磁碟塊的偏移量),也就是說內部結點僅僅包含著索引資訊。所以在相同盤快大小情況下,B+樹的內部結點能存更多的關鍵字,一次性讀入記憶體的關鍵字也越多,IO次數隨之降低;而B樹非根節點存有資料。
    舉個例子,假設磁碟中的一個盤塊容納16bytes,而一個關鍵字2bytes,一個關鍵字具體資訊指標2bytes。一棵9階B-tree(一個結點最多8個關鍵字)的內部結點需要2個盤快。而B+ 樹內部結點只需要1個盤快。當需要把內部結點讀入記憶體中的時候,B 樹就比B+ 樹多一次盤塊查詢時間(在磁碟中就是磁碟片旋轉的時間)。
  • 查詢不同(B+樹查詢效率更穩定):
    B樹在在某個節點找到具體的資料以後就結束查詢;而B+樹則需要通過索引找到葉子結點中的資料才結束,也就是說B+樹的搜尋過程必須經歷從根結點到葉子結點,更穩定。
  • 遍歷方式不同
    B樹遍歷必須不停地訪問每個樹節點;B+樹因為葉節點有指標相連線,所以直接從左到右遍歷查詢即可,對於範圍查詢效率很高。

7.7 B+樹的問題

  • 資料插入時隨機IO
    新資料寫入後,葉子節點超出閾值會發生分裂上移等情況,導致邏輯檢視上連續的葉節點在物理磁碟上往往不連續甚至是分離很遠

7.8 應用

Mysql 索引就是用的B+樹。
InnoDB引擎中將資料和主鍵一起存放到B+樹的葉節點,二級索引也是用了B+樹葉節點存放二級索引列和主鍵。MyIsam引擎是將主鍵和指向該條record的指標存放到B+樹的葉節點中。

0x08 B*樹

B*樹

8.1 簡介

B*樹是B+樹的變體,非葉非根節點之間加入了指向兄弟節點的指標。

8.2 規則

  • B*樹定義了非葉子結點關鍵字個數至少為(2/3)*M,即塊的最低使用率為2/3(而不是B+樹的1/2),提升了節點空間利用率。

  • B*樹的分裂也和B+樹不同,如果臨接的兄弟節點未滿,就把一部分資料移動到兄弟節點;如果兄弟節點已滿,則從當前節點和兄弟節點各拿出1/3的資料建立一個新的節點。這樣的好處是減少了分裂移動和建立新節點的次數。

0x9 2-3-4樹

9.1 簡介

2-3-4樹是一個多叉樹,它的每個節點最多有四個子節點和三個資料項。2-3-4樹和紅黑樹一樣,也是平衡樹,它的效率比紅黑樹稍差,但是程式設計容易。

非葉節點的子節點總是比它含有的資料項多1。如下圖所示:
2-3-4樹

9.2 規則

  1. 任一非葉節點資料項個數只能是[1,2,3],對應的子節點數為[2,3,4];
  2. 所有葉子節點都在同一層,也就是說所有葉子節點到根節點的長度一致;
  3. 每個節點的key從左到右保持了從小到大的順序,兩個key之間的子樹中所有的key一定大於它的父節點的左key,小於父節點的右key,對於3個key的節點,兩兩key之間也是如此。

9.3 思想

2-3-4樹是2-3樹的擴充套件,他又是一棵3階B樹。

9.4 時間複雜度

  • 查詢
    O(log(N/3))

9.5 插入規則

  1. 當前節點key<3,直接插入
  2. key=3,則將改節點分裂為1個父節點+2個子節點,將該父節點向上層節點插入
  3. 重複這個步驟直到滿足2-3-4樹規則

0x10 Trie樹

注意:本章摘自Trie樹
一組單詞,inn, int, at, age, adv, ant 得到的Trie樹如下:
Trie樹

10.1 簡介

Trie樹,即字典樹,又稱單詞查詢樹或鍵樹,是一種樹形結構。典型應用是用於統計和排序大量的字串(但不僅限於字串),所以經常被搜尋引擎系統用於文字詞頻統計。它的優點是最大限度地減少無謂的字串比較,查詢效率比較高。

10.2 規則

  • 根節點不包含字元,除根節點外每一個節點都只包含一個字元。
  • 從根節點到某一節點,路徑上經過的字元連線起來,為該節點對應的字串。
  • 每個節點的所有子節點包含的字元都不相同(注意是子節點不同,而跨代節點可以相同)。

10.3 思想

Trie的核心思想是空間換時間,利用字串的公共字首來降低查詢時間的開銷以達到提高效率的目的。

10.4 查詢

從最開頭的Trie樹圖片中可以看到:

  • 每條邊對應一個字母。
  • 每個節點對應一項字首。葉節點對應最長字首,即單詞本身。
  • 單詞inn與單詞int有共同的字首“in”, 因此他們共享左邊的一條分支,root->i->in。同理,ate, age, adv, 和ant共享字首"a",所以他們共享從根節點到節點"a"的邊。

查詢過程非常簡單。比如要查詢int,順著路徑i -> in -> int就找到了。

10.5 建立

搭建Trie的基本演算法也很簡單,無非是逐一把每個單詞的所有字母逐一插入Trie。插入前先看字首是否存在。如果存在,就共享,否則建立對應的節點和邊。比如要插入單詞add,就有下面幾步:

  1. 考察字首a,發現邊a已經存在。於是順著邊a走到節點a
  2. 考察剩下的字串dd的第一個d,發現從節點a出發,已經有邊d存在。於是順著邊d走到節點ad
  3. 考察最後一個字元d,這下從節點ad出發沒有邊d了,於是建立節點ad的子節點add,並把邊ad->add標記為d
    trie樹2

10.6 應用

10.6.1 TopK

  • 一個文字檔案,大約有一萬行,每行一個詞,要求統計出其中最頻繁出現的前10個詞,請給出思想,給出時間複雜度分析

  • 提示:用trie樹統計每個詞出現的次數,時間複雜度是O(n*le)le表示單詞的平均長度),然後是找出出現最頻繁的前10個詞。當然,也可以用堆來實現,時間複雜度是O(n*lg10)。所以總的時間複雜度,是O(n*le)O(n*lg10)中較大的那一個。

10.6.2 尋找熱門查詢

  • 搜尋引擎會通過日誌檔案把使用者每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度為1-255位元組。假設目前有一千萬個記錄,這些查詢串的重複讀比較高,雖然總數是1千萬,但是如果去除重複和,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的使用者越多,也就越熱門。請你統計最熱門的10個查詢串,要求使用的記憶體不能超過1G。

  • 提示:利用trie樹,關鍵字域存該查詢串出現的次數,沒有出現為0。最後用10個元素的小根推來對字串出現頻率進行排序。

0x11 LSM樹

詳細內容可以參考論文閱讀-The Log-Structured Merge-Tree (LSM-Tree)

11.1 簡介

LSM樹全稱為Log-Structured MergeTree,即日誌結構合併樹。

11.1.1 B樹存在的問題

  • 查詢
    從原理來說,b+樹在查詢過程中應該是不會慢的,但如果資料插入雜亂無序時(比如插入順序是5 -> 10000 -> 3 -> 800,類似這樣跨度很大的資料),就需要先找到這個資料應該被插入的位置然後再插入資料。這個查詢過程如果非常離散,且隨著新資料的插入,葉子節點會逐漸分裂成多個節點,邏輯上連續的葉子節點在物理上往往已經不再不連續,甚至分離的很遠。就意味著每次查詢的時候,所在的葉子節點都不在記憶體中。這時候就必須使用磁碟尋道時間來進行查找了,相當於是隨機IO了。
  • 寫入
    且B+樹的更新基本與插入是相同的,也會有這樣的情況。且還會有寫資料時的磁碟IO。

綜上B樹最大的效能問題就是隨機IO。

11.1.2 可用的解決方案

解決上述隨機IO造成的大量磁碟尋道時間的方案:

  • 可以採用SSD
    傳統的機械硬碟(HDD)執行主要是靠機械驅動頭,包括馬達、碟片、磁頭搖臂等必需的機械部件,它必須在快速旋轉的磁碟上移動至訪問位置,至少95%的時間都消耗在機械部件的動作上。SSD卻不同機械構造,無需移動的部件,主要由主控與快閃記憶體晶片組成的SSD可以以更快速度和準確性訪問驅動器到任何位置。傳統機械硬碟必須得依靠主軸主機、磁頭和磁頭臂來找到位置,而SSD用整合的電路代替了物理旋轉磁碟,訪問資料的時間及延遲遠遠超過了機械硬碟。SSD有如此的“神速”,完全得益於內部的組成部件:主控–快閃記憶體–韌體演算法。
  • LSM樹
    LSM樹
    LSM樹主要思想是通過犧牲磁碟讀效能換取寫的順序性。LSM-tree通過磁碟的順序寫,來達到最優的寫效能,因為這會大大降低磁碟的尋道次數,一次磁碟IO可以寫入多個索引塊。

11.2 思想

Log-Structured思想最早由 Rosenblum和Ousterhout[4]於1992年在研究日誌結構的檔案系統時提出。他們將整個磁碟就看做是一個日誌,在日誌中存放永久性資料及其索引,每次都新增到日誌的末尾;通過將很多小檔案的存取轉換為連續的大批量傳輸,使得對於檔案系統的大多數存取都是順序性的,從而提高磁碟和頻寬利用率,故障恢復速度快。 O’Neil等人受到這種思想的啟發,借鑑了Log不斷追加(而不是修改)的特點,結合B樹的資料結構,提出了一種延遲更新,批量寫入硬碟的資料結構LSM樹及其演算法。LSM樹努力地在讀和寫兩方面尋找一個平衡點,以最小化系統的存取效能的開銷,特別適用於會產生大量插入操作的應用環境。

LSM樹主要思想是通過犧牲磁碟讀效能換取寫的順序性和效能提升,將多次資料修改操作先放在記憶體中形成有序樹,再定時統一合併刷入磁碟
C0與C1
LSM樹的主要設計是劃分不同等級的樹。以兩級樹為例,那麼一份索引資料就由兩棵樹組成,一棵C0樹在記憶體,另一棵C1樹在磁碟。

  • 記憶體中的C0樹可以不一定是B-樹,也可以是AVL、2-3樹等,因為在記憶體中不需要為了減少磁碟IO而強制壓縮樹高度。
  • 而存在於磁碟的樹是一棵類B樹。

寫入流程如下:

  1. 首先向順序日誌檔案中寫一條用於恢復這次插入行為的日誌記錄
  2. 該行資料的索引被插入到常駐記憶體的 C0 樹中
  3. 會適時地將這些C0樹上的資料遷移到磁碟上的C1樹中
  4. 每個索引的搜尋過程都是先C0後C1

具體來說,資料先寫入記憶體中的樹C0,當大小達到閾值後會開啟滾動合併過程:

  1. 首先讀取包含C1樹的葉節點的多頁塊,這會使得C1中的一系列條目駐留到快取中
  2. 每次合併都會去讀取已經被快取的C1樹的一個磁碟頁大小的葉節點
  3. 然後將第二步中讀取到的C1樹葉節點上的條目與C0樹的葉節點條目進行合併,由此減少C0樹的大小
  4. 合併完成後,會為C1樹建立一個已合併的新葉節點(在快取中,填滿後被刷入磁碟)

LSM樹

比如一共有N個數據,按每m個數據就需要排序依次,這樣最終能得到N/m個有序結構。查詢時就挨個在這些有序結構中做二分查詢,直到找到結果或搜尋完畢。

這樣搜尋效率是O(N/m*logm),相較B樹下降較多。所以還用了一些方法來優化:

  1. Bloom filter 布隆過濾器
    就是個帶概率的bitmap,可以快速查出某一個小的有序結構裡有沒有指定的key,可以不再使用二分查詢。效率得到了提升,但付出的是空間代價。注意,Bloom filter可以肯定某個key不存在,但不能肯定該key存在。
  2. 小樹合併為大樹
    就是compact的過程,有個程序不斷地將小樹合併到大樹上,這樣大部分的老資料查詢也可以直接使用logm的方式找到,不需要再進行O(N/m*logm)的查詢了。

11.3 效能

  • B樹,因為它最常用的目錄節點是可快取在記憶體裡的,所以實際上是一種混合資料結構:它結合了低成本磁碟和高成本記憶體,前者用來存放大多數的資料,後者為最熱門的資料提供訪問。
  • 而LSM樹將此層次結構擴充套件到多個層級,並在執行多頁磁碟資料讀取時結合了merge IO的優點。

下圖展示了對於通過B樹以及LSM樹(僅包含記憶體中的C0和和磁碟上的C1樹)的兩種資料訪問模式的資料熱度,縱軸是訪問開銷/MB;橫軸是插入速率/MB
LSM樹-B樹對比

從上圖可以得到以下結論:

  • 最低的訪問速率時,Cold Data的磁碟訪問開銷並太高
  • Warm Data階段,B樹結構的資料訪問成本開銷急劇上升,此時磁碟臂會成為磁碟訪問的主要限制因素;而LSM樹結構的資料訪問成本上升很緩慢
  • 到了Hot Data階段,B樹結構的資料都應該快取到記憶體中了,此時稱之為沸點。使用記憶體快取對B樹來說效果顯著,隨著訪問速率進入Hot Data區域而開銷圖形卻變平緩,甚至更頻繁的訪問也不會導致更高的成本上升;而我們可以看出LSM樹的作用是降低訪問成本,對於任何實際訪問速率的諸如插入和刪除之類的可合併操作,特別是針對Cold Data

此外,很多需要快取B樹的情況,如上圖中的Hot Data階段,其實可以用大部分駐留在磁碟的LSM樹來代替。在這些場景中,由於LSM樹的批處理效應,資料在邏輯訪問速率方面是Hot的,但在磁碟物理訪問的速率方面僅是Warm的。 對於具有大量可合併操作(寫入、刪除)的應用程式而言,這是一個非常重要的優勢。

11.4 LSM-tree讀寫放大

本節內容轉自LSM-tree 基本原理及應用

11.4.1 讀寫放大的定義

讀寫放大(read and write amplification)是 LSM-tree 的主要問題,定義:讀寫放大 = 磁碟上實際讀寫的資料量 / 使用者需要的資料量。注意是和磁碟互動的資料量才算,這份資料在記憶體裡計算了多少次是不關心的。比如使用者本來要寫 1KB 資料,結果你在記憶體裡計算了1個小時,最後往磁碟寫了 10KB 的資料,寫放大就是 10,讀也類似。

11.4.2 寫放大

我們以 RocksDB 的 Level Style Compaction 機制為例,這種合併機制每次拿上一層的所有檔案和下一層合併,下一層大小是上一層的 r 倍。這樣單次合併的寫放大就是 r 倍,這裡是 r 倍還是 r+1 倍跟具體實現有關,我們舉個例子。

假如現在有三層,檔案大小分別是:9,90,900,r=10。又寫了個 1,這時候就會不斷合併,1+9=10,10+90=100,100+900=1000。總共寫了 10+100+1000。按理來說寫放大應該為 1110/1,但是各種論文裡不是這麼說的,論文裡說的是等號右邊的比上加號左邊的和,也就是10/1 + 100/10 + 1000/100 = 30 = r * level。個人感覺寫放大是一個過程,用一個數字衡量不太準確,而且這也只是最壞情況。

11.4.2 讀放大

為了查詢一個 1KB 的資料。最壞需要讀 L0 層的 8 個檔案,再讀 L1 到 L6 的每一個檔案,一共 14 個檔案。而每一個檔案內部需要讀 16KB 的索引,4KB的布隆過濾器,4KB的資料塊(看不懂不重要,只要知道從一個SSTable裡查一個key,需要讀這麼多東西就可以了)。一共 24*14/1=336倍。key-value 越小讀放大越大。

11.5 LSM樹總結

LSM樹特點如下:

  • 延遲、批量更新

  • 寫入放在記憶體,寫效率提升好幾個數量級,不會有B樹磁碟隨機IO情況

  • 讀效率略低,因為需要遍歷多個樹,但有優化。且最近的訪問一般是查最近的資料,往往位於記憶體中的C0樹。而大範圍查詢時可通過樹的目錄節點、多頁塊等技術降低查詢開銷

  • 滾動合併記憶體中的小樹和磁碟上的大樹,提升查詢效率

  • 磁碟中的C1樹的節點為單個磁碟頁大小,根目錄下的每個層級上的單頁節點序列會被打包,然後一起放入連續的多頁磁碟塊中(囊括了根節點以下的節點),利於磁碟順序訪問

  • 記憶體中C0樹有序,可採用AVL樹、2-3樹等;磁碟中的樹C1-Ck也有序,為了減少IO採用類B樹。

  • 資料寫入
    先以順序追加的形式寫入WAL(HLog)。然後將對資料的寫入、修改增量儲存在記憶體中的小樹中(MemStore),這些小樹往往是AVL、2-3樹這樣的排序樹或是SkipList跳錶,所以小樹結構是有序的。大小達到閾值後再和磁碟上的大樹(這些磁碟中的樹基於減少IO次數的考慮,需要壓得很低,所以一般是類B樹)合併,並將合併結果先寫入快取,當頁寫滿後再flush到磁碟,注意此時資料依然有序。這樣絕對沒有磁碟隨機IO的問題,大大提升寫入效能。

  • 資料讀取
    但在資料讀取時可能還需要合併記憶體修改資料和磁碟歷史資料,並遍歷查詢多棵樹,所以讀效能降低。所以可在磁碟樹中使用BloomFilter進行優化。

  • 資料刪除
    刪除的時候會現在LSM樹的記憶體中的C0樹查詢,若果沒有就建立一個類似墓碑的條目到C0樹,以後的查詢就會因為找到這個key value對應的墓碑標記而忽略此條目。真正的刪除是當合並的時候,墓碑和對應的舊資料相遇是反生湮滅。

11.5 應用

LSM樹適用於長期具有高頻次資料更新而查詢較少的場景。

11.5.1 HBase

  • 寫入
  1. 先寫入WAL的HBase實現 -> HLog,方式是順序磁碟追加
  2. 然後寫入對應列簇的Store中的MemStore
  3. MemStore大小達到閾值後會被刷入磁碟成為StoreFile。注意此檔案內部是根據RowKey, Version, Column排序,但多個StoreFile之間在合併前是無序的。
  4. HBase會定時把這些小的StoreFile合併為大StoreFile(B+樹),減少讀取開銷(類似於LSM中的樹合併)
  • 讀取
    先搜尋記憶體小樹即MemStore,不存在就到StoreFile中尋找
  • 讀取優化
    1. 布隆過濾器。可快速得到是否資料不在該集合,但不能100%肯定資料在這個集合,即所謂假陽性。
    2. 合併。合併後,就不用再遍歷繁多的小樹了,直接找大樹。
  • 刪除
    新增<key, del>標記,在Major Compact中被刪除的資料和此墓碑標記才會被真正刪除。
  • 合併
    HBase Compact過程,就是RegionServer定期將多個小StoreFile合併為大StoreFile,也就是LSM小樹合併為大樹。這個操作的目的是增加讀的效能,否則搜尋時要讀取多個檔案。HBase中合併有兩種:
    1. Minor Compact
      僅合併少量的小HFile
    2. Major Compact
      合併一個Region上的所有HFile,此時會刪除那些無效的資料(更新時,老的資料就無效了,最新的那個<key, value>就被保留;被刪除的資料,將墓碑<key,del>和舊的<key,value>都刪掉)。很多小樹會合併為一棵大樹,大大提升度效能

11.5.2 InfluxDB

InfluxDB是一個時序資料庫,使用了LSM樹理念。對於時序資料而言,LSM tree的讀寫效率很高。但是熱備份以及資料批量清理的效率不高。

InfluxDB內更新資料也用了LSM樹延時批量處理方法,刪除資料也有LSM樹墓碑概念。

11.5.3 LevelDB

注:本小節大量內容轉自
LSM Tree 學習筆記
作者:fatedier

11.5.3.1 儲存模型
  • WAL
    在設計資料庫的時候經常被使用,當插入一條資料時,資料先順序寫入 WAL 檔案中,之後插入到記憶體中的 MemTable 中。這樣就保證了資料的持久化,不會丟失資料,並且都是順序寫,速度很快。當程式掛掉重啟時,可以從 WAL 檔案中重新恢復記憶體中的 MemTable。

  • MemTable
    MemTable 對應的就是 WAL 檔案,是該檔案內容在記憶體中的儲存結構,通常用 SkipList跳錶 來實現。MemTable 提供了 k-v 資料的寫入、刪除以及讀取的操作介面。其內部將 k-v 對按照 key 值有序儲存,這樣方便之後快速序列化到 SSTable 檔案中,仍然保持資料的有序性。

  • Immutable Memtable
    顧名思義,Immutable Memtable 就是在記憶體中只讀的 MemTable,由於記憶體是有限的,通常我們會設定一個閥值,當 MemTable 佔用的記憶體達到閥值後就自動轉換為 Immutable Memtable,Immutable Memtable 和 MemTable 的區別就是它是隻讀的,系統此時會生成新的 MemTable 供寫操作繼續寫入。之所以要使用 Immutable Memtable,就是為了避免將 MemTable 中的內容序列化到磁碟中時會阻塞寫操作。

  • SSTable
    SSTable
    SSTable 就是 MemTable 中的資料在磁碟上的有序儲存,其內部資料是根據 key 從小到大排列的。通常為了加快查詢的速度,需要在 SSTable 中加入index資料索引,可以快讀定位到指定的 k-v 資料。
    SSTable 通常採用的分級的結構,例如 LevelDB 中就是如此。MemTable 中的資料達到指定閥值後會在 Level 0 層建立一個新的 SSTable。當某個 Level 下的檔案數超過一定值後,就會將這個 Level 下的一個 SSTable 檔案和更高一級的 SSTable 檔案合併,由於 SSTable 中的 k-v 資料都是有序的,相當於是一個多路歸併排序,所以合併操作相當快速,最終生成一個新的 SSTable 檔案,將舊的檔案刪除,這樣就完成了一次合併過程。
    LSM樹合併

11.5.3.2 寫入

MemTable寫入
對應於使用LSM的LevelDB來說,對於一個寫操作流程如下:

  1. 先寫入WAL檔案,這個過程是追加的方式順序寫磁碟
  2. key-value資料寫入到MemTable(往往是SkipList實現)中
  3. 當MemTable達到一定的限制後,這部分轉成immutable MemTable(不可寫,只讀)
  4. 當Immutable MemTable達到一定限制,將flush到磁碟中,即SSTable
  5. SSTable再定期進行compaction操作
11.5.3.3 更新

更新操作其實並不真正存在,和寫入一個 k-v 資料沒有什麼不同,只是在讀取的時候,會從 Level0 層的 SSTable 檔案開始查詢資料,資料在低層的 SSTable 檔案中必然比高層的檔案中要新,所以總能讀取到最新的那條資料。也就是說此時在整個 LSM 的多個樹中可能會同時存在多個 key 值相同的資料,只有在之後合併 SSTable 檔案的時候,才會將舊的值刪除。

11.5.3.4 刪除

刪除一條記錄的操作比較特殊,並不立即將資料從檔案中刪除,而是記錄下對這個 key 的刪除操作標記,同插入操作相同,插入操作插入的是 k-v 值,而刪除操作插入的是 k-del 標記(墓碑),只有當合並 SSTable 檔案時才會真正的刪除。

11.5.3.5 合併

當資料不斷從 Immutable Memtable 序列化到磁碟上的 SSTable 檔案中時,SSTable 檔案的數量就不斷增加,而且其中可能有很多更新和刪除操作並不立即對檔案進行操作,而只是儲存一個操作記錄,這就造成了整個 LSM Tree 中可能有大量相同 key 值的資料,佔據了磁碟空間。

為了節省磁碟空間佔用,控制 SSTable 檔案數量,需要將多個 SSTable 檔案進行合併,生成一個新的 SSTable 檔案。比如說有 5 個 10 行的 SSTable 檔案要合併成 1 個 50 行的 SSTable 檔案,但是其中可能有 key 值重複的資料,我們只需要保留其中最新的一條即可,這個時候新生成的 SSTable 可能只有 40 行記錄。

通常在使用過程中我們採用分級合併的方法,其特點如下:
每一層都包含大量 SSTable 檔案,key 值範圍不重複,這樣查詢操作只需要查詢這一層的一個檔案即可。(第一層比較特殊,key 值可能落在多個檔案中,並不適用於此特性)
當一層的檔案達到指定數量後,其中的一個檔案會被合併進入上一層的檔案中。

11.5.3.6 讀取

MemTable讀取
LSM Tree 的讀取效率並不高,當需要讀取指定 key 的資料時,先在記憶體中的 MemTable 和 Immutable MemTable 中查詢,如果沒有找到,則繼續從 Level 0 層開始,找不到就從更高層的 SSTable 檔案中查詢,如果查詢失敗,說明整個 LSM Tree 中都不存在這個 key 的資料。如果中間在任何一個地方找到這個 key 的資料,那麼按照這個路徑找到的資料都是最新的。

在每一層的 SSTable 檔案的 key 值範圍是不重複的,所以只需要查詢其中一個 SSTable 檔案即可確定指定 key 的資料是否存在於這一層中。Level 0 層比較特殊,因為資料是 Immutable MemTable 直接寫入此層的,所以 Level 0 層的 SSTable 檔案的 key 值範圍可能存在重複,查詢資料時有可能需要查詢多個檔案。

11.5.3.7 優化讀取

因為這樣的讀取效率非常差,通常會進行一些優化,例如 LevelDB 中的 Mainfest 檔案,這個檔案記錄了 SSTable 檔案的一些關鍵資訊,例如 Level 層數,檔名,最小 key 值,最大 key 值等,這個檔案通常不會太大,可以放入記憶體中,可以幫助快速定位到要查詢的 SSTable 檔案,避免頻繁讀取。

另外一個經常使用的方法是布隆解析器(Bloom filter),布隆解析器是一個使用記憶體判斷檔案是否包含一個關鍵字的有效方法。

11.5.3.8 小結

由於時間序列資料庫的特性,運用 LSM Tree 的演算法非常合適。持續寫入資料量大,資料和時間相關,編碼到 key 值中很容易使 key 值有序。讀取操作相對來說較少,而且通常不是讀取單個 key 的值,而是一段時間範圍內的資料,這樣就把 LSM Tree 讀取效能差的劣勢縮小了,反而由於資料在 SSTable 中是按照 key 值順序排列,讀取大塊連續的資料時效率也很高。

11.5.4 Cassandra

LSM Tree的樹節點可以分為兩種:

  • 儲存在記憶體中的稱之為MemTable,
  • 儲存在磁碟上的稱之為SSTable.

LSM樹合併

  • 寫操作寫入記憶體中MemTable,效率很高

  • 每層SSTable檔案到達一定條件後,進行合併操作,然後上移到更高層。合併操作在實現上一般是策略驅動、可外掛化的。比如Cassandra的合併策略可以選擇SizeTieredCompactionStrategyLeveledCompactionStrategy

  • SSTable合併

  1. SSTable合併策略為歸併排序
  2. 按key合併
  3. 合併到高層可能對應到多個檔案,寫放大
  4. Cassandra提供了策略進行合併檔案的選擇,還提供了合併時I/O的限制,以期減少合併操作對上層業務的影響

合併


  • 讀的時候和前面提到的HBase思想類似
  1. 也是先讀記憶體中的MemTable
  2. 如果第一步沒找到,就去覆蓋該key range的所有SStable遍歷查詢
  3. 第二部效率低,但能加入布隆過濾器,可以非精確地判斷key是不是在某個結SSTable中,存在一定假陽性。

11.5.5 Kudu

本小節內容轉自以下地址:
連結:http://www.imooc.com/article/256564
作者:慕虎7371278
來源:慕課網

Kudu支援OLAP和OLTP。為了更好的支援OLAP,Kudu對LSM做了一些優化。

Kudu中的LSM-MemTable實現叫MemRowSet,SSTable的實現叫DisRowSet(列式儲存)。對於列式儲存,讀取一個記錄需要分別讀每個欄位,因此kudu精心設計了RowSet中的索引(針對併發訪問等改進過的B樹),加速這個過程。

除了列式儲存,Kudu還保證一個key只可能出現在一個RowSet中,並分別記錄了RowSet的最大值和最小值,有利於範圍查詢。

這也意味著,對於資料更新,不能再像之前一樣直接插入MemTable即可。需要找到對應的RowSet去更新,為了保持寫吞吐,kudu並不直接更新RowSet,而是又新建一個DeltaStore,專門記錄資料的更新。所以,後臺除了RowSet的Compaction執行緒,還要對DeltaStore進行merge和apply。從權衡的角度考慮,Kudu其實是犧牲了一點寫效率和單記錄查詢效率,換取了批量查詢效率。

11.5.4 其他

RocksDB

0x12 一些好文

漫畫:什麼是紅黑樹?

【資料結構和演算法05】 紅-黑樹(看完包懂~)

日誌結構的合併樹 The Log-Structured Merge-Tree

Log Structured Merge Trees(LSM) 原理

詳解SSTable結構和LSMTree索引

LSM Tree 學習筆記

LSM樹的不同實現介紹

LSM樹原理、應用與優化-淺談大資料原理(二)

0xFF 參考文件

AVL樹,紅黑樹,B樹,B+樹,Trie樹都分別應用在哪些現實場景中?

漫畫:什麼是紅黑樹?

B樹、B+樹、紅黑樹、AVL樹比較

B樹和B+樹的總結

ConcurrentHashMap與紅黑樹實現分析Java8

2-3樹與2-3-4樹

【資料結構和演算法06】2-3-4樹

B+樹的實現

淺談AVL樹,紅黑樹,B樹,B+樹原理及應用

SSD固態硬碟的結構和基本工作原理概述

舉例講解B+樹與LSM樹的區別與聯絡

The Log-Structured Merge-Tree (LSM-Tree) 論文

LSM-tree 一種高效的索引資料結構

日誌結構的合併樹 The Log-Structured Merge-Tree

InfluxDB引擎淺析

知乎-關於LSM樹

LSM Tree/MemTable/SSTable基本原理

B樹、B+樹、LSM樹以及其典型應用場景

LSM樹的不同實現介紹

LSM樹原理、應用與優化-淺談大資料原理(二)