redis源碼閱讀之面向哈希表優化

寫在前面

2020年了,給本身加個任務,把redis代碼完整讀一遍。我新建了一個github項目(地址在文章末尾),會在redis源碼之上增長註釋,後續也會爲其中一些值得拎出來講的點單獨寫文章。git

本文內容:github

  • 常規哈希表科普
  • redis rehash面臨的問題
  • redis的漸進式hashredis

    • 何時會啓動rehash
    • 如何漸進式rehash
    • 何時執行一步rehash
    • rehash進行時又有增刪改查如何處理
    • 何時不容許rehash
    • 桶的初始數量,擴容後大小,縮容後大小
  • redis dict的其餘優化
  • dict benchmark

常規哈希表科普

首先,科普一下哈希表(hash table)的常規實現。通常來講,哈希表基於數組實現,數組的每一個元素即爲一個桶(bucket or slot),向哈希表插入鍵值對(key-value pair or entry)時,先對key使用hash函數獲得hash code(一個整型值),而後用hash code取餘桶數量獲得對應的桶下標,最後將entry存入桶中。macos

刪除、修改、查找的操做相似,增刪改查的時間複雜度都是O(1)。數組

因爲不一樣的entry可能哈希到同一個桶內,爲了解決哈希衝突的問題,可使用鏈地址法。即桶中存放鏈表,鏈表上存放哈希到這個桶的多個entry。數據結構

那麼問題來了,因爲桶數量是固定的,插入的entry越多,衝突也就越多,桶上的鏈表就越長,時間複雜度也就慢慢退化成遍歷鏈表的時間複雜度。多線程

那麼桶數量設置爲多大合適呢,太大浪費內存,過小不夠快。因此一個高性能的哈希表內部都會提供擴容、縮容策略(rehash)。即根據內部存儲的entry個數和桶個數的比例,決定是否調整桶個數。桶個數調整後,本來屬於同一個桶的元素,可能變成屬於不一樣的桶,因此全部的entry都須要從新計算歸屬於哪一個桶。即rehash是O(n)的。併發

名詞補充:哈希表的裝載因子(load factor) = entry總數 / 桶個數

redis rehash面臨的問題

很顯然,當一個哈希表的元素個數很是多時,rehash可能會很是耗時。而redis面臨的問題更嚴重,因爲redis是個單線程模型,雖然省去了不少線程間同步、切換的開銷,可是缺點也很明顯,就是一旦有耗時或阻塞操做,全部其餘工做都無法作,好比讀取客戶端的數據、處理其餘哈希表等等。dom

redis的解決方案是,將rehash的操做分步進行。即rehash作一點,又去作其餘工做,不讓其餘工做等過久,運用分治思想,將rehash的開銷分攤開。下面咱們來詳細介紹一下redis的rehash實現。函數

redis的漸進式rehash

聲明,爲了後文不產生歧義,咱們將redis中基於哈希表提供給上層使用的鍵值型數據結構統一描述成Dict(字典)。

何時會啓動rehash

會致使dict中元素增長的函數,都會判斷裝載因子是否大於5,若是是,則開啓rehash。

dict也直接提供了接口dictResize供上層調用。好比,上層能夠在定時器中讀取dict當前裝載因子,決定是否手動觸發rehash。

刪除元素時內部並不會主動觸發rehash,上層能夠自行決定是否縮容。

如何漸進式rehash

Dict內部使用兩塊哈希表。在正常狀況下,Dict只使用0號哈希表,只有rehash時,纔會使用到1號哈希表。rehash時,是逐步將0號老哈希表遷移到1號新哈希表的過程,徹底遷移完成後,再將1號哈希表標記爲0號哈希表,並結束rehash。

這裏說的逐步,順序是從0號哈希表的第一個桶到最後最後一個桶。

rehash分步的最小粒度,是0號哈希表中的一個桶中全部entry都遷移到1號哈希表上。

遷移過程當中,一個entry只會存在於一個哈希表上,不會同時重複存在。

何時執行一步rehash

增刪改查時都會進行小步rehash,而且只遷移一個桶

提供了dictRehash(dict *d, int n)接口,上層能夠直接調用並傳入參數,指定本次想要遷移的桶的數量來手動觸發遷移。

遷移時有個細節,空桶和非空桶遷移時耗時是有明顯區別,redis爲了區分對待,將空桶單獨計數,爲想遷移桶的10倍。

另外還提供了dictRehashMilliseconds(dict *d, int ms)接口,上層能夠經過傳遞限制時間,手動觸發遷移,並設置這次遷移的時長。

rehash進行時又有增刪改查如何處理

增長時,直接往新的1號哈希表增長。

刪除、修改、查詢時,因爲沒法肯定entry在哪塊哈希表上,因此只能先查0號哈希表,找不到再查1號哈希表。

何時不容許rehash

若是在rehash進行中,上層獲取並長久持有了dict的迭代器,那麼rehash須要暫停,以免迭代器迭代時訪問到重複entry或丟失entry。

另外redis若是正在將數據持久化,也會關閉rehash的開關,避免copy-on-write受影響。

桶的初始數量,擴容後大小,縮容後大小

redis dict的桶初始數量是4,後續縮容也最少保持4個桶。

擴容後大小爲擴容前entry數量的兩倍,取整到2的冪方。

縮容縮到當前entry個數,取整到2的冪方。

redis dict的其餘優化

entry插入時,向桶鏈表的最前面插入,這裏運用的是時間局部性原理,認爲新插入的元素後續被訪問的概率高。

桶數量永遠爲2的冪方,hash code換算成桶下標時,使用按位與運算而不是取餘運算,更高效,我以前的文章 譯- Go開源項目BigCache如何加速併發訪問以及避免高額的GC開銷 也有提到這種方式。

查找訪問後刪除這種一般須要兩次查找開銷的操做,可合併爲一次查找操做。

dict的value使用了union,便可存儲指針,又可存儲int基礎類型。

dict benchmark

dict.c中自帶了一個benchmark程序,在個人macos上執行,輸出以下:

Inserting: 5000000 items in 4778 ms
Linear access of existing elements: 5000000 items in 2685 ms
Linear access of existing elements (2nd round): 5000000 items in 2703 ms
Random access of existing elements: 5000000 items in 3664 ms
Accessing missing: 5000000 items in 2985 ms
Removing and adding: 5000000 items in 5919 ms

結語

redis字典的源碼大概有1500行左右,本文還有許多細節沒有講,感興趣的能夠看看我提供了註釋版本的源碼:https://github.com/q191201771...

原文連接: https://pengrl.com/p/0010/
原文出處: yoko blog ( https://pengrl.com)
原文做者: yoko ( https://github.com/q191201771)
版權聲明: 本文歡迎任何形式轉載,轉載時完整保留本聲明信息(包含原文連接、原文出處、原文做者、版權聲明)便可。本文後續全部修改都會第一時間在原始地址更新。
本篇文章由一文多發平臺ArtiPub自動發佈
相關文章
相關標籤/搜索