分佈式存儲系統設計(3)—— 存儲結構

在NoSQL存儲系統中,通常都採用Key-Value的數據類型,Key-Value結構簡單,易於存儲,很是適合分佈式NoSQL存儲系統。但簡單的數據類型對業務存儲的數據就有必定的侷限性,好比須要存儲列表類型的數據。針對這個問題,系統對Key-Value類型的數據作了一些擴展,支持在一個Key下存儲多個字段和列表,擴大了數據存儲的業務場景。本文主要介紹這個分佈式存儲系統所支持的數據類型,以及數據在內存中的存儲實現。數組

數據類型

  1. Key-Value

Key-Value是最簡單的數據類型,Key和Value都不支持結構化數據,業務進行讀寫的時候須要進行序列化和反序列化,系統並不理解數據結構,都看成二進制序列處理,具備通用性,這樣Key和Value就支持任意類型的數據,只要序列化後的數據長度不超過限制。數據結構

  1. Key-Fields

Key-Fields支持多個字段,使用整型的Tag進行字段標識,每一個Tag對應的Field長度能夠不同,Field也是序列化的數據,比較通用。對於須要在一個Key的數據中存儲多個字段的場景,Key-Fields相比Key-Value友好了不少。多線程

  1. Key-Rows

Key-Rows支持多個行,每一行就是一個Key-Fields,支持多個字段,各行的Field數量、長度均可以不同,很是靈活。Key-Rows能夠用來存儲列表,支持更復雜的業務場景,如存放一個用戶的購物訂單信息。併發

多階哈希表

因爲數據是全內存存儲,爲了保證進程重啓時不丟失數據,使用共享內存的方式,同時也方便了多個進程訪問數據。對於Key-Value類型的數據,最高效數據結構天然是哈希表。這裏介紹一種多階哈希表的實現,具備簡單、高效、魯棒等特色,很是適合工程應用。分佈式

多階哈希表採用相似再哈希的思想來解決衝突。以下圖所示,把一個線性數組劃分爲N階,每一階的桶大小(元素個數)爲Ni(1iN),插入數據時,經過Hash(Key)%Ni計算在每一階的位置,從第1階開始,若是出現衝突,則繼續在下一階查找,只到找到空位爲止,若是在N階都發生了衝突,則插入失敗,查找的過程須要相似。須要注意的是,在插入一個新的數據時,須要先在全部階都查找一遍,對性能會有必定的影響,例如數據1第一次插入在第3階,隨後在其查找路徑上第2階的數據2被刪除,在數據1再次插入時,會在第2階找到空位,若是插入此位置,第3階的數據2會殘留而沒法清理,形成空間的浪費。而對於NoSQL數據存儲系統來講,新增數據佔比比較少,大部分操做是數據更新和讀取,因此多階哈希表在這種場景下能夠保持比較好的性能。性能

衡量多階哈希表的好壞主要有2個指標,空間利用率和平均查找次數。經過實驗可知,階數越多,處理衝突的能力越強,空間的利用率也高,但平均查找次數越多,可見空間利用率和平均查找次數相互制約,須要根據實際狀況進行權衡。測試

對於每一階的桶大小,須要選擇不一樣的質數,能夠在數學上證實各階的桶大小互質時,數據在各階的分佈會更加均勻,從而下降了衝突的機率。最簡單的方法是各階選擇連續的質數,那有沒有更好的選擇,能夠在相同階數下,獲得更高的空間利用率和更少的平均查找次數。在每階桶大小相近的狀況下,低階會分佈更多的數據,因此從低階到高階桶大小遞減的狀況下,會提高空間利用率;在階數不變的條件下,爲了下降平均查找次數,應該讓低階桶大小越大越好,同時爲了保持高階對衝突的處理能力,桶大小不能過小。綜上所述,桶大小從低階到高階依次遞減,在低階遞減比較劇烈,在高階遞減比較平緩,而等比數列比較符合這個特徵。桶大小採用係數爲1.5的等比數列與桶大小平均分佈的對比測試結果以下,使用隨機數進行插入,第一次出現插入失敗時的空間利用率做爲最終的結果,能夠看出在相同階數下,桶大小爲等比數列時會有更高的空間利用率和更少的平均查找次數,階數爲20階是比較好的選擇,此時多階哈希表的空間利用率達到93%,平均查找次數約爲3次。spa

在前面所列舉的幾種數據類型,Key和數據都是變長的,而哈希表的節點是定長的,若是把數據直接存在哈希表節點,是很是浪費空間的。爲了提升內存的利用率,這裏採用一種哈希表和鏈表相結合的數據接口,以下圖所示,哈希表節點存的是數據索引,Key和數據都存放在一個定長塊鏈表中,這樣就能夠根據數據大小來分配定長塊的數量,即高效又不浪費空間。定長塊的大小也須要仔細權衡,太大了會致使無效空洞比較大,過小了又會使得每一個數據佔用的定長塊數量比較多,對性能有必定的影響。線程

讀寫衝突

對於一塊存儲空間,單進程進行讀寫是最簡單的設計,沒有讀寫衝突的問題,但單進程在處理能力上就會成爲瓶頸。爲了提高處理能力,有必要使用多進程或多線程併發的方式,這樣就引入了讀寫衝突的問題。設計

若是多個寫進程併發操做一塊存儲空間,須要使用互斥鎖機制,因爲存儲結構使用了鏈表,須要大粒度的鎖才能保證不出現錯鏈,而且鎖機制實現起來也比較複雜。在這一系列的上一篇文章中提升一個更好的解決方法,把單機的存儲空間劃分爲多個存儲單元,每一個存儲單元由一個寫進程處理,這樣在併發的同時也保證了互斥,這種無鎖的實現性能也更好。

解決了寫進程互斥的問題,讀寫進程之間仍是會存在衝突,若是某個數據在進行寫操做的過程當中,正好讀進程在讀取這個數據,就有可能讀到的是不完整的數據。這個問題能夠經過校驗來解決,在數據中記錄一個校驗值,每次寫入就更新校驗值,讀取數據的時候,對數據進行校驗,若是校驗失敗說明出現了讀寫衝突,則須要進行重試。爲了下降讀寫衝突的機率,在更新數據的時候,按照下圖所示的流程,並不在原來的定長塊鏈上進行覆寫進行處理,而是分配新的定長塊鏈寫入數據。

大多數使用分佈式存儲的業務,都是寫少讀多的場景,在上面的讀寫衝突處理機制下,一個存儲單元是能夠同時支持一個寫進程和多個讀進程,使得讀操做有更高的併發能力,很是適合寫少讀多的狀況。

相關文章
相關標籤/搜索