阿里面試官:HashMap 熟悉吧?好的,那就來聊聊 Redis 字典吧!

最近,小黑哥的一個朋友出去面試,回來跟小黑哥抱怨,面試官不按套路出牌,直接打亂了他的節奏。html

事情是這樣的,前面面試問了幾個 Java 的相關問題,我朋友回答還不錯,接下來面試官就問了一句:看來 Java 基礎還不錯,Java HashMap 你熟悉吧?面試

我朋友回答。工做常常用,有看過源碼。redis

我朋友原本想着,你隨便來吧,這個問題以前已經準備好了,隨便問吧。算法

誰知道,面試官下面一句:數組

那好的,咱們來聊聊 Redis 字典吧。服務器

直接將他整蒙逼。數據結構

小黑哥的朋友因爲沒怎麼研究過 Redis 字典,因此這題就直接回答不知道了。ide

固然,若是面試中真不知道,那就回答不瞭解,直接下一題,不要亂答。學習

不過這一題,小黑哥以爲仍是很惋惜,其實 Redis 字典基本原理與 HashMap 差很少,那咱們其實能夠套用這其中的原理,不求回答滿分,可是怎麼也能夠得個及格分吧~idea

面試過程真要碰到這個問題,咱們能夠從下面三個方面回答。

  • 數據結構
  • 元素增長過程
  • 擴容

字典數據結構

提及字典,也許你們比較陌生,可是咱們都知道 Redis 自己提供 KV 查詢的方式,這個 KV 就是其實經過底層就是經過字典保存。

另外,Redis 支持多種數據類型,其中一種類型爲 Hash 鍵,也能夠用來存儲 KV 數據。

小黑哥剛開始瞭解的這個數據結構的時候,原本覺得這個就是使用字典實現。其實並非這樣的,初始建立 Hash 鍵,默認使用另一種數據結構-ZIPLIST(壓縮列表),以此節省內存空間。

不過一旦如下任何條件被知足,Hash 鍵的數據結構將會變爲字典,加快查詢速度。

  • 哈希表中某個鍵或某個值的長度大於 server.hash_max_ziplist_value (默認值爲 64 )。
  • 壓縮列表中的節點數量大於 server.hash_max_ziplist_entries (默認值爲 512 )。

Redis 字典新建時默認將會建立一個哈希表數組,保存兩個哈希表。

其中 ht[0] 哈希表在第一次往字典中添加鍵值時分配內存空間,而另外一個 ht[1] 將會在下文中擴容/縮容纔會進行空間分配。

字典中哈希表其實就等同於Java HashMap,咱們知道 Java 採用數組加鏈表/紅黑樹的實現方式,其實哈希表也是使用相似的數據結構。

哈希表結構以下所示:

其中 table 屬性是個數組, 其中數組元素保存一種 dictEntry 的結構,這個結構徹底相似與 HashMap 中的 Entry 類型,這個結構存儲一個 KV 鍵值對。

同時,爲了解決 hash 碰撞的問題,dictEntry 存在一個 next 指針,指向下一個dictEntry ,這樣就造成 dictEntry 的鏈表。

如今,咱們回頭對比 Java 中 HashMap,能夠發現二者數據結構基本一致。

只不過 HashMap 爲了解決鏈表過長問題致使查詢變慢,JDK1.8 時在鏈表元素過多時採用紅黑樹的數據結構。

下面咱們開始添加新元素,瞭解這其中的原理。

元素增長過程

當咱們往一個新字典中添加元素,默認將會爲字典中 ht[0] 哈希表分配空間,默認狀況下哈希表 table 數組大小爲 4(DICT_HT_INITIAL_SIZE)。

新添加元素的鍵值將會通過哈希算法,肯定哈希表數組的位置,而後添加到相應的位置,如圖所示:

繼續增長元素,此時若是兩個不一樣鍵通過哈希算法產生相同的哈希值,這樣就發生了哈希碰撞。

假設如今咱們哈希表中擁有是三個元素,:

咱們再增長一個新元素,若是此時恰好在數組 3 號位置上發生碰撞,此時 Redis 將會採用鏈表的方式解決哈希碰撞。

注意,新元素將會放在鏈表頭結點,這麼作目的是由於新增長的元素,很大機率上會被再次訪問,放在頭結點增長訪問速度。

這裏咱們在對比一下元素添加過程,能夠發現 Redis 流程其實與 JDK 1.7 版本的 HashMap 相似。

當咱們元素增長愈來愈多時,哈希碰撞狀況將會愈來愈頻繁,這就會致使鏈表長度過長,極端狀況下 O(1) 查詢效率退化成 O(N) 的查詢效率。

爲此,字典必須進行擴容,這樣就會使觸發字典 rehash 操做。

擴容

當 Redis 進行 Rehash 擴容操做,首先將會爲字典沒有用到 ht[1] 哈希表分配更大空間。

畫外音:ht[1] 哈希表大小爲第一個大於等於 ht[0].used*2 的 2^2(2的n 次方冪)

而後再將 ht[0] 中全部鍵值對都遷移到 ht[1] 中。

簡單起見,忽略指向空節點

當節點所有遷移完畢,將會釋放 ht[0]佔用空間,並將 ht[1] 設置爲 ht[0]

s

擴容 操做須要將 ht[0]全部鍵值對都 Rehashht[1] 中,若是鍵值過多,假設存在十億個鍵值對,這樣一次性的遷移,勢必致使服務器會在一段時間內中止服務。

另外若是每次 rehash 都會阻塞當前操做,這樣對於客戶端處理很是不友好。

爲了不 rehash對服務器的影響,Redis 採用漸進式的遷移方式,慢慢將數據遷移分散到多個操做步驟。

這個操做依賴字典中一個屬性 rehashidx,這是一個索引位置計數器,記錄下一個哈希表 table 數組上元素,默認狀況爲值爲 -1

假設此時擴容前字典如圖所示:

當開始 rehash 操做,rehashidx將會被設置爲 0

這個期間每次收到增長,刪除,查找,更新命令,除了這些命令將會被執行之外,還會順帶將 ht[0]哈希表在 rehashidx 位置的元素 rehash 到 ht[1] 中。

假設此時收到一個 K3 鍵的查詢操做,Redis 首先執行查詢操做,接着 Redis 將會爲 ht[0]哈希表上 table 數組第 rehashidx索引上全部節點都遷移到 ht[1] 中。

當操做完成以後,再將 rehashidx 屬性值加 1。

最後當全部鍵值對都 rehashht[1]中時,rehashidx將會被從新設置爲 -1。

雖然漸進式的 rehash 操做減小了工做量,可是卻帶來鍵值操做的複雜度。

這是由於在漸進式 rehash 操做期間,Redis 沒法明確知道鍵到底在 ht[0]中,仍是在 ht[1] 中,因此這個時候 Redis 不得不查找兩個哈希表。

以查找爲例,Redis 首先查詢 ht[0] ,若是沒找到將會繼續查找 ht[1],除了查詢之外,更新,刪除也會執行如上的操做。

添加操做其實就沒這麼麻煩,由於ht[0]不會在使用,那就統一都添加到 ht[1] 中就行了。

最後咱們再對比一下 Java HashMap 擴容操做,它是一個一次性操做,每次擴容須要將全部鍵值對都遷移到新的數組中,因此若是數據量很大,消耗時間就會久。

總結

Redis 字典使用哈希表做爲底層實現,每一個字典包含兩個哈希表,一個平時使用,一個僅在 rehash 操做中使用。

哈希表總的來講,跟 Java HashMap 真的很相似,底層實現也是一個數組加鏈表數據結構。

最後,當對哈希表進行擴容操做時間,將會採用漸進性 rehash 操做,慢慢將全部鍵值對遷移到新哈希表中。

其實瞭解 Redis 字典的其中的原理,再去比較 Java HashMap ,其實能夠發現這二者有如此多的類似點。

因此學習這類知識時,不要僅僅去背,咱們要了解其底層原理,知其然知其因此然。

幫助資料

  1. https://redisbook.readthedocs.io/en/latest/internal-datastruct/dict.html

歡迎關注個人公衆號:程序通事,得到平常乾貨推送。若是您對個人專題內容感興趣,也能夠關注個人博客:studyidea.cn

相關文章
相關標籤/搜索