① Redis中hash、set、zset的底層數據結構原理
Redis-哈希對象(hash)
Redis-集合對象(set)
其中hashtable的key為set中元素的值,而value為null
inset為可以理解為數組,使用inset數據結構需要滿足下述兩個條件:
intset的底層結構
查詢方式一般採用二分查找法,實際查詢復雜度也就在log(n)
Redis-有序集合對象(zset)
底層實現為 字典(dict) + 跳錶(skiplist),當數據比較少的時候用ziplist編碼結構存儲。
同時滿足以下兩個條件採用ziplist存儲:
ziplist存儲方式
總結
② Redis底層數據結構
Redis中值的數據結構有String(字元串)、List(列表)、Hash(哈希)、Set(集合)和 Sorted Set(有序集合)五種,使用可參考 https://www.jianshu.com/p/fdd24839f460 。
而底層數據結構一共有 6 種,分別是簡單動態字元串、雙向鏈表、壓縮列表、哈希表、跳錶和整數數組。它們和數據類型的對應關系如下圖所示:
可以看到,String 類型的底層實現只有一種數據結構,也就是簡單動態字元串。而 List、Hash、Set 和 Sorted Set 這四種數據類型,都有兩種底層實現結構。通常情況下,我們會把這四種類型稱為集合類型,它們的特點是一個鍵對應了一個集合的數據。
為了實現從鍵到值的快速訪問,Redis 使用了一個哈希表來保存所有鍵值對。一個哈希表,其實就是一個數組,數組的每個元素稱為一個哈希桶。哈希桶中的元素保存的並不是值本身,而是指向具體值的指針。
這也就是說,不管值是 String,還是集合類型,哈希桶中的元素都是指向它們的指針。在下圖中,可以看到,哈希桶中的 entry 元素中保存了 key和 value指針,分別指向了實際的鍵和值。
哈希沖突,也就是指,兩個 key 的哈希值和哈希桶計算對應關系時,正好落在了同一個哈希桶中。畢竟,哈希桶的個數通常要少於 key 的數量,這也就是說,難免會有一些 key 的哈希值對應到了同一個哈希桶中。Redis 解決哈希沖突的方式,就是 鏈式哈希 。鏈式哈希也很容易理解,就是指同一個哈希桶中的多個元素用一個鏈表來保存,它們之間依次用指針連接。
如下圖所示:entry1、entry2 和 entry3 都需要保存在哈希桶 3 中,導致了哈希沖突。此時,entry1 元素會通過一個 next指針指向 entry2,同樣,entry2 也會通過 next指針指向 entry3。這樣一來,即使哈希桶 3 中的元素有 100 個,我們也可以通過 entry 元素中的指針,把它們連起來。
其實,為了使 rehash 操作更高效,Redis 默認使用了兩個全局哈希表:哈希表 1 和哈希表 2。一開始,當你剛插入數據時,默認使用哈希表 1,此時的哈希表 2 並沒有被分配空間。隨著數據逐步增多,Redis 開始執行 rehash,這個過程分為三步:
這個過程看似簡單,但是第二步涉及大量的數據拷貝,如果一次性把哈希表 1 中的數據都遷移完,會造成 Redis 線程阻塞,無法服務其他請求。此時,Redis 就無法快速訪問數據了。為了避免這個問題,Redis 採用了 漸進式 rehash 。
簡單來說就是在第二步拷貝數據時,Redis 仍然正常處理客戶端請求,每處理一個請求時,從哈希表 1 中的第一個索引位置開始,順帶著將這個索引位置上的所有 entries 拷貝到哈希表 2 中;等處理下一個請求時,再順帶拷貝哈希表 1 中的下一個索引位置的 entries。如下圖所示:
對於 String 類型來說,找到哈希桶就能直接增刪改查了,所以,哈希表的 O(1) 操作復雜度也就是它的復雜度了。
一個集合類型的值,第一步是通過全局哈希表找到對應的哈希桶位置,第二步是在集合中再增刪改查。首先,操作復雜度與集合的底層數據結構有關。例如,使用哈希表實現的集合,要比使用鏈表實現的集合訪問效率更高。其次,操作效率和這些操作本身的執行特點有關,比如讀寫一個元素的操作要比讀寫所有元素的效率高。
String類型對應的簡單動態字元串到後面再說,集合類型的底層數據結構主要有 5 種:整數數組、雙向鏈表、哈希表、壓縮列表和跳錶。
整數數組和雙向鏈表也很常見,它們的操作特徵都是順序讀寫,也就是通過數組下標或者鏈表的指針逐個元素訪問,操作復雜度基本是 O(N),操作效率比較低;壓縮列表和跳錶我們平時接觸得可能不多,但它們也是 Redis 重要的數據結構。
壓縮列表 實際上類似於一個數組,數組中的每一個元素都對應保存一個數據。和數組不同的是,壓縮列表在表頭有三個欄位 zlbytes、zltail 和 zllen,分別表示列表長度、列表尾的偏移量和列表中的 entry 個數;壓縮列表在表尾還有一個 zlend,表示列表結束。
跳錶 在鏈表的基礎上,增加了多級索引,通過索引位置的幾個跳轉,實現數據的快速定位,如下圖所示:
Redis 之所以能快速操作鍵值對,一方面是因為 O(1) 復雜度的哈希表被廣泛使用,包括 String、Hash 和 Set,它們的操作復雜度基本由哈希表決定,另一方面,Sorted Set 也採用了 O(logN) 復雜度的跳錶。不過,集合類型的范圍操作,因為要遍歷底層數據結構,復雜度通常是 O(N)。
不能忘了復雜度較高的 List 類型,它的兩種底層實現結構:雙向鏈表和壓縮列表的操作復雜度都是 O(N)。因此,因地制宜地使用 List 類型。例如,既然它的 POP/PUSH 效率很高,那麼就將它主要用於 FIFO 隊列場景,而不是作為一個可以隨機讀寫的集合。