本文將引導您了解日誌結構合併樹(LSM-Tree),包括其核心概念和結構。到最後,您將能夠從頭開始建立自己的基於 LSM-Tree 的儲存引擎。
日誌結構合併樹(LSM-Tree)是一種針對高吞吐量寫入操作進行最佳化的資料結構。廣泛應用於資料庫和儲存系統,例如Cassandra、RocksDB、LevelDB。
LSM-Tree 的關鍵思想是首先將操作寫入記憶體資料結構(通常是跳躍列表或 AVL 樹等有序結構)。隨後,這些寫入會被批次並作為 SSTable 順序寫入磁碟,從而最大限度地減少隨機 I/O。
LSM-Tree 分為兩個主要組件:
通常,SSTable 的結構不僅包括一系列有序的鍵值對(資料塊)。它也包含索引區塊、元資料區塊和其他元件。這些細節將在實施部分深入討論。
寫入資料涉及新增新的鍵值對或更新現有的鍵值對。更新會覆蓋舊的鍵值對,這些鍵值對稍後會在壓縮過程中被刪除。
資料寫入時,首先進入Memtable,其中鍵值對被加入到記憶體中的有序資料結構中。同時,寫入操作會記錄在 WAL 中並持久保存到磁碟,以防止資料庫崩潰時資料遺失。
Memtable 有一個定義的閾值(通常基於大小)。當Memtable超過此閾值時,它會切換到唯讀模式並轉換為新的SSTable,然後在磁碟上持久化到Level 0。
一旦 Memtable 被刷新為 SSTable,對應的 WAL 檔案就可以安全刪除。後續的寫入操作將在新的 Memtable(和新的 WAL)上進行。
在LSM-Tree中,資料不會立即被刪除。相反,刪除是使用一種名為 邏輯刪除 的機制來處理的(類似於軟刪除)。當刪除某個鍵值對時,會寫入一個標示「墓碑」的新條目,表示對應的鍵值對被刪除。實際的移除發生在壓縮過程中。
這種基於邏輯刪除的刪除確保了 LSM-Tree 的 僅追加屬性,避免隨機 I/O 並保持對磁碟的順序寫入。
查詢資料的過程從在Memtable中搜尋開始。如果找到鍵值對,則將其傳回給客戶端。如果找到墓碑標記的鍵值對,表示要求的資料已被刪除,也會傳回此資訊。如果在 Memtable 中找不到該鍵,則查詢將繼續從 Level 0 到 Level N 搜尋 SSTable。
由於查詢資料可能涉及搜尋多個SSTable 檔案並可能導致隨機磁碟I/O,因此LSM-Tree 通常更適合寫入密集型工作負載,而不是讀取密集型工作負載。
查詢效能的常見最佳化是使用布隆過濾器。布隆過濾器可以快速判斷特定SSTable中是否存在某個鍵值對,減少不必要的磁碟I/O。此外,SSTables 的排序特性使得高效的搜尋演算法(例如二分搜尋)能夠用於更快的查找。
這裡,我們介紹一下Leveled Compaction策略,LevelDB和RocksDB都使用該策略
另一個常見策略是大小分層壓縮策略,其中較新且較小的 SSTable 會依序合併到較舊且較大的 SSTable 中。
如前所述,SSTable 儲存一系列按鍵排序的項目。在分級壓縮策略中,SSTables被組織成多個層級(等級0到等級N)。
在 Level 0 中,SSTable 可以具有重疊的鍵範圍,因為它們是直接從 Memtable 中刷新的。然而,在等級 1 到 N 中,同一層級內的 SSTable 不具有重疊的鍵範圍,儘管不同層級的 SSTable 之間允許鍵範圍重疊。
下面顯示了一個說明性(儘管不完全準確)的範例。在Level 0 中,第一個和第二個SSTable 的鍵範圍重疊,而在Level 1 和Level 2 中,每個等級內的SSTable 具有不相交的鍵範圍。然而,不同層級(例如,層級 0 和層級 1,或層級 1 和層級 2)之間的 SSTable 可能具有重疊的按鍵範圍。
現在讓我們探討一下 Leveled Compaction 策略是如何維持這種組織架構的。
由於Level 0是一個特例,所以壓縮策略討論分為兩部分:
Level 0 to Level 1 壓縮和 Level N to Level N 1 (N > 0) 壓縮的主要差異在於較低等級(Level 0 或 N 級)。
多SSTable的壓縮和合併流程如下圖所示。合併期間,僅保留每個鍵的最新值。如果最新值具有「墓碑」標記,則該鍵將被刪除。在實作中,我們使用k路合併演算法來執行此過程。
要注意的是,上面對壓縮過程的描述僅提供了進階概述。實際實施過程中還有很多細節需要解決。
例如,在LevelDB中,在壓縮過程中為Level N 1建立新的SSTable時,如果新的SSTable與Level N 2中的10個以上SSTable重疊,則流程切換到建置另一個SSTable。這限制了單次壓縮所涉及的資料大小。
透過上面對LSM-Tree的概述,相信您現在已經對LSM-Tree有了基本的了解,並對其實現有了一些想法。接下來我們將從頭開始建立一個基於LSM-Tree的儲存引擎。下面,我們只介紹核心程式碼;完整程式碼請參考https://github.com/B1NARY-GR0UP/originium。
我們將LSM-Tree的實作分解為以下核心元件並一一實現:
在介紹資料寫入的過程中,我們提到LSM-Tree首先將資料寫入記憶體中的有序資料結構。一些常見的有序資料結構及其操作的時間複雜度如下:
Data Structure | Insert | Delete | Search | Traverse |
---|---|---|---|---|
Skip List | O(logn) | O(logn) | O(logn) | O(n) |
AVL Tree | O(logn) | O(logn) | O(logn) | O(n) |
Red-Black Tree | O(logn) | O(logn) | O(logn) | O(n) |
我們選擇Skip List主要有兩個原因:它更容易實現和維護(KISS原則),底層鍊錶結構有利於順序遍歷,更容易將記憶體中的資料持久化到磁碟。
跳過清單的完整實作可以在 https://github.com/B1NARY-GR0UP/originium/blob/main/pkg/skiplist/skiplist.go 取得。
跳過列表由一個基本鍊錶和建立在其之上的多層索引組成。對於大型資料集,索引層顯著縮短了搜尋路徑。
在我們的實作中,Skip List的核心結構定義如下:
type SkipList struct { maxLevel int p float64 level int rand *rand.Rand size int head *Element }
Skip List中儲存的元素結構定義如下:
type Element struct { types.Entry next []*Element } // https://github.com/B1NARY-GR0UP/originium/blob/main/pkg/types/entry.go type Entry struct { Key string Value []byte Tombstone bool }
types.Entry 表示儲存引擎中的鍵值對,包括鍵、值和用於刪除的墓碑標記。
next:包含指向每個層級的下一個元素的指標。
這個結構看起來可能很抽象,所以我們用一個例子來說明它:
Level 3: 3 ----------- 9 ----------- 21 --------- 26 Level 2: 3 ----- 6 ---- 9 ------ 19 -- 21 ---- 25 -- 26 Level 1: 3 -- 6 -- 7 -- 9 -- 12 -- 19 -- 21 -- 25 -- 26 next of head [ ->3, ->3, ->3 ] next of Element 3 [ ->6, ->6, ->9 ] next of Element 6 [ ->7, ->9 ]
在這個三級Skip List中,頭節點的next指標引用每一級的第一個節點。元素 3 和 6 儲存每個層級的下一個元素。
例如,如果我們想在第 2 層尋找元素 19 的下一個節點,我們使用 e19.next[2-1]。
func (s *SkipList) Set(entry types.Entry)
LSM-Tree 使用邏輯刪除來執行刪除,因此我們在跳躍清單實作中不需要刪除方法。要刪除元素,只需將條目的墓碑設為 true 即可。因此,Set 方法處理插入新的鍵值對、更新現有鍵值對以及刪除元素。
讓我們來探索Set方法的實作。透過從最高處開始遍歷每一層的節點,將最後一個比要設定的key小的元素保存在更新切片中。
type SkipList struct { maxLevel int p float64 level int rand *rand.Rand size int head *Element }
這次遍歷結束時,curr指向底層鍊錶中最後一個比要設定的key小的元素。因此,我們只需要檢查 curr 的下一個元素是否等於我們想要設定的鍵。如果匹配,則該元素已插入;我們更新現有元素並返回。
type Element struct { types.Entry next []*Element } // https://github.com/B1NARY-GR0UP/originium/blob/main/pkg/types/entry.go type Entry struct { Key string Value []byte Tombstone bool }
如果找不到該元素,則將其插入為新元素。使用 randomLevel,我們計算該元素的索引等級。如果它超出了跳躍列表中當前的級別數,我們將頭節點添加到更新切片中,並將 s.level 更新為新的級別數。
Level 3: 3 ----------- 9 ----------- 21 --------- 26 Level 2: 3 ----- 6 ---- 9 ------ 19 -- 21 ---- 25 -- 26 Level 1: 3 -- 6 -- 7 -- 9 -- 12 -- 19 -- 21 -- 25 -- 26 next of head [ ->3, ->3, ->3 ] next of Element 3 [ ->6, ->6, ->9 ] next of Element 6 [ ->7, ->9 ]
接下來構造要插入的元素,更新每一層的next指針,完成插入。
func (s *SkipList) Set(entry types.Entry)
跳過清單可以依靠多層索引來執行快速搜尋操作。實作中的巢狀 for 迴圈代表基於索引的搜尋操作。如果最終在底層鍊錶中找到對應的元素,則傳回該元素。
curr := s.head update := make([]*Element, s.maxLevel) for i := s.maxLevel - 1; i >= 0; i-- { for curr.next[i] != nil && curr.next[i].Key < entry.Key { curr = curr.next[i] } update[i] = curr }
我們選擇跳過清單的一個原因是它方便的順序遍歷,只需遍歷底層鍊錶即可實現。
// update entry if curr.next[0] != nil && curr.next[0].Key == entry.Key { s.size += len(entry.Value) - len(curr.next[0].Value) // update value and tombstone curr.next[0].Value = entry.Value curr.next[0].Tombstone = entry.Tombstone return }
WAL 的完整實作可以在 https://github.com/B1NARY-GR0UP/originium/blob/main/wal/wal.go 找到。
前面提到,WAL(Write-Ahead Logging)的目的是為了防止資料庫崩潰導致Memtable中的資料遺失。因此,WAL需要記錄對Memtable的操作,並在資料庫重新啟動時從WAL檔案中復原Memtable。
WAL的核心結構如下,其中fd儲存WAL檔案的檔案描述符:
// add entry level := s.randomLevel() if level > s.level { for i := s.level; i < level; i++ { update[i] = s.head } s.level = level }
由於我們需要記錄對 Memtable 的操作,這本質上涉及將每個操作(Set、Delete)作為一個 Entry 寫入 WAL。 Write方法的定義如下:
e := &Element{ Entry: types.Entry{ Key: entry.Key, Value: entry.Value, Tombstone: entry.Tombstone, }, next: make([]*Element, level), } for i := range level { e.next[i] = update[i].next[i] update[i].next[i] = e } s.size += len(entry.Key) + len(entry.Value) + int(unsafe.Sizeof(entry.Tombstone)) + len(e.next)*int(unsafe.Sizeof((*Element)(nil)))
將這些條目寫入檔案時,我們需要標準化 WAL 檔案格式。我們這裡使用的格式是長度資料。首先我們將Entry序列化,然後計算序列化資料的長度,最後將長度和序列化資料依序寫入WAL檔案。
核心程式碼如下:
func (s *SkipList) Get(key types.Key) (types.Entry, bool) { curr := s.head for i := s.maxLevel - 1; i >= 0; i-- { for curr.next[i] != nil && curr.next[i].Key < key { curr = curr.next[i] } } curr = curr.next[0] if curr != nil && curr.Key == key { return types.Entry{ Key: curr.Key, Value: curr.Value, Tombstone: curr.Tombstone, }, true } return types.Entry{}, false }
由於我們使用的是WAL檔案格式長度資料,所以在讀取的時候,我們先讀取8個位元組(int64)來取得資料的長度,然後根據這個長度讀取資料並反序列化檢索條目。
核心程式碼如下:
type SkipList struct { maxLevel int p float64 level int rand *rand.Rand size int head *Element }
Memtable 的完整實作可以在 https://github.com/B1NARY-GR0UP/originium/blob/main/memtable.go 找到。
Memtable負責將客戶端操作寫入skip list並記錄在WAL中。它還可以在資料庫啟動時從 WAL 中復原資料。
Memtable的核心結構如下,包括兩個主要組件skiplist和wal:
type Element struct { types.Entry next []*Element } // https://github.com/B1NARY-GR0UP/originium/blob/main/pkg/types/entry.go type Entry struct { Key string Value []byte Tombstone bool }
執行Set操作時,skip list和WAL都需要同時更新。
Level 3: 3 ----------- 9 ----------- 21 --------- 26 Level 2: 3 ----- 6 ---- 9 ------ 19 -- 21 ---- 25 -- 26 Level 1: 3 -- 6 -- 7 -- 9 -- 12 -- 19 -- 21 -- 25 -- 26 next of head [ ->3, ->3, ->3 ] next of Element 3 [ ->6, ->6, ->9 ] next of Element 6 [ ->7, ->9 ]
要檢索值,只需傳回跳過清單的 Get 操作的結果即可。
func (s *SkipList) Set(entry types.Entry)
從 WAL 檔案復原 Memtable 需要先讀取 WAL 文件,然後依序將 WAL 檔案中的 Entry 記錄套用到 Memtable,最後刪除復原的 WAL 檔案。
檢索 WAL 檔案清單:
curr := s.head update := make([]*Element, s.maxLevel) for i := s.maxLevel - 1; i >= 0; i-- { for curr.next[i] != nil && curr.next[i].Key < entry.Key { curr = curr.next[i] } update[i] = curr }
讀取 WAL 並還原 Memtable:
// update entry if curr.next[0] != nil && curr.next[0].Key == entry.Key { s.size += len(entry.Value) - len(curr.next[0].Value) // update value and tombstone curr.next[0].Value = entry.Value curr.next[0].Tombstone = entry.Tombstone return }
在前面的介紹中,我們只提到「SSTable(Sorted String Table)是一種維護一系列有序鍵值對的資料儲存格式」。在這裡,我們將對SSTable的結構進行更詳細的解釋。
在LevelDB中,SSTable由多個具有不同用途的區塊組成。示意圖如下:
索引資訊本質上是一個名為BlockHandle的指標結構,它包含兩個屬性:offset和size,用於定位對應的Block。
在我們的 SSTable 實作中,我們簡化了 LevelDB SSTable 結構。示意圖如下:
SSTable 的完整實作可以在 https://github.com/B1NARY-GR0UP/originium/tree/main/sstable 找到。
資料塊的結構定義如下,儲存有序的條目序列。
type SkipList struct { maxLevel int p float64 level int rand *rand.Rand size int head *Element }
我們為資料塊實作了三種主要方法:
type Element struct { types.Entry next []*Element } // https://github.com/B1NARY-GR0UP/originium/blob/main/pkg/types/entry.go type Entry struct { Key string Value []byte Tombstone bool }
我們使用前綴壓縮對鍵值序列進行編碼。在緩衝區中,我們依序寫入公共前綴的長度、後綴的長度、後綴本身、值的長度、值和「墓碑」標記。
Level 3: 3 ----------- 9 ----------- 21 --------- 26 Level 2: 3 ----- 6 ---- 9 ------ 19 -- 21 ---- 25 -- 26 Level 1: 3 -- 6 -- 7 -- 9 -- 12 -- 19 -- 21 -- 25 -- 26 next of head [ ->3, ->3, ->3 ] next of Element 3 [ ->6, ->6, ->9 ] next of Element 6 [ ->7, ->9 ]
最後,我們使用s2壓縮資料。
S2 是 Snappy 壓縮演算法的高效能擴充。
func (s *SkipList) Set(entry types.Entry)
curr := s.head update := make([]*Element, s.maxLevel) for i := s.maxLevel - 1; i >= 0; i-- { for curr.next[i] != nil && curr.next[i].Key < entry.Key { curr = curr.next[i] } update[i] = curr }
在解碼過程中,過程只是相反。使用前綴和後綴重構完整的鍵值對。
// update entry if curr.next[0] != nil && curr.next[0].Key == entry.Key { s.size += len(entry.Value) - len(curr.next[0].Value) // update value and tombstone curr.next[0].Value = entry.Value curr.next[0].Tombstone = entry.Tombstone return }
// add entry level := s.randomLevel() if level > s.level { for i := s.level; i < level; i++ { update[i] = s.head } s.level = level }
索引塊的結構定義如下。它儲存每個資料塊的第一個和最後一個鍵,以及對應資料塊的BlockHandle。
e := &Element{ Entry: types.Entry{ Key: entry.Key, Value: entry.Value, Tombstone: entry.Tombstone, }, next: make([]*Element, level), } for i := range level { e.next[i] = update[i].next[i] update[i].next[i] = e } s.size += len(entry.Key) + len(entry.Value) + int(unsafe.Sizeof(entry.Tombstone)) + len(e.next)*int(unsafe.Sizeof((*Element)(nil)))
類似地,索引區塊實作了三個主要方法:編碼、解碼和搜尋。 Encode 和 Decode 方法的實作想法基本上相同,所以我們專注於 Search 方法。
資料區塊的搜尋方法旨在在單一資料區塊中儲存的有序鍵值序列中定位特定的鍵值對。相反,索引區塊的搜尋方法用於在整個 SSTable 中定位包含給定鍵的資料區塊。
func (s *SkipList) Get(key types.Key) (types.Entry, bool) { curr := s.head for i := s.maxLevel - 1; i >= 0; i-- { for curr.next[i] != nil && curr.next[i].Key < key { curr = curr.next[i] } } curr = curr.next[0] if curr != nil && curr.Key == key { return types.Entry{ Key: curr.Key, Value: curr.Value, Tombstone: curr.Tombstone, }, true } return types.Entry{}, false }
func (s *SkipList) All() []types.Entry { var all []types.Entry for curr := s.head.next[0]; curr != nil; curr = curr.next[0] { all = append(all, types.Entry{ Key: curr.Key, Value: curr.Value, Tombstone: curr.Tombstone, }) } return all }
這兩個Block的實作非常簡單,都只需要Encode和Decode方法。
引入SSTable中的所有Block後,建構SSTable只需根據鍵值對逐步建立每個Block。最後返回記憶體索引和編碼後的SSTable。
type SkipList struct { maxLevel int p float64 level int rand *rand.Rand size int head *Element }
K-Way Merge 的完整實作可在 https://github.com/B1NARY-GR0UP/originium/tree/main/pkg/kway 取得。
在概念部分,我們以圖表說明了壓縮和合併多個SSTable的過程。此過程是使用 k 路合併 演算法完成的。
k 路合併演算法是將 k 個排序序列合併為單一排序序列的方法,時間複雜度為 O(knlogk)。
此演算法的一個實作使用 最小堆 作為輔助結構:
標準函式庫在容器/堆中提供了堆實作。透過實作heap.Interface,我們可以建構一個最小堆。
type Element struct { types.Entry next []*Element } // https://github.com/B1NARY-GR0UP/originium/blob/main/pkg/types/entry.go type Entry struct { Key string Value []byte Tombstone bool }
Level 3: 3 ----------- 9 ----------- 21 --------- 26 Level 2: 3 ----- 6 ---- 9 ------ 19 -- 21 ---- 25 -- 26 Level 1: 3 -- 6 -- 7 -- 9 -- 12 -- 19 -- 21 -- 25 -- 26 next of head [ ->3, ->3, ->3 ] next of Element 3 [ ->6, ->6, ->9 ] next of Element 6 [ ->7, ->9 ]
func (s *SkipList) Set(entry types.Entry)
Merge方法的函數定義:
curr := s.head update := make([]*Element, s.maxLevel) for i := s.maxLevel - 1; i >= 0; i-- { for curr.next[i] != nil && curr.next[i].Key < entry.Key { curr = curr.next[i] } update[i] = curr }
遵循k路合併演算法流程。
type SkipList struct { maxLevel int p float64 level int rand *rand.Rand size int head *Element }
type Element struct { types.Entry next []*Element } // https://github.com/B1NARY-GR0UP/originium/blob/main/pkg/types/entry.go type Entry struct { Key string Value []byte Tombstone bool }
最後,遍歷映射以將元素新增至結果佇列中,刪除任何標記為「墓碑」的鍵值對。由於map是無序的,所以結果隊列在回傳之前需要先排序。
Level 3: 3 ----------- 9 ----------- 21 --------- 26 Level 2: 3 ----- 6 ---- 9 ------ 19 -- 21 ---- 25 -- 26 Level 1: 3 -- 6 -- 7 -- 9 -- 12 -- 19 -- 21 -- 25 -- 26 next of head [ ->3, ->3, ->3 ] next of Element 3 [ ->6, ->6, ->9 ] next of Element 6 [ ->7, ->9 ]
布隆過濾器的完整實作可以在 https://github.com/B1NARY-GR0UP/originium/blob/main/pkg/filter/filter.go 找到。
布隆過濾器是一種資料結構,可以有效檢查元素是否是集合的成員。
插入和查詢操作的時間複雜度都是O(k),其中k是雜湊函數的數量。 可能會出現誤報(布隆過濾器預測某個元素在集合中,但事實並非如此),但不會出現誤報(布隆過濾器預測某個元素不在集合中)在集合中,但實際上是)。
真陽性(TP):系統將事件預測為“陽性”,而且它確實是陽性。
誤報(FP):系統將事件預測為“正”,但實際上是負的。
真陰性(TN):系統將事件預測為“陰性”,並且它確實是陰性。
假陰性(FN):系統將事件預測為“陰性”,但實際上是陽性。
布隆過濾器的核心結構包括位數組(可以最佳化為使用 uint8)和多個雜湊函數。
func (s *SkipList) Set(entry types.Entry)
建立 Bloom Filter 實例的方法接受兩個參數:n(期望的元素數量)和 p(期望的誤報率)。
首先,驗證參數。然後,使用特定公式計算位數組的大小(m)和雜湊函數的數量(k)。最後根據m和k初始化位數組和哈希函數。
type SkipList struct { maxLevel int p float64 level int rand *rand.Rand size int head *Element }
當新增元素時,所有雜湊函數都用於計算鍵的雜湊值。然後將這些值對應到位數組中的索引,並將對應位置設為 true。
type Element struct { types.Entry next []*Element } // https://github.com/B1NARY-GR0UP/originium/blob/main/pkg/types/entry.go type Entry struct { Key string Value []byte Tombstone bool }
為了檢查某個鍵是否在集合中,雜湊函數計算雜湊值並將它們對應到位數組中的索引。如果這些位置中的任何一個不為 true,則該元素不在集合中,並傳回 false。
Level 3: 3 ----------- 9 ----------- 21 --------- 26 Level 2: 3 ----- 6 ---- 9 ------ 19 -- 21 ---- 25 -- 26 Level 1: 3 -- 6 -- 7 -- 9 -- 12 -- 19 -- 21 -- 25 -- 26 next of head [ ->3, ->3, ->3 ] next of Element 3 [ ->6, ->6, ->9 ] next of Element 6 [ ->7, ->9 ]
Leveled Compaction 的完整實作可以在 https://github.com/B1NARY-GR0UP/originium/blob/main/level.go 找到。
實作了 K-Way Merge 和 Bloom Filter 等元件後,我們就可以完成實作的最後部分,也就是 LSM-Tree 儲存引擎中最關鍵的 SSTable 壓縮過程。此實作遵循「資料壓縮」部分中所述的分級壓縮策略。
在分級壓縮策略中,SSTables被組織成多個層級(Level 0 - Level N)。我們需要一個結構來儲存這些資訊並管理不同層級的 SSTable。
因此,我們實作了一個名為 levelManager 的結構。我們使用一個[]*list.List來儲存每個層級的SSTable訊息,其中切片的索引對應於該層級。切片中的每個元素都是一個列表。 List,雙向鍊錶,保存特定層級中所有 SSTable 的資訊。
func (s *SkipList) Set(entry types.Entry)
compactLN 方法負責 Level N 到 Level N 1 (N > 0) 的壓縮。它從 LN 中選擇最舊的 SSTable 以及 LN 1 中與此 SSTable 有重疊鍵範圍的所有 SSTable。
curr := s.head update := make([]*Element, s.maxLevel) for i := s.maxLevel - 1; i >= 0; i-- { for curr.next[i] != nil && curr.next[i].Key < entry.Key { curr = curr.next[i] } update[i] = curr }
所選的 SSTable 會依照從最舊到最新的順序處理。來自資料區塊的鍵值對被加入到二維切片中,然後使用 K-Way Merge 演算法進行合併。
// update entry if curr.next[0] != nil && curr.next[0].Key == entry.Key { s.size += len(entry.Value) - len(curr.next[0].Value) // update value and tombstone curr.next[0].Value = entry.Value curr.next[0].Tombstone = entry.Tombstone return }
透過合併的鍵值對,我們建立了一個新的 Bloom Filter 和 SSTable。新SSTable的相關資訊附加在Level N 1的末尾。
// add entry level := s.randomLevel() if level > s.level { for i := s.level; i < level; i++ { update[i] = s.head } s.level = level }
最後,刪除舊的SSTable,並將新建的SSTable寫入磁碟。
e := &Element{ Entry: types.Entry{ Key: entry.Key, Value: entry.Value, Tombstone: entry.Tombstone, }, next: make([]*Element, level), } for i := range level { e.next[i] = update[i].next[i] update[i].next[i] = e } s.size += len(entry.Key) + len(entry.Value) + int(unsafe.Sizeof(entry.Tombstone)) + len(e.next)*int(unsafe.Sizeof((*Element)(nil)))
compactL0 方法處理 0 級到 1 級壓縮。與compactLN不同的是,它不僅從L0中選擇一個SSTable,而且還從L0中選擇所有重疊的SSTable。其餘過程與compactLN 相同。
搜尋方法在所有 SSTable 中找到對應的鍵值對。它從 L0 開始,迭代每個層級直至 LN。透過利用布林過濾器和 SSTable 的有序結構,可以有效地跳過不包含所需鍵值對的 SSTable。
type SkipList struct { maxLevel int p float64 level int rand *rand.Rand size int head *Element }
至此,我們已經實現了基於LSM-Tree的儲存引擎的所有核心元件。透過按照 LSM-Tree 介紹中的描述組裝這些組件,我們可以最終確定資料庫介面。
完整程式碼:https://github.com/B1NARY-GR0UP/originium/blob/main/db.go
文件:https://github.com/B1NARY-GR0UP/originium?tab=readme-ov-file#usage
我們首先了解LSM-Tree,熟悉其核心元件以及處理客戶端請求的流程。最終,我們從頭開始建立了自己的 LSM-Tree 儲存引擎。
當然,這個實作只是一個原型。生產級儲存引擎需要考慮更多細節。 ORIGINIUM未來將持續進行最佳化和改進。希望本文和 ORIGINIUM 能幫助您加深對 LSM-Tree 的理解。
本文涵蓋的所有內容到此結束。如果有任何錯誤或疑問,請隨時透過私訊聯繫或發表評論。謝謝!
以上是從頭開始建造 LSM-Tree 儲存引擎的詳細內容。更多資訊請關注PHP中文網其他相關文章!