Redis基礎知識學習筆記

本文爲專欄《Redis核心技術與實戰 - 蔣德鈞 - 極客時間》的學習筆記。另外,在此還要感謝評論區大神、課表明 Kaito 同窗的分享與幫助。react

✔️ 知識點總覽

首先咱們都知道 Redis 是一個很是經典的,高性能的,「單線程」的鍵值數據庫。web

爲何高性能呢?除了 Redis 是基於內存的數據庫以外,還要歸功於它的底層數據結構。高效的數據結構是Redis快速處理數據的基礎。算法

除了數據結構之外,爲何Redis是「單線程」的,卻還可以那麼快?那咱們就須要瞭解 Redis 的線程模型究竟是怎樣的。數據庫

對於一款數據庫來講,光夠快是不夠的,還須要夠強壯,也就是常說的高可用。後端

對於 Redis 的高可用來講,基於內存的數據庫有一個致命問題:一旦發生宕機,內存中的數據將會所有丟失。若是單純地從後端數據庫恢復數據,是很是耗費性能且耗時的。因此持久化機制對於 Redis 來講是十分必要的。而咱們知道,讀寫磁盤是很是耗時的操做,那麼 Redis 是如何在保證高性能的前提下實現持久化機制的呢?這就須要來了解一下 AOFRDB 了。數組

高可用不止包括宕機後的數據恢復,還包括服務儘可能少的中斷。Redis 採用了主從庫讀寫分離的模式,具體是如何實現的呢?數據如何同步?又是如何保證主從數據一致的呢?同時還要兼顧到在此過程當中儘可能不要讓主庫中斷對外提供服務。這就須要瞭解 Redis 的主從架構了。緩存

那麼這又帶來了新的問題:主庫掛了怎麼辦?若是主庫掛了咱們確定須要一個新的主庫,好比把某一個從庫切換爲主庫。那麼須要考慮的問題是:如何判斷主庫真的掛了?若是切換的話應該選哪一個從庫做爲新主庫?切換完成後如何將新主庫的信息通知給從庫和客戶端呢?markdown

Redis 經過哨兵機制實現了主從庫自動切換功能,高效解決了主動複製模式下故障轉移的問題。網絡

瞭解哨兵機制後,新的問題又又又來了:該由哪一個哨兵執行主從切換?若是哨兵掛了還能執行主從切換嗎?數據結構

達到了夠快,夠強以後,最後還要看夠不夠裝。若是須要存儲的數據量很是龐大怎麼辦?咱們須要瞭解什麼是切片集羣以及它的實現方案。

至此,就對 Redis 相關的基礎知識點有了一個全局的大致上的瞭解,而後針對每一個點再進行深挖。

✔️ 數據類型 & 數據結構

Redis有哪幾種數據類型?底層數據結構是怎樣的?他們之間是如何對應的?

Redis 中的全部數據都是以鍵值對的形式保存在全局哈希表中,每一個鍵值對的值又對應了多種數據類型,借用專欄中的一張圖:

有序集合爲何選擇跳錶而不是紅黑樹?

有序集合選擇跳錶而沒有選擇紅黑樹,是由於雖然插入刪除查找時間複雜度相同,可是根據區間查找這個操做紅黑樹沒有跳錶效率高。

整數數組和壓縮列表在查找操做的時間複雜度上沒有很大優點,爲何仍是被 Redis 選爲底層數據結構?

一是由於Redis是內存數據庫,須要儘可能優化內存,提升內存利用率。數組和壓縮列表是很是緊湊的數據結構,比鏈表佔用的內存要少。

二是由於數組對CPU高速緩存支持更友好(空間局部性:訪問數組時會將訪問元素附近的多個元素一塊兒帶到高速緩存中)。因此當集合數據元素比較少時,默認採用內存緊湊排列的方式存儲,同時可以利用CPU高速緩存,不會下降訪問速度。當元素數量超過閾值以後,避免查詢時間複雜度過高,保證查詢效率,轉爲哈希或者跳錶結構。

什麼是漸進式rehash?過程是怎樣的?

當全局哈希表內數據愈來愈多,某些衝突鏈會過長,查詢效率下降。因此 Redis 會進行 rehash 操做:增長哈希桶數量,減小單個桶中的元素。

爲了讓 rehash 操做更加高效,Redis 默認使用兩個全局哈希表,每次只使用其中一個。當元素數量達到閾值,便進行 rehash 操做:給另外一個哈希表分配更大的內存空間,將正在使用的哈希表數據 copy 過去,釋放舊的哈希表空間。

可是這個過程涉及大量數據拷貝,一次性遷移會致使線程阻塞。爲了不採起了漸進式 rehash 的操做:分配更大的新的空間以後,Redis 仍然正常處理請求,每處理一個請求,會將舊哈希表中第一個索引位置的全部元素copy 到新哈希表中。下一個請求時再 copy 一份。這樣將一次性的大量拷貝分攤到了多個請求處理過程當中,避免了線程阻塞。

✔️ 線程模型

Redis爲何用單線程?Redis是單線程的爲何可以這麼快?Redis的線程模型是什麼樣的?

Redis爲何用單線程?

當咱們編寫多線程程序,在剛開始增長線程數時,系統的吞吐率會上升。但增長到必定程度以後,吞吐量的提高會趨於平緩,甚至降低。這是由於多線程模式須要處理共享資源的併發訪問控制,另外多個線程的切換也會消耗必定的資源。還有多線程也會提高系統的複雜度,增長開發難度,下降可維護性。最後就是 Redis 服務中運行的絕大多數操做的性能瓶頸都不是CPU,使用多線程的意義不大。

Redis是單線程的爲何可以這麼快?

首先Redis採用了高效的數據結構,這是它高性能的一個重要緣由。另外一個緣由就是 Redis 的線程模型:單線程下的多路複用機制。實際上是基於 reactor 模型的單 reactor 單線程模式。要注意 Redis 並非徹底單線程的,但主要的網絡 IO 和鍵值對讀寫是由一個線程完成的,也是對外服務的主要流程,因此常說 Redis 是單線程的。其餘功能好比持久化、異步刪除、集羣數據同步等都是其餘額外線程完成的。

Redis的線程模型是什麼樣的?

Redis 單線程模型主要是文件事件處理器,包括4個部分:

  1. 多個 socket
  2. IO 多路複用程序
  3. 文件事件分派器
  4. 事件處理器(鏈接應答處理器、命令請求處理器、命令回覆處理器)

客戶端與服務端的一次通訊過程是這樣的:

Redis 初始化時,會將 server socket 的 AE_READABLE 事件與鏈接應答處理器關聯。

客戶端 socket 向 Redis 請求鏈接,server socket 會產生一個 AE_READABLE 事件,IO 多路複用程序監聽到以後,將 socket 壓入隊列,文件事件分派器從隊列中獲取 socket ,交給鏈接應答處理器。鏈接應答處理器建立一個能與客戶端通訊的 socket01 ,並將 AE_READABLE 事件與命令請求處理器關聯。

假設客戶端發送一個 set 請求,Redis 中的 socket01 會產生 AE _READABLE 事件,IO 多路複用程序將 socket01 壓入隊列,事件分派器獲取到 socket01 以後,由於事件已經關聯了命令請求處理器,因此會交給命令請求處理器來處理。命令請求處理器完成寫入操做,將 socket01 的AE_WRITABLE 事件與命令回覆處理器關聯。 客戶端若是準備好接受返回結果了,Redis 的 socket01 會產生一個 AE_WRITABLE 事件,壓入隊列中,事件分派器找到關聯命令的命令回覆處理器,由命令回覆處理器對 socket01 輸入本次操做的結果(好比 ok ),解除socket01 的 AE_WRITABLE 事件與命令回覆處理器的關聯。這樣就完成了一次通訊。

對照下圖來看,注意 server socket 其實應該有多個,不要被誤導。實在很差意思忘記圖出自哪裏了,侵刪。

注意 Redis 6.0 引入了多線程,具體後邊再說。

✔️ 持久化機制

什麼是AOF日誌?具體是如何實現的?

不一樣於通常數據庫的寫前日誌(WAL),AOF 是一種寫後日志。AOF 日誌是以文本的形式保存下來了 Redis 收到的每一條命令,先寫入內存中的緩衝區,而後再擇機落入磁盤。因爲是命令執行成功後再記錄日誌,因此記錄日誌時再也不須要對命令進行語法檢查,記錄日誌的過程也不會阻塞當前的寫操做。

但同時也帶來了兩個問題:一個是若是剛執行完命令尚未寫 AOF 日誌就宕機了,這時就有數據丟失的問題。另外一個是雖然向磁盤寫日誌不阻塞當前操做,但有可能會阻塞後續操做,由於 AOF 也是在主線程中執行的,而將數據寫入磁盤這個操做是一個相對很慢的操做。

咱們能夠經過配置有取捨地解決這兩個問題。AOF的配置 appendfysnc 有三個值可選:

  1. Always:同步寫回。每一個命令執行完,馬上將AOF日誌寫到磁盤。
  2. Evertsec:每秒寫回。每一個命令執行完,先講AOF日誌寫到AOF內存緩衝區中,而後每隔一秒將緩衝區中的全部內容寫入磁盤。
  3. No:由操做系統控制什麼時候將緩衝區內容寫回磁盤。

這三種方案都沒法作到既兼顧高性能,又兼顧高可靠性。第一種可靠性最高,數據丟失機率很是小,但性能最差。第二種性能適中,宕機最多丟失 1 秒內的數據。第三種性能最好,但宕機時丟失的數據也更多。

AOF 日誌愈來愈大怎麼辦?什麼是 AOF 重寫機制?重寫的過程是怎樣的?有哪些地方有可能會阻塞?

AOF 不斷地往裏追加內容,會變得愈來愈大。文件過大有可能系統有限制,沒法保存。添加新的內容時效率也會更低。並且發生宕機後恢復數據須要一條一條執行很是多的命令,過程會變得很慢。這就須要 AOF 重寫機制了。

AOF 重寫就是以當前數據庫的全部鍵值對爲準,從新建立一個新的 AOF 日誌,裏邊記錄了全部鍵值對的寫入命令。這樣舊的 AOF 日誌中對於一個鍵有可能有不少條命令,重寫後就變爲一條了。

Redis 爲了保證高性能,重寫時固然不會讓主線程阻塞。重寫過程能夠總結爲**「一個拷貝,兩處日誌」**。

重寫時會由主線程 fork 出一個後臺的子進程,fork 會拷貝一份主線程的內存給子進程。此時主線程不會阻塞,仍然繼續處理新的操做,新的命令仍然會存在舊的AOF日誌中。同時這些新的指令也會存在 AOF 重寫緩衝區中。等子進程對拷貝的全部數據都重寫完成以後,AOF 重寫緩衝區中的內容也會寫入新的 AOF 日誌,完成以後就能夠用新的 AOF 替代舊的了。

這裏須要注意的是,Redis 爲了不一次性拷貝大量的數據,採用了 Copy On Write 機制。fork 時複製給子進程的其實是內存頁表(虛擬內存和物理內存的映射索引表),而不是實際內存數據。此時主進程和子進程共享內存中的數據。當主進程某個 key 有新數據寫入時,會分配一塊新的內存,將數據寫入新的內存。這樣主進程和子進程的數據就會逐漸分離。這裏須要注意,Copy On Write 的粒度是內存頁,也就是說主進程分配到一塊新的內存以後,要把當前寫入數據所在的內存頁一塊兒所有 copy 過去。

在這個過程當中,有兩個有可能會阻塞的點須要注意:fork 時複製內存頁表這個過程會消耗大量 CPU 資源,拷貝時是會阻塞進程的,阻塞時間取決於整個實例的內存大小。另外,Copy On Write 時,複製的粒度是內存頁,默認一頁的大小是 4kb。若是複製的 key 是一個 bigkey,那麼從新申請大塊內存並複製也是一個耗時比較長的過程。

還有若是系統開啓了內存大頁機制(Huge Page,內存頁大小爲 2M ),那麼主進程申請內存後複製時阻塞的時間會大大增加。因此使用 Redis 時建議關閉系統 huge page 功能。(Huge Page 特性主要是爲了提升 TLB 命中率,相同的內存大小下,Huge Page 能夠減小頁表項,TLB就能夠緩存更多的頁表項,能減小 TLB miss 致使的開銷)

其實在不少丟失數據不敏感的業務場景,通常是不須要開啓 AOF 的。

什麼是RDB?RDB的機制是怎樣的?

RDB,即內存快照。Redis 持久化的另外一種方案,配合 AOF 使用口感更佳。

若是隻是用 AOF 來作持久化,當數據量很大,操做記錄不少的時候,若是要作故障恢復,須要一條一條執行不少命令,效率低下。須要 RDB 來配合處理。

生成 RDB 文件就是將某一時刻Redis中的全部數據以文件的形式寫在磁盤上,若是宕機後恢復數據,只須要直接讀入內存便可,相比 AOF 效率很高。

Redis提 供了兩個指令來生成 RDB 文件:

  1. save:主線程中執行,會致使阻塞
  2. bgsave:建立一個子進程,專門用於寫入 RDB 文件,默認配置。

bgsave 命令建立 RDB 文件的過程與 AOF 重寫相似,一樣藉助 COW 技術。從主線程 fork 出子進程(拷貝內存頁表),與主線程共享內存數據。當主線程有寫操做發生時,複製對應的內存頁。

要注意,RDB 文件不宜頻繁生成。一方面會給磁盤帶來很是大壓力,並且有可能出現一次 RDB 尚未寫完,後一次就已經開始了,從而陷入惡性循環。另外一方面 fork 的過程是會阻塞主線程的,也會影響性能。

還有就是能夠採用增量快照的方式避免屢次全量快照的開銷:在一次全量快照以後,記錄下哪些數據被修改了,以後生成 RDB 只對修改的數據進行記錄。但這會帶來另外一個問題:記錄修改操做會額外耗費不少的內存,Redis 的內存是很寶貴的資源。

因此說若是隻是用 RDB 的方式作數據持久化,沒法肯定一個很好的快照頻率。若是頻率過高影響性能,若是頻率過低,宕機發生的話會丟失大量數據。因此通常的用法是 RDB 結合 AOF 同時使用。

以必定頻率執行 RDB,在兩次 RDB 之間,使用 AOF 日誌記錄。等到第二次 RDB 的時候,中間的 AOF 就能夠清空了。恢復數據時首先使用 RDB 文件恢復大部分數據,而後在使用 AOF 恢復剩餘的部分數據,這樣就基本達到了魚和熊掌兼得的目的。

對於 RDB 的頻率,Redis 默認的配置是: 知足下邊這三種任一種狀況,都會執行 bgsave 命令

save 900 1 // 900 秒內,對數據庫至少修改 1 次。下面同理
save 300 10
save 60 10000
複製代碼

Redis 4.0 以後,AOF 重寫時,就是將內存數據以 RDB 的格式寫入 AOF 文件的開頭。但帶來的問題是 RDB 格式的數據可讀性不好。

✔️ 主從架構

對於高可靠性,RDB 和 AOF 保證了數據儘可能不丟失,而服務儘可能少中斷須要主從架構來保證。Redis 的主從庫模式採用的是讀寫分離的方式。主從庫均可以讀,但寫只能是主庫,再由主庫同步給從庫。

Redis主從之間是如何實現數據一致的?數據同步的過程是怎樣的?

在主從庫第一次同步時,須要進行一次全量複製。從庫和主庫創建鏈接,並告訴主庫即將進行同步。主庫確認回覆後,便可開始同步。

從庫須要向主庫發送 psync 命令:psync runID offset。runID 是每一個 Redis 實例都會自動生成的一個隨機惟一 ID,用來標記示例。offset 是複製的偏移量。第一次複製時,runID 未知,傳 ? 。offset 傳 -1。 主庫收到後會返回本身的 runID,和目前主庫的複製進度 offset。

首次複製主庫會將所有數據發送給從庫,這個過程依賴於 RDB。也就是生成一份 RDB 文件發送給從庫,從庫會先清空數據庫以後,將數據讀入內存。在複製的同時,主庫也會正常提供服務,並將新的寫操做緩存在 replication buffer 中。最後,當 RDB 文件發送完成後,主庫把 replication buffer 中的數據發送給從庫,從庫從新執行這些操做,同步就完成了。

完成了首次全量複製以後,主從之間會維護一個長鏈接,主庫會將後續收到的全部命令經過連接同步給從庫。 這裏須要注意,若是有不少從節點掛在主節點上,主節點要和全部從庫進行全量複製的話,會給主庫帶來極大的壓力。通常會採用 主-從-從 模式,讓更多的從節點掛在其餘從節點上,這樣能夠分攤主庫的壓力。

還須要考慮的地方是,網絡鏈接阻塞甚至斷開了怎麼辦?Redis 2.8 之前一旦主從節點網絡斷開,從庫會從新進行一次全量複製,這個開銷是很是大的。2.8 之後從庫開始進行增量複製。具體是利用到了 repl_backlog_buffer 緩衝區。

repl_backlog_buffer 是一個環形緩衝區,每有一個從庫掛到主庫,都會分配一塊出來。主庫除了將全部命令同步給從庫以外,也會在這個緩衝區中記錄一份。當從庫斷開鏈接重連以後,從新發送命令到主庫,主庫根據 offset 在 repl_backlog_buffer 中找到斷開的位置,將以後的命令發送給從庫便可。因爲 repl_backlog_buffer 是環形的,因此若是主從斷開過久,新的緩存會把舊的緩存覆蓋掉,這以後從庫再連回來(或者網絡延遲、從庫執行緩慢致使),那就不得再也不從新進行一次全量複製了。 因此要控制好 repl_backlog_size 這個參數的大小。通常粗略計算爲:repl_backlog_size = 主庫寫入命令速度 * 命令大小 - 主從庫命令傳輸速度 * 命令大小。考慮到一些突發的請求壓力,通常還會再在結果上乘2。若是併發峯值特別大,那麼還能夠設置爲更大,或者考慮使用切片機羣來分擔主庫請求壓力。後邊再說。

注意區分 replication buffer 和 repl_backlog_buffer。

前者是 Redis 服務端與客戶端通訊時,用來交互數據的緩存。每一個客戶端鏈接都會分配一塊 buffer 出來。Redis 先把數據寫入這個 buffer,而後將 buffer 中的數據發到 client socket 中經過網絡發送出去。從庫也是一個 client,也是同樣的,專門用來將用戶的寫命令從主庫傳到從庫。Redis 提供了 client-output-buffer-limit 參數限制這個 buffer 的大小,若是超過限制,主庫會強制斷開從庫的鏈接。若是不限制,從庫處理請求的速度又很慢的話,這個 buffer 會無限膨脹,最終致使 OOM。

然後者是爲了主從同步設計的,避免一旦斷開就要進行全量複製的性能開銷。這個 buffer 只用來對比主從數據差別,真正信息傳遞仍是要靠 replication buffer。

什麼是Redis的哨兵機制?基本流程是怎樣的?

哨兵機制實現了Redis主從集羣故障轉移的功能,若是主庫掛掉,能夠自動執行主從切換。

哨兵機制主要解決主從切換的三個問題:

  1. 如何判斷主庫真的掛掉了?(監控)
  2. 選擇哪一個從庫做爲主庫?(選主)
  3. 如何把新主庫的信息通知到從庫和客戶端?(通知)

哨兵機制的流程:

哨兵進程在運行時,會週期性地給全部主從庫發送 PING 命令,監測他們是否正常運行。若是一個節點沒有在規定的時間內響應哨兵,則會被標記爲「主觀下線」。

若是是從節點,下線影響不大,標記完就行了。若是是主節點,不能直接開始主從切換。由於有可能存在誤判,好比網絡堵塞或是主庫壓力比較大。主從切換的開銷很大,必需要避免沒必要要的開銷。

哨兵通常也會集羣部署,因此須要有超過一半的哨兵都認爲主庫「主觀下線」,主庫纔會被標記爲「客觀下線」。這時纔會觸發主從切換流程。

哨兵經過篩選加打分來選擇新的主庫。

若是從庫老是和主庫斷連,則說明這個從庫網絡情況很差,不適合作主庫。這樣的節點會被篩選掉。

剩下的節點中會進行三輪打分。

第一輪優先級最高的從庫得分高。咱們能夠經過配置,給從庫不一樣的優先級。人爲給一個性能最好的機器上的從庫優先級設爲最高,那麼主從切換時就會選這個從庫做爲主庫。

第二輪判斷從庫和舊主庫的同步程度,越接近的得分越高。這裏是經過主從同步的 repl_backlog_buffer 中的 offset 對比判斷。

若是還相等,就進行第三輪判斷,ID號小的從庫得分高。

選出新的主庫以後,哨兵會把新主庫的鏈接信息發送給其餘從庫,讓他們執行 replicaof 命令,和新主庫創建鏈接,並進行數據同步。同時哨兵也會通知客戶端,讓客戶端將請求操做發送到新的主庫節點。

須要注意的是,在主從切換期間,若是是讀寫分離的,那麼讀請求能夠在從庫正常執行。但主庫掛掉,並且尚未新主庫產生時,寫請求會失敗。若是不想讓客戶端感知到主從切換,須要客戶端將寫請求寫入消息隊列中,待主從切換完成後再從消息隊列中拉取指令執行。

客戶端與哨兵之間經過廣播的方式同步主從節點信息。哨兵會向客戶端廣播主庫地址。客戶端也能夠主動向哨兵獲取,通常的SDK都封裝了相應功能。

哨兵集羣之間的通訊是經過 Redis 的 pub/sub 機制進行的。哨兵只要和主庫創建了鏈接,就會在主庫的一個固定主題 sentinel:hello 下發布本身的ip和端口等信息。全部的哨兵都會經過對這個主題的訂閱和發佈來發現其餘哨兵。發現以後他們會彼此之間創建網絡鏈接來通訊。

哨兵還須要鏈接從庫來進行監控。這是經過向主庫發送 INFO 命令來完成的。主庫接收到 INFO 命令以後,會將從庫列表發送給哨兵。

哨兵還須要鏈接客戶端,向客戶端廣播監控、選主、切換等各個過程當中發生的事件。這也是經過 pub/sub 機制完成的。哨兵提供了多個消息主題,不一樣主題包含了不一樣的關鍵事件。好比主庫主觀上/下線,客觀上/下線,主從切換等。

由哪一個哨兵來執行主從切換是如何肯定的?若是有哨兵掛了還能執行主從切換嗎?

哨兵會經過選舉機制來選出執行主從切換的哨兵。結合判斷主庫下線的流程一塊兒來看:

當任何一個哨兵發現主庫主觀下線後,就會向其餘哨兵發送消息,讓他們馬上確認主庫狀態。其餘哨兵若是發現主庫也主觀下線下,返回 Y,不然返回 N。當一個哨兵得到了大於等於配置項中的 quorum 給定的數量的同意票時(包括本身的一張同意票),該哨兵標記主庫爲客觀下線。

此時該哨兵會向其餘哨兵發送一條消息來發起投票,表示但願由本身來執行主從切換(至關於投資及一票)。最終成爲 Leader 去執行主從切換的哨兵須要達成兩個條件:得到超過半數以上的同意票而且同意票數量要大於等於 quorum。(3 個哨兵,quorum=2,那麼就須要 2 票來當選)。

在一輪投票中,每一個哨兵只能投一票。當一個哨兵收到了其餘哨兵的發起投票消息時,會投票給最早收到本輪投票消息的那個哨兵。或者本身自己已經判斷主庫客觀下線,也發起了選舉投票,那麼他已經投票給本身了,就不能再投票給別的哨兵了。

若是在一輪投票中,沒有誕生 Leader,哨兵集羣會等待一段時間以後,從新進行選舉。若是誕生了 Leader,就由 Leader 哨兵來執行主從切換。

須要注意:若是哨兵集羣只有 2 個實例,一個哨兵想要成爲 Leader,必須得到 2 票同意,此時的哨兵集羣沒法選舉出 Leader,沒法執行主從切換。因此一般至少要配置 3 個哨兵。

✔️ 切片集羣

須要存儲的數據量特別大的時候怎麼辦?什麼是切片集羣?官方提供的 Redis Cluster 是如何實現的?

比較容易想到的方案是:縱向擴展。也就是說升級單個 Redis 實例的資源配置,如增長內存容量、磁盤容量,使用更高配置的 CPU。但這個方案面臨兩個難點:一是會收到硬件和成本的限制。內存越高,升級時成本越大。二是以前提到的,RDB 持久化時,fork 子進程的操做的耗時,是和數據量成正比的,特別大的數據量會致使 fork 時阻塞主線程的時間變長。但縱向擴展的優勢也很明顯,那就是實現簡單直接。

那麼有沒有更好的解決方案呢?有,那就是橫向擴展,也就是 Redis 切片集羣:橫向增長 Redis 實例的數量,將數據均勻存放在多個實例上。那麼就帶來了兩個問題:數據切片後,在多個實例之間如何分佈?客戶端如何找到想要的數據在哪一個實例上?

Redis 3.0 開始提供了 Redis Cluster 方案實現切片集羣。採用哈希槽來處理數據與實例的映射關係。一個切片集羣一共有 16384 個哈希槽,每一個鍵值對都會根據它的 key 映射到一個哈希槽中:首先根據 key,按照 CRC16 算法計算獲得一個 16bit 的值,而後再用這個值對 16384 取模,獲得的就是在哈希槽中的位置。當咱們使用 cluster create 命令建立集羣時,Redis 會自動將 16384 個槽位平均分配到全部實例上。固然也能夠手動指定每一個實例上的槽位的數量。(注意手動分配時,必須將 16384 個槽位所有分配完,不然集羣沒法正常工做)。

那麼客戶端在訪問集羣時,計算出了須要的數據在哪一個哈希槽以後,如何肯定哈希槽在哪一個實例上呢?Redis 實例會把本身的哈希槽信息發給和他相鏈接的其餘實例,完成哈希槽分配信息的擴散,集羣中的每一個實例都會有全部哈希槽和實例的映射關係了。客戶端會將收到的映射信息緩存在本地,當請求時,就能夠根據哈希值找到哈希槽再找到實例了。

可是集羣中的實例是會有新增和刪除的,當新增或刪除發生時,爲了負載均衡,Redis 會將哈希槽在全部實例上從新分配一遍。這樣哈希槽的映射關係就改變了,這時客戶端又該怎麼辦呢?Redis Cluster 方案提供了一種重定向機制。若是客戶端向一個實例發送了讀寫請求,可是該實例上並無這個鍵值對對應的哈希槽,他會返回 MOVED 命令相應結果,其中包含了正確的實例的訪問地址:

GET hello:key
(error) MOVED 13320 172.16.19.5:6379
複製代碼

還有另外一種狀況,由於一個哈希槽中會分佈多個鍵值對,那麼就會有訪問的哈希槽正在遷移中,一個部分已經到了新的實例上,一部分還在原實例上。這時若是客戶端發送讀寫請求,若是舊實例上有要找的數據,那就正常執行指令。若是沒有,會返回 ASK 命令給客戶端,其中也攜帶了新的實例的地址。客戶端收到後會向新實例發送 ASKING 命令請求。也就是說 ASK 命令表示槽位正在遷移中,我這裏沒有說明可能已經遷移到新的地址了,你去新的地址找找看。不會想 MOVED 命令更新客戶端緩存的哈希槽映射信息,這樣就避免了遷移中的數據找不到的狀況發生。

爲何 Redis Cluster 要採用「 key —> 哈希槽 —> 實例 」的方式?而不是直接儲存 key 和實例之間的映射關係?

主要有如下幾點緣由:

  1. 整個集羣的 key 的總量是沒法估量的,若是直接記錄 key 和實例的映射關係,當 key 特別多時,這個映射表會很是龐大,不管存儲在服務端仍是客戶端都要佔用大量的存儲空間。而 Redis Cluster 方案中的哈希槽的總數是固定的,不會過分膨脹。
  2. Redis Cluster 採用的是無中心化的模式,客戶端向某個節點訪問一個 key,若是這個節點沒有這個 key,須要有幫客戶端糾正錯誤,路由到正確節點上的能力(MOVED/ASK)。這就須要每一個節點都擁有完整的哈希槽映射關係,節點之間須要交換這些信息。若是存儲的是 key 和實例的映射,節點之間交換信息的量會很是大,大量消耗網絡資源。
  3. 當集羣中實例增長/減小,以及均衡數據的時候,節點之間要發生數據遷移,這會須要修改每一個 key 與節點的映射關係,維護成本很是高。
  4. 在 key 與實例之間增長一箇中間層哈希槽,至關於將數據和節點解耦。key 經過哈希計算,只關係對應哪一個哈希槽。只消耗了不多的 CPU 資源,讓數據分配的更均勻,並且還讓映射關係的存儲佔用空間變得很小,有利於客戶端和服務端的存儲,節點交換信息也更加輕量。
  5. 當集羣實例增長減小,數據均衡時,只須要以哈希槽爲單位進行操做,簡化了集羣維護和管理的難度。

補充一點:哈希槽其實本質上就是一致性哈希 —— 有 16384 個槽位的哈希環。但相比直接的一致性哈希,哈希槽的方式多了一箇中間層,也就是槽位,達到了解耦的目的,更方便數據遷移,下降維護難度。

相關文章
相關標籤/搜索