引入
基礎理論的進步,是推動技術實現重大突破,促使相關領域的技術達成跨越式發展的核心。
在發展日新月異的大數據領域,基礎理論的核心無疑是算法。不管是技術設計,還是工程實踐,都必須仰仗相關算法的支持,才能夠真的落地應用。
下面我們就看看大數據相關領域有哪些核心的算法。
存儲類算法
大數據存儲相關的核心算法,主要是為了高效存儲和管理海量數據,以及提升數據讀寫性能和存儲利用率等。
以下我們來看看大數據領域最經典的存儲類算法:
B樹及其變種(B+樹、B* 樹)
原理:
- B樹:是一種自平衡的多路搜索樹,每個節點可以有多個子節點。它的所有葉子節點都在同一層,并且包含了所有的數據。
- B+樹:是 B樹的一種變種,它的非葉子節點只存儲索引信息,所有的數據都存儲在葉子節點中,葉子節點之間通過指針相連,形成一個有序鏈表,便于范圍查詢。(最通用)
- B*樹:在 B+樹的基礎上,對節點的分裂規則進行了優化,提高了空間利用率。
應用場景:廣泛應用于關系型數據庫的索引結構,能夠高效地支持點查詢和范圍查詢。
優點:查詢、插入和刪除操作的時間復雜度都是 O (log n),性能穩定。
缺點:對于大規模數據的寫入操作,可能會導致頻繁的節點分裂和合并,影響性能。
B+樹是一種平衡的、多叉的樹形結構,能夠支持O(logn)的插入和查詢時間復雜度。B+樹的整個結構是有序存儲的,這使得B+樹能夠高效地支持范圍查詢;在空間放大維度,B+樹能夠達到70%的空間利用率。綜上所述,B+樹有較好的綜合性能,在現代的諸多存儲系統中,B+樹索引很常見,例如關系數據庫MySQL的默認存儲引擎InnoDB。
在大數據領域是避免不了使用多線程與高并發場景的,所以需要對B+樹索引進行并發控制。由于B+樹的樹形結構會不斷動態調整,要實現一個正確的多線程B+樹,存在著較大的設計挑戰。
目前來說,實現B+樹的并發,可以采用以下3種機制:
- 鎖耦合
鎖耦合機制是B+樹中應用最為廣泛的一種加鎖方式。鎖耦合機制就是一種節點級別的加鎖方式,但是路徑上的節點的鎖會更早地釋放,同時能保證線程安全。在鎖耦合機制中,每個線程同時最多擁有兩個節點的鎖,分別為父節點和孩子節點。父節點的節點可以在孩子節點的鎖獲取之后釋放,這樣可以充分減少每個節點加鎖和釋放的臨界區大小,從而最大化多線程性能。- 樂觀鎖機制
采用鎖耦合機制,每個讀/寫線程仍然是互相阻塞的,而樂觀鎖機制則是為了減少寫線程對讀線程的阻塞,并進一步減少加鎖的數量。內部節點除節點內部的鎖字段之外,還額外維護一個寫版本號。每當寫線程對節點完成修改之后,先對寫版本號完成自增操作,隨后釋放寫鎖。每當讀線程訪問一個節點的時候,首先記錄節點版本號,在完成對節點的訪問之后檢測節點版本號是否發生變化,如果節點寫版本號發生變化,讀線程重做對該節點的訪問,否則意味著節點訪問過程中該節點并未發生寫操作,因此讀節點操作成功執行。- 無鎖機制。
通過無鎖的方式來操作B+樹,提升隨機讀和范圍查詢的性能。它的核心的思想是把B+樹的頁(page)通過page id(PID)映射到map,map的[key,value]變成[PID,page value]?,把直接對page的修改,變成一個修改的操作記錄,加入到“page value”?。所以“page value”可能是一個“base page”?,即page原始的內容,和一串對page修改形成的記錄的鏈表,而在修改記錄鏈表中加入一個修改記錄節點可以很容易變成一個無鎖的方式來實現。另外,對B+樹的split和merge操作也通過類似的原理,把具體的操作細化成好幾個原子操作,避免傳統的加鎖方式。
SkipList(跳表)
原理
- SkipList 是一種可以用來快速查找數據的數據結構,它基于有序鏈表,并通過在鏈表節點上增加多層索引來提高查找效率。在 SkipList 中,每個節點都可能有多個指針,這些指針指向不同層次的下一個節點,高層的指針可以跳過更多的節點,從而加快查找速度。
- 構建 SkipList 時,會按照一定的概率隨機決定每個節點在不同層次出現的概率。例如,一個節點可能以 50% 的概率出現在第一層,以 25% 的概率出現在第二層,以 12.5% 的概率出現在第三層,以此類推。這樣就形成了一個類似金字塔形狀的多層結構,使得查找操作可以在對數時間內完成。
應用場景:由于 SkipList 在插入、刪除和查找操作上都具有較高的效率,適合在內存中存儲和操作大量的有序數據,能夠快速地根據分數對元素進行排序和查找;在分布式哈希表(DHT)等分布式數據結構中,SkipList 可以用于實現節點之間的快速路由和數據查找。通過在不同節點上構建 SkipList 結構,可以高效地定位數據所在的節點,提高分布式系統的性能和可擴展性。
優點:SkipList 的插入、刪除和查找操作的平均時間復雜度為 O (log n),與平衡樹(如紅黑樹)等數據結構相當,但 SkipList 的實現相對簡單,代碼復雜度較低,易于理解和維護。而且 SkipList 支持動態擴展和收縮,能夠方便地適應數據量的變化。
缺點:SkipList 的空間復雜度相對較高,因為每個節點可能包含多個指針,需要額外的空間來存儲這些指針。此外,由于 SkipList 的節點層數是隨機生成的,在極端情況下可能會出現查找性能下降的情況,但這種情況發生的概率較低。
跳躍表(SkipList)是一種能高效實現插入、刪除、查找的內存數據結構,這些操作的期望復雜度都是O(logN)。與紅黑樹以及其他的二分查找樹相比,跳躍表的優勢在于實現簡單,而且在并發場景下加鎖粒度更小,從而可以實現更高的并發性。正因為這些優點,跳躍表廣泛使用于KV數據庫中,諸如Redis、LevelDB、HBase都把跳躍表作為一種維護有序數據集合的基礎數據結構。
LSM樹(Log-Structured Merge Tree)
原理:將數據的寫入操作先記錄在內存中(通常是一個有序的數據結構,如跳表),當內存中的數據達到一定閾值后,再批量地將數據寫入磁盤,形成一個有序的數據文件(SSTable,Sorted String Table)。磁盤上的數據會按層級進行組織,不同層級的數據會定期進行合并操作,以減少數據冗余和提高查詢效率。
應用場景:適用于寫多讀少的場景,如日志存儲、時間序列數據存儲等。
優點:寫入性能高,能夠快速處理大量的寫入請求;
缺點:讀取時可能需要合并多個 SSTable,讀取性能相對較低,并且在合并過程中會產生一定的 I/O 開銷。
2000年年初,Google發表了Bigtable的論文,論文中的創新點之一就是它所使用的文件組織方式,即LSM樹。
算法的核心也是基于硬件特性來,才能真正的解決落地的問題。對于磁盤讀寫來說,順序讀寫要遠比隨機讀寫快,LSM樹通過將隨機寫轉化為順序寫,消去隨機的本地更新操作來提高寫入性能,但查詢(包括點查詢和范圍查詢)性能會有一定程度的下降,因為一次查詢操作可能要遍歷磁盤中的許多個不同的SST文件。針對查詢性能問題,在不同應用實現時會有一些優化,比如在HBase中設計了異步的compaction來降低文件個數,來提高讀取性能。
LSM樹本質上和B+樹一樣,是一種磁盤數據的索引結構。但和B+樹不同的是,LSM樹的索引對寫入請求更友好。因為無論是何種寫入請求,LSM樹都會將寫入操作處理為一次順序寫。
LSM樹的索引一般由兩部分組成,一部分是內存部分,一部分是磁盤部分。內存部分一般采用跳躍表來維護一個有序的KeyValue集合。磁盤部分一般由多個內部KeyValue有序的文件組成。
哈希算法(Hash Tables)
原理:通過哈希函數將鍵映射到一個固定大小的數組中,數組中的每個位置稱為一個槽(Slot)。當插入、查找或刪除數據時,先計算鍵的哈希值,然后根據哈希值找到對應的槽。如果發生哈希沖突(即不同的鍵映射到了同一個槽),可以采用開放尋址法、鏈地址法等方法來解決。
應用場景:適用于快速查找和插入的場景,如緩存系統、分布式哈希表(DHT)等。在分布式系統中,一致性哈希算法是一種常用的哈希算法,用于實現數據的均勻分布和節點的動態擴展。
優點:平均查找、插入和刪除操作的時間復雜度為 O (1),性能高效;
缺點:哈希函數的設計比較關鍵,如果哈希函數設計不當,可能會導致哈希沖突頻繁,影響性能。并且哈希表不支持范圍查詢。
哈希表是一種無序的數據結構,它提供了快速的插入操作和查找操作。
一個好的哈希表能夠保證插入和查找的時間復雜度為O(1),即插入和查詢性能與哈希表中的數據量無關。這種設計可以實現高效的寫性能和查詢性能,但是它犧牲了范圍查詢性能。
哈希表結構設計中最關鍵的問題是:
- 如何選擇合適的哈希函數;
- 如何選擇合適的哈希沖突處理機制。
常見的哈希沖突解決機制有四種:
- 鏈地址法。
在鏈地址法下,哈希表的每個桶由一個鏈表構成。鏈表中存儲的是所有哈希值相同的鍵值對。因此在進行查詢操作時,可以通過遍歷該鏈表查詢對應的鍵值對。- 線性探測法。
在線性探測法下,哈希表是一個連續的桶數組,對于任意一個哈希鍵,根據哈希函數定位到一個映射位置,插入和查找都基于該地址進行向后探測。當插入一個鍵值時,判斷映射地址是否為空,如果該地址為空,則在映射地址插入鍵值對,否則向后探測直到找到空桶,并將該鍵值對放入該空桶。查詢操作則從映射地址開始向后掃描所有鍵值對,直到找到待查詢鍵值對或者遇到一個空桶。- 雙選擇法。
雙選擇法采用兩個獨立的哈希函數,對于每個鍵值對,都有兩個可插入的桶。當執行插入的時候,根據兩個哈希函數分別將哈希鍵映射到兩個桶a和b中。根據桶a和桶b的填充度,選擇填充度更低的桶插入鍵值對。同樣,執行查詢操作時,只需要遍歷兩個桶即可定位到查詢鍵值。- 布谷鳥探測法。
布谷鳥探測法是雙選擇法的一種變種。它同樣采用兩個哈希函數。當執行鍵值對插入時,根據兩個哈希函數分別將哈希鍵映射到兩個桶a和b中。如果桶a和b存在空閑位置,則將鍵值對插入到空閑位置中;否則,隨機挑選一個桶中的鍵值對,將其踢出該桶,并存入待插入鍵值對,被踢出的鍵值對則嘗試插入到其對應的另一個桶中。采用不同哈希沖突解決方式,在查詢性能、插入性能、哈希表填充度三個維度會有不同的表現,解決哈希沖突的方案也是沒有“銀彈”。
鏈地址法的插入性能更優,并且對于空間的占用是逐漸增長的;線性探測法的填充度可以做到最優,但是這是以犧牲查詢和插入性能為前提的;在查詢性能上,布谷鳥和雙選擇法會比其他方法更優。在實際的鍵值數據庫中,不同的設計會采用不同的哈希函數和哈希沖突解決機制。Redis采用的就是鏈地址法,這使得Redis的空間占用更為緩慢,空間管理也更為靈活。
LRU(Least Recently Used)和 LFU(Least Frequently Used)緩存算法
原理:
- LRU:基于 “最近最少使用” 的原則,當緩存空間滿時,優先淘汰最近最少使用的數據。通常使用雙向鏈表和哈希表來實現,雙向鏈表用于維護數據的訪問順序,哈希表用于快速查找數據。
- LFU:基于 “最不經常使用” 的原則,當緩存空間滿時,優先淘汰使用頻率最低的數據。可以使用多個鏈表和哈希表來實現,每個鏈表存儲相同使用頻率的數據。
應用場景:常用于緩存系統中,如數據庫緩存、Web 服務器緩存等,以提高數據的訪問速度。
優點:
- LRU:實現簡單,能夠較好地反映數據的訪問局部性。
- LFU:能夠更好地適應數據的使用頻率。
缺點:
- LRU:對于某些特殊的訪問模式,可能會導致性能下降。
- LFU:實現相對復雜,并且在數據訪問模式發生變化時,需要一定的時間來調整。
總結
今天提到的都是存儲相關最核心的算法,本文主要是拋磚引玉,后續在分享大數據相關組件底層實現原理時,有涉及到相關算法的時候,我們再深入看看。