Redis的複合數據結構及設計原理:hash/set/zset

Redis的複合數據結構

咱們以前已經講過了Redis的數組列表(List),但其實Redis中最經常使用的數據結構是字典(hash),能夠說,Redis總體的設計都是基於字典的,這不只僅體如今咱們存取數據都是經過鍵值對的方式,還在於其餘的複合數據結構set/zset也都是基於hash來設計的。php

hash 字典

字典在任何語言中都是很是基礎和常見的數據結構,在Java中它是HashMap,在PHP中它是Array,在JS中它是Object,它更常見的是通用的數據傳輸格式JSON
hashTable數據結構 (1).png
字典是一種可變容器型數據結構,能夠存儲任意類型的數據,它經過哈希表來存儲數據和訪問,哈希表是其實現原理。java

hashTable 哈希表

哈希表又稱爲散列表,它根據鍵(key)來直接訪問指定存儲位置的數據,而肯定要訪問的指定存儲位置是經過散列函數對key值進行壓縮生成摘要,生成固定長度的隨機字母和數字的字符串,建立散列值,但在hashTable中,咱們會使用純數字。優秀的散列函數會盡可能均勻的分佈數據,避免散列衝突。python

哈希表的主要目的是加快查找指定key的速度,不管hashTable中有多少元素,它的查找效率均爲O(1),至關於無需遍歷,直接定位到元素。
hashTable數據結構.png算法

如圖所示,哈希表是數組+鏈表的二維數據結構,數組是第一維,鏈表是第二維。數組中的每一個元素稱爲槽或者桶,存儲着鏈表的第一個元素的指針。在上圖中咱們一共有11個槽,如今咱們有a-h共8個key須要存儲,我麼會使用哈希函數對每一個key值生成純數字的摘要,而後將數字對11取模,這樣就可以肯定該key值應該放在哪一個槽中。當多個key值的摘要取模後相等時,就會使用鏈表進行串聯依次存儲key值,這種狀況稱爲散列衝突,也叫碰撞。
hashTable數據結構 (2).pngsegmentfault

這樣,當咱們須要取某個key時,只須要對這個key進行從新hash獲得摘要,而後取模就能知道它在哪一個槽裏了,而後經過鏈表的遍歷依次匹配,就能獲得指定的key,進而取得value。數組

無序的字典

這種哈希表遍歷的時候是無序的,由於常規的遍歷方式是從槽0遍歷到槽10,噹噹前槽存在元素鏈表時,再按照順序依次進行遍歷,這樣咱們的遍歷順序就跟存儲的時候不一樣,所以說是無序的。瀏覽器

對於JS的對象來講,咱們也說遍歷的順序是沒法保證的,但若是元素的集合已是肯定的,那麼遍歷的順序應該是一致的呀。這裏涉及到的問題就是槽的數量可能不一樣,所以順序是徹底不一樣的,在不一樣瀏覽器引擎的設計中,初始化設置的槽數不一樣,擴縮容的時機和空間數也是不一樣的,所以沒法保證。對於Redis也是這樣。緩存

擴縮容

咱們前面說到字典的查找效率是O(1)的,這是創建在字典可以充分hash的前提下,也就是一維數組要足夠用保證二維鏈表不會過長,不然查找效率會下降到O(lgn),甚至在只有一個槽時下降到O(n)數據結構

所以咱們會在散列衝突較多時對字典進行擴容,但擴容是以犧牲空間爲代價提升效率的,Redis做爲內存佔用型的緩存系統,能夠說內存很是寶貴,所以咱們須要在槽空洞過多時進行縮容。函數

擴容條件:hashTable中元素的個數等於一維數組長度時,會對數組長度進行兩倍的擴容。不過若是系統正在作bgsave(後臺刷新內存數據到磁盤中)時,會延遲擴容時機。但當元素達到一維數組長度的5倍時,就會強制執行擴容。

縮容條件:元素個數小於一維數組長度的10%

rehash 從新哈希

hashTable數據結構 (4).png

當擴縮容時,咱們須要申請新的一維數組,並對全部元素進行從新哈希和掛載元素鏈表。因爲Redis是單線程的,所以咱們爲了避免阻塞服務的正常運轉,咱們採用漸進式rehash的策略。

也就是全部的字典結構內部首層是一個數組,數組的兩個元素分別指向一個哈希表,正常狀況下只有一個哈希表,而在遷移過程當中會同時保留新舊兩個哈希表,元素有可能存在於兩個表中的任意一個,所以會同時嘗試從兩個哈希表中查找數據。當數據搬遷完成後,老的哈希表就會被自動刪除。

漸進式hash不是一次性將字典的內容搬完,而是經過每一個執行命令時搬運一部分,同時定時任務搬運一部分,最終用時間來換取執行效率。

哈希函數

對於hashTable來講,哈希函數相當重要,由於好的哈希函數能夠將哈希表的key值打散的比較均勻,這樣高隨機性的元素分佈也可以提高總體的查找效率。Redis使用的哈希函數是siphash

若是哈希函數打散的效果不好,或者有模式能夠遵循,那麼就會存在hash攻擊,攻擊者利用模式的偏向性經過大量產生數據,將這些數據儘量掛載在同一個鏈表上,這種hash不均勻會致使查找的性能急劇降低同時浪費大量的內存空間,進而拖垮Redis總體的性能。

set 集合

set和字典很是相似,其內部實現就是上述的hashTable的特殊實現,與字典不一樣的地方有兩點:

  1. 只關注key值,全部的value都是NULL
  2. 在新增數據時會進行去重。

hashTable數據結構 (5).png

zset 有序集合

zSet是Redis很是有特點的數據結構,它是基於Set並提供排序的有序集合。其中最爲重要的特色就是支持經過score的權重來指定權重。

zadd code 9.0 "java"
zadd code 8.0 "python"
zadd code 8.5 "php"
zrange code 0 -1
1) "python"
2) "php"
3) "java"

hashTable數據結構 (6).png

此時,全部的value都變成了score。而這種支持經過score來進行排序的則是經過另外一個特殊的數據結構:跳躍列表。

skiplist 跳躍列表

hashTable數據結構 (10).png

Redis的zset數據結構是一個複合結構,經過一個相似於set的hashTable來實現value和score的對應關係,也支持set的快速讀寫和去重的功能。同時經過skiplist來支持按照score排序的功能。

hashTable數據結構 (8).png

上圖中每一列表明一個元素,從左到右score值愈來愈大,最左側的kv head表明起始位置,score值爲MIN_VALUE。最底層用雙向鏈表串聯,用於反向遍歷,元素的二層以上會有指向下一個同層高元素的單向指針。

增長元素

hashTable數據結構 (9).png

如上圖所示,當咱們須要根據score值插入紫色kv節點時,咱們首先從kv-head的最高層進行啓動,判斷指針的下個元素的score值是否小於新元素的score值,若是小於,則繼續向前遍歷,不然從kv-head降一層,從新比較判斷。

經過這種對比,咱們能夠僅僅比較6次就找到合適的新元素位置,這在大量數據的時候性能提高效果很是明顯。

從上面的算法能夠看出每一個元素的層高對該算法的執行效率影響很是明顯,若是層高所有一致,效率就會變成O(n),從頭遍歷的感受。雖然爲了不這種狀況,skiplist除了考慮score值外,還考慮對value值進行字符串對比並進行排序。可是層高算法依然很是重要。

每一個元素的層高經過隨機算法分配層高,Redis層高總共64層,每層的晉升率爲25%,所以1層每一個元素是100%,二層是25%,三層就是1/16,四層就是1/64的機率。這樣是爲了減小層高,可以減小向下遍歷的次數,同時可以承載更多的元素也不下降效率。

skipList會記錄最高的層高,並將kv-head的高度置爲這個層高。

查找元素

查找元素的方法和上述新增元素的方法是一致的。

刪除元素

經過上述的查找方式找到元素後,直接刪除元素,並更新先後的指針便可。若是最高層數變化了,須要更新下maxLevel參數。

更新元素

當更新元素時,其實就是改變了score值,這時Redis會直接先刪除,而後插入。這樣在某些場景下效率較低,如score值改變並不影響排序。

元素排名

咱們能夠獲取指定排名段的元素列表,也能夠得到指定元素的排名,這個rank實際上是經過skiplist的span屬性獲得的。

咱們在每一個forward單項指針上都增長了span(跨度)屬性,代表從上一個元素到下一個元素中間通過了多少個元素,所以,當咱們沿着上面的方法查找時,只須要將通過的全部指針的span值進行累加,就知道指定元素的排名是多少了。

參考資料

  1. 《Redis深度歷險 核心原理與應用實踐》
相關文章
相關標籤/搜索