1. 程式人生 > >資料對映--B樹

資料對映--B樹

難得一篇文章能從較高的角度介紹B樹而不是陷入了實現細節

我們在之前介紹了很多有序的樹,什麼平衡有序二叉樹,skiplist,有序陣列。不過,這些樹都有個共有的特性,就是不適合於ssd與磁碟。

那麼本週開始,我會開始介紹一些面向磁碟和ssd的儲存結構。

本週呢我們就來介紹面向磁碟結構一種最長見的資料結構 -- B樹。他應該是大家在日常接觸最多的資料結構之一了~ 因為只要你在使用資料庫,你就是在用B樹。甚至當你在用hbase的時候,他其實也只是個分散式的大B樹而已。

我們一直都在強調,硬體是骨頭,軟體是肉。軟體的目標就是儘可能的發揮硬體的技術特性,並儘可能的繞開硬體的限制。

那麼,作為骨頭的磁碟,具有什麼樣的硬體特性呢?

在之前的文章中,我們已經給大家介紹過了磁碟的一些具體的技術特性,下面我們用一句話概括一下,如果要發揮磁碟的全部特性,軟體需要滿足的技術特點:一次讀取或寫入固定大小的一塊資料,並儘可能減少隨機查詢這個操作的次數(因為隨機查詢意味著隨機尋道)。嗯 我是特意沒有寫順序讀寫這個操作的,因為我認為,只要能做到上面兩個條件,那麼順序讀寫就能夠自然而然的做到。

而對於ssd來說,如果要發揮ssd的全部特性,那麼軟體需要滿足的技術特點是:一次讀取或寫入固定大小的一塊資料,並儘可能的減少刪除這個操作的次數(因為ssd的擦除操作需要的代價比較大)

通過上面的兩個介紹,你一定會發現,無論ssd還是磁碟,他們都有一個共性的要求是,一次讀寫固定大小的一塊資料。

不知道這時候大家會不會立刻聯想到一個數據結構?對,就是陣列。 陣列的特性就是擁有固定的大小。

但是,回憶一下之前我們說過的: 有序陣列有一個最大的難點就在於如何能夠讓陣列以更便宜的方式來實現陣列的自動擴充套件。

好,鋪墊了這麼多,我們終於要開始進入正題了~

因為陣列的大小是固定的,那麼如果想擴充套件怎麼辦呢?一種能夠想到的方式是,每次滿了就建立一個新陣列,然後資料全複製到新陣列中 但這樣做有個很大的問題,是每一次都需要做一次資料的全拷貝,代價相對比較大。

另外一種方式是,保持陣列大小不變,但增加陣列的個數,不是也可以增加整個資料結構承載資料的總量麼?

這個思路就是b樹的核心思路,另外這裡有個小八卦要給大家說一下,大家經常看到的

b樹,b-樹,其實是同一類結構,b-樹不是“b減樹的意思哦~


那麼b樹的核心是幾個關鍵詞

1.      樹高:一般來說,樹的高度要比二叉平衡樹低很多

2.      陣列:每一個node,都是一個“陣列”,陣列是很關鍵的決定性因素,我們後面寫入和讀取分析的時候會講到。

然後我們進行一下讀取和寫入的模擬。

從讀取來說:如果我要查詢28這個資料對應的value是多少,路徑大概是:首先走root節點,取出root node後,對該陣列進行二分查詢,發現35>28>17,所以進入branch節點中的第二個節點,取出該節點後再進行二分查詢。發現30>28>26,所以進入branch節點的p2 value,取出該節點,對該三個值的陣列進行二分查詢,從而定位到28這個資料的對應value

而寫入刪除則涉及到分裂和合並這兩個btree最重要的操作,比如,要寫入37,那麼會先找到36所應該被插入的陣列[36,60]這個陣列,然後判斷其是否有空,如果有空,則對該陣列進行重新排序。而如果沒有空,則必須要進行分裂。分裂的緣由是因為組成b-tree的每一個node,都是一個數組,陣列最大的特性是,陣列內元素個數是固定的。因此必須要把原有已經滿掉的數組裡面的一半的資料拿出來,放到新的一個新建立的空陣列中,然後把要寫入的資料寫入到老或新的這兩個數組裡面的一個裡面去。

【這裡要留個問題給大家了,我想問一下,為什麼b-tree要使用陣列來儲存資料呢?為什麼不選擇連結串列等結構呢?】

對於上面的這個小的b-tree sample裡面呢,因為陣列[35,60],陣列已經滿了,所以要進行分裂。於是陣列在插入了新值以後,變成了兩個[35,36] [60] ,然後再改變父節點的指標並依次傳導上去即可。

當出現刪除的時候,會可能需要進行合併的工作,也就是寫入這個操作的反向過程。在一些場景中,因為不斷地插入新的id,刪除老的id,會造成b-tree的右傾,這時候需要有後臺程序對這種傾向進行不斷地調整。

基本上,這就是b-tree的運轉過程了。

B+tree


B+tree 其實就是在原有b-tree的基礎上。增加兩條新的規則

1.      Branch節點不能直接查到資料後返回,所有資料必須讀穿或寫穿到leaf節點後才能返回成功

2.      子葉節點的最後一個元素是到下一個leaf節點的指標。

這樣做的原因是,更方便做範圍查詢,在b+樹種,如果要查詢20~56.只需要找到20這個起始節點,然後順序遍歷,不再需要不斷重複的訪問branchroot節點了。

在瞭解了b樹的基本原理了以後,讓我們來做一個小結。

在面向磁碟/ssd的資料結構中,因為從這類介質中進行查詢和寫入的代價遠遠的大於記憶體,並且一次必須讀取一個或幾個相鄰塊內的資料效率才會高,而隨機尋道次數則直接決定了磁碟的瓶頸。因此,面向磁碟類的結構一個最重要的理念,就是儘可能的減少磁碟尋道次數。而實現這個理念所依託的核心思路,就是讓每一個取出的塊都能擁有更大的價值。舉個例子,如果磁碟進行了一次隨機尋道,拿到了一批資料,而這個批內可以進行4次二分查詢。那麼如果要從2^8個數據內定位到我們所需要的資料,則只需要進行兩次隨機尋道,取出兩批資料,就可以定位到資料了。

這是針對這類整塊取出或寫入資料的硬體的一種通用的解決方法,後面我們介紹的其他面向磁碟的資料結構,也都擁有類似的結構,而不同點則主要是針對一些具體的硬體技術特性而做出的針對性的優化。這個到我們介紹LSM/SSTable的時候大家就會看到。

B樹是上世紀80年代的產物,設計上是比較簡單的,因為b樹採取的是原位更新的方式,所以對讀取是比較優化的,而代價是在寫入的時候也需要先通過隨機查詢來找到資料要寫入的目標位置,這個操作會導致磁碟的隨機寫。因此對寫入而言,如果你使用的是磁碟,那麼很可能在不斷地寫入刪除寫入刪除多次後,b樹會出現更多的隨機尋道,從而導致寫的效能下降。(B樹很適合讀,一般最多需要兩次磁碟隨機尋道就可以讀取資料,但是在長時間執行,經過多次插入,刪除之後,就像記憶體一樣,在磁碟上也出現了很多碎片,如果是寫入到原陣列,就直接寫,但是,如果原陣列已經寫滿了,就要對原陣列進行分裂,這時就要在磁碟上找到一塊空閒塊,這就會出現較多的隨機尋道)

B樹另外的一個挑戰則是如何保持b樹元素的均衡,如果各位實際的觀察過資料庫,都會發現隨著使用者的使用,資料庫可能都會出現一定程度的向右的傾斜,這種現象產生的主要原因與我們使用b樹的方式有一定關係,因為我們往往會給資料增加一個唯一標識,寫入的時候則主要會以單調遞增的方式建立這個id。那麼最後我們寫入資料庫的一個數據的序列就可能是:插入1,2,3,刪除3,插入4,5,7 ,刪除4,5 插入8,9,10。。。一直這樣寫入下去,就會出現前面的節點資料相對的比較鬆散,而後面的資料則相對的比較緻密,雖然前面的資料比較鬆散,但鬆散卻並不意味著空間節省,因為b樹必須保證全域性有序,因此空間只能被空在那裡,造成了較多的空間浪費。這個問題主要的解決方法就是做一個後臺執行緒,不斷的將前面的資料做一個數據整理,並寫到新的塊內。以騰出更多的空間。

下面照例,我們使用一些通用的標準對b樹進行一下簡單的評價:

1.是否支援範圍查詢

因為是有序結構,所以能夠很好的支援範圍查詢。

2.集合是否能夠隨著資料的增長而自動擴充套件

可以,主要增長方式如下: 如果單個數組內還有空隙,那麼資料可以直接放入陣列內,而如果陣列沒有空隙,則進行分裂,從而可以支援資料的自動擴充套件。

3.讀寫效能如何

因為從巨集觀上可以做到一次排除一半的資料,並且在寫入時也沒有進行其他額外的資料查詢性工作,所以對於b樹來說,其讀寫的時間複雜度都是O(log2n)。

4.是否面向磁碟結構

一般來說,在有記憶體的情況下,root層和branch裡面的一部分都會被快取在記憶體中,所以如果樹的高度是三層,那麼前兩層一般都會被快取在記憶體中,所以查詢基本上只需要一次隨機尋道時間, 這比二叉樹系列和skiplist系列都要強不少。

5.並行指標

b樹也是一個並行度比較不錯的資料結構,相比較skiplist而言,他很難使用compare and set的方式來進行資料的寫入,而必須使用lock來保證讀寫訪問的同步。不過因為可以儘可能的將鎖下推,所以鎖的顆粒度可以維持在比較小的級別,從而可以提供比較高的並行度。同時,因為b樹主要使用的目標場景是磁碟,對於磁碟讀寫來說,Compare and set 帶來的效能提升幾乎可以忽略。因此我們可以認為,b樹的並行度比skiplist要差,但比其他樹的要好

6.記憶體佔用

這是b樹的一個短板,在最壞的情況下,b樹的所有塊都剛好做完分裂。那麼整棵樹需要消耗兩倍的空間才能儲存下所有的資料,空間相對的有些浪費。所以一般會通過重新平衡的方式加以部分的糾正