帶你走進 Redis

張哥說技術發表於2022-12-30

帶你走進 Redis


作者:bobobliu,騰訊 CSIG 後臺開發工程師

本文主要講述 Redis 的基礎知識和常識性內容,幫助大家瞭解和熟悉 Redis;後續透過閱讀原始碼、實踐 Redis 後會總結相關的知識點,再繼續分享給大家。

一、什麼是 Redis

Redis 是一個開源、基於記憶體、使用 C 語言編寫的 key-value 資料庫,並提供了多種語言的 API。它的資料結構十分豐富,基礎資料型別包括:string(字串)、list(列表,雙向連結串列)、hash(雜湊,鍵值對集合)、set(集合,不重複)和 sorted set(有序集合)。主要可以用於資料庫、快取、分散式鎖、訊息佇列等...

以上的資料型別是 Redis 鍵值的資料型別,其實就是資料的儲存形式,但是資料型別的底層實現是最重要的,底層的資料結構主要分為 6 種,分別是簡單動態字串雙向連結串列壓縮連結串列雜湊表跳錶整數陣列。各個資料型別和底層結構的對應關係如下:

資料型別和底層結構的對應關係

stringlisthashsetsorted set
簡單動態字串雙向連結串列、壓縮連結串列壓縮連結串列、雜湊表壓縮連結串列、整數陣列壓縮連結串列、跳錶

底層實現的時間複雜度

跳錶雙向連結串列壓縮連結串列雜湊表整數陣列
O(logN)O(N)O(N)O(1)O(N)

可以看出除了 string 型別的底層實現只有一種資料結構,其他四種均有兩種底層實現,這四種型別為集合型別,其中一個鍵對應了一個集合的資料;

1.1 Redis 鍵值是如何儲存的呢?

Redis 為了快速訪問鍵值對,採用了雜湊表來儲存所有的鍵值對,一個雜湊表對應了多個雜湊桶,所謂的雜湊桶是指雜湊表陣列中的每一個元素,當然雜湊表中儲存的不是值本身,是指向值的指標,如下圖。

其中雜湊桶中的 entry 元素中儲存了key 和value 指標,分別指向了實際的鍵和值。透過 Redis 可以在 O(1)的時間內找到鍵值對,只需要計算 key 的雜湊值就可以定位位置,但從下圖可以看出,在 4 號位置出現了衝突,兩個 key 對映到了同一個位置,這就產生了雜湊衝突,會導致雜湊表的操作變慢。雖然 Redis 透過鏈式衝突解決該問題,但如果資料持續增多,產生的雜湊衝突也會越來越多,會加重 Redis 的查詢時間;

帶你走進 RedisRedis 儲存資料示意圖

為了解決上述的雜湊衝突問題,Redis 會對雜湊表進行rehash操作,也就是增加目前的雜湊桶數量,使得 key 更加分散,進而減少雜湊衝突的問題,主要流程如下:

  1. 採用兩個 hash 表進行操作,當雜湊表 A 需要進行擴容時,給雜湊表 B 分配兩倍的空間;
  2. 將雜湊表 A 的資料重新對映並複製給雜湊表 B;
  3. 釋放 A 的空間。

上述的步驟可能會存在一個問題,當雜湊表 A 向 B 複製的時候,是需要一定的時間的,可能會造成 Redis 的執行緒阻塞,就無法服務其他的請求了。

針對上述問題,Redis 採用了漸進式 rehash,主要的流程是:Redis 還是繼續處理客戶端的請求,每次處理一個請求的時候,就會將該位置所有的 entry 都複製到雜湊表 B 中,當然也會存在某個位置一直沒有被請求。Redis 也考慮了這個問題,透過設定一個定時任務進行 rehash,在一些鍵值對一直沒有操作的時候,會週期性的搬移一些資料到雜湊表 B 中,進而縮短 rehash 的過程。

1.2 Redis 為什麼採用單執行緒呢?

首先要明確的是 Redis 單執行緒指的是網路 IO鍵值對讀寫是由一個執行緒來完成的,但 Redis 持久化、叢集資料等是由額外的執行緒執行的。瞭解 Redis 使用單執行緒之前可以先了解一下多執行緒的開銷。

通常情況下,使用多執行緒可以增加系統吞吐率或者可以增加系統擴充套件性,但多執行緒通常會存在同時訪問某些共享資源,為了保證訪問共享資源的正確性,就需要有額外的機制進行保證,這個機制首先會帶來一定的開銷。其實對於多執行緒併發訪問的控制一直是一個難點問題,如果沒有精細的設計,比如說,只是簡單地採用一個粗粒度互斥鎖,就會出現不理想的結果。即使增加了執行緒,大部分執行緒也在等待獲取訪問共享資源的互斥鎖,並行變序列,系統吞吐率並沒有隨著執行緒的增加而增加。

這也是 Redis 使用單執行緒的主要原因。

值得注意的是在 Redis6.0 中引入了多執行緒。在 Redis6.0 之前,從網路 IO 處理到實際的讀寫命令處理都是由單個執行緒完成的,但隨著網路硬體的效能提升,Redis 的效能瓶頸有可能會出現在網路 IO 的處理上,也就是說單個主執行緒處理網路請求的速度跟不上底層網路硬體的速度。針對此問題,Redis 採用多個 IO 執行緒來處理網路請求,提高網路請求處理的並行度,但多 IO 執行緒只用於處理網路請求,對於讀寫命令,Redis 仍然使用單執行緒處理!

1.3 Redis 單執行緒為什麼還這麼快?

IO 多路複用機制:使其在網路 IO 操作中能併發處理大量的客戶端請求從而實現高吞吐率

IO 多路複用機制是指一個執行緒處理多個 IO 流,也就是常說的 select/epoll 機制。在 Redis 執行單執行緒的情況下,該機制允許核心中同時存在多個監聽套接字和已連線套接字。核心會一直監聽這些套接字上的連線請求或資料請求。一旦有請求到達,就會交給 Redis 執行緒處理,這就實現了一個 Redis 執行緒處理多個 IO 流的效果,進而提升併發性。

Redis 是基於記憶體的,絕大部分請求都是記憶體操作,十分的迅速;

Redis 具有高效的底層資料結構,為最佳化記憶體,對每種型別基本都有兩種底層實現方式;

主要執行過程是單執行緒,避免了不必要的上下文切換和資源競爭,不存在多執行緒導致的 CPU 切換和鎖的問題;


二、Redis 資料丟失問題

由上一小節我們大概瞭解了 Redis 的儲存和快的主要原因,通常情況下我們會把 Redis 當作快取使用,將後端資料庫中的資料儲存在記憶體中,然後從記憶體中直接讀取資料,響應速度會非常快。但是如果伺服器當機了,記憶體中的資料也就會丟失,當然我們可以重新從後端資料庫中恢復這些快取資料,但是頻繁訪問資料庫,會給資料庫帶來一定的壓力;另一方面資料是從慢速的資料庫中讀取的,效能肯定比不上 Redis,也會導致這些資料的應用程式響應變慢。

所以對 Redis 來說,實現資料的持久化,避免從後端恢復資料是至關重要的,目前 Redis 持久化主要有兩大機制,分別是AOF(Append Only File)日誌RDB 快照

2.1 AOF 日誌

AOF 日誌是寫後日志,也就是 Redis 先執行命令,然後將資料寫入記憶體,最後才記錄日誌,如下圖:

帶你走進 Redis

AOF 日誌中記錄的是 Redis 收到的每一條命令,這些命令都是以文字的形式儲存的,例如我們以 Redis 收到 set key value 命令後記錄的日誌為例,AOF 檔案中儲存的資料如下圖所示,其中*3 代表當前命令分為三部分,每部分都是透過$+數字開頭,其中數字表示該部分的命令、鍵、值一共有多少位元組。

帶你走進 Redis

AOF 為了避免額外的檢查開銷,並不會檢查命令的正確性,如果先記錄日誌再執行命令,就有可能記錄錯誤的命令,再透過 AOF 日誌恢復資料的時候,就有可能出錯,而且在執行完命令後記錄日誌也不會阻塞當前的寫操作。但是 AOF 是存在一定的風險的,首先是如果剛執行一個命令,但是 AOF 檔案中還沒來得及儲存就當機了,那麼這個命令和資料就會有丟失的風險,另外 AOF 雖然可以避免對當前命令的阻塞(因為是先寫入再記錄日誌),但有可能會對下一次操作帶來阻塞風險(可能存在寫入磁碟較慢的情況)。這兩種情況都在於 AOF 什麼時候寫入磁碟,對於這個問題 AOF 機制提供了三種選擇(appendfsync 的三個可選值),分別是Always、Everysec、No具體如下:

AOF 寫入磁碟的機制

Always同步寫回:每個命令執行完立馬寫入磁碟
Everysec每秒寫回:每個命令執行完,先把日誌寫入 AOF 檔案的緩衝區,每隔一秒把緩衝區的內容寫入磁碟
No作業系統的寫回:每個命令執行完,先把日誌寫入 AOF 檔案的緩衝區,由作業系統決定何時把緩衝區的內容寫入磁碟

我們可以根據不同的場景來選擇不同的方式:

  1. Always 可靠性較高,資料基本不丟失,但是對效能的影響較大;
  2. Everysec 效能適中,即使當機也只會丟失 1 秒的資料;
  3. No 效能好,但是如果當機丟失的資料較多。

雖然有一定的寫回策略,但畢竟 AOF 是透過檔案的形式記錄所有的寫命令,但如果指令越來越多的時候,AOF 檔案就會越來越大,可能會超出檔案大小的限制;另外,如果檔案過大再次寫入指令的話效率也會變低;如果發生當機,需要把 AOF 所有的命令重新執行,以用於故障恢復,資料過大的話這個恢復過程越漫長,也會影響 Redis 的使用。

此時,AOF 重寫機制就來了:

AOF 重寫就是根據所有的鍵值對建立一個新的 AOF 檔案,可以減少大量的檔案空間,減少的原因是:AOF 對於命令的新增是追加的方式,逐一記錄命令,但有可能存在某個鍵值被反覆更改,產生了一些冗餘資料,這樣在重寫的時候就可以過濾掉這些指令,從而更新當前的最新狀態。

AOF 重寫的過程是透過主執行緒 fork 後臺的 bgrewriteaof 子程式來實現的,可以避免阻塞主程式導致效能下降,整個過程如下:

  • AOF 每次重寫,fork 過程會把主執行緒的記憶體複製一份 bgrewriteaof 子程式,裡面包含了資料庫的資料,複製的是父程式的頁表,可以在不影響主程式的情況下逐一把複製的資料記入重寫日誌;
  • 因為主執行緒沒有阻塞,仍然可以處理新來的操作,如果這時候存在寫操作,會先把操作先放入緩衝區,對於正在使用的日誌,如果當機了這個日誌也是齊全的,可以用於恢復;對於正在更新的日誌,也不會丟失新的操作,等到資料複製完成,就可以將緩衝區的資料寫入到新的檔案中,保證資料庫的最新狀態。

2.2 RDB 快照

上一小節裡瞭解了避免 Redis 資料丟失的 AOF 方法,這個方法記錄的是操作命令,而不是實際的資料,如果日誌非常多的話,Redis 恢復的就很緩慢,會影響到正常的使用。

這一小節主要是講述的另一種 Redis 資料持久化的方式:記憶體快照。即記錄記憶體中的資料在某一時刻的狀態,並以檔案的形式寫到磁碟上,即使伺服器當機,快照檔案也不會丟失,資料的可靠性也就得到了保證,這個檔案稱為 RDB(Redis DataBase)檔案。可以看出 RDB 記錄的是某一時刻的資料,和 AOF 不同,所以在資料恢復的時候只需要將 RDB 檔案讀入到記憶體,就可以完成資料恢復。但為了 RDB 資料恢復的可靠性,在進行快照的時候是全量快照,會將記憶體中所有的資料都記錄到磁碟中,這就有可能會阻塞主執行緒的執行。Redis 提供了兩個命令來生成 RDB 檔案,分別是savebgsave

  • save:在主執行緒中執行,會導致阻塞;
  • bgsave:會建立一個子程式,該程式專門用於寫入 RDB 檔案,可以避免主執行緒的阻塞,也是預設的方式。

我們可以採用 bgsave 的命令來執行全量快照,提供了資料的可靠性保證,也避免了對 Redis 的效能影響。執行快照期間資料能不能修改呢?如果不能修改,快照過程中如果有新的寫操作,資料就會不一致,這肯定是不符合預期的。Redis 借用了作業系統的寫時複製,在執行快照的期間,正常處理寫操作。

主要流程為:

  • bgsave 子程式是由主執行緒 fork 出來的,可以共享主執行緒的所有記憶體資料;
  • bgsave 子程式執行後,開始讀取主執行緒的記憶體資料,並把它們寫入 RDB 檔案中;
  • 如果主執行緒對這些資料都是讀操作,例如 A,那麼主執行緒和 bgsave 子程式互不影響;
  • 如果主執行緒需要修改一塊資料,如 C,這塊資料會被複制一份,生成資料的副本,然主執行緒在這個副本上進行修改;bgsave 子程式可以把原來的資料 C 寫入 RDB 檔案;
帶你走進 Redis

透過上述方法就可以保證快照的完整性,也可以允許主執行緒處理寫操作,可以避免對業務的影響。那多久進行一次快照呢

理論上來說快照時間間隔越短越好,可以減少資料的丟失,畢竟 fork 的子程式不會阻塞主執行緒,但是頻繁的將資料寫入磁碟,會給磁碟帶來很多壓力,也可能會存在多個快照競爭磁碟頻寬(當前快照沒結束,下一個就開始了)。另一方面,雖然 fork 出的子程式不會阻塞,但 fork 這個建立過程是會阻塞主執行緒的,當主執行緒需要的記憶體越大,阻塞時間越長;

針對上面的問題,Redis 採用了增量快照,在做一次全量快照後,後續的快照只對修改的資料進行記錄,需要記住哪些資料被修改了,可以避免全量快照帶來的開銷。

2.3 混合使用 AOF 日誌和 RDB 快照

雖然跟 AOF 相比,RDB 快照的恢復速度快,但快照的頻率不好把握,如果頻率太低,兩次快照間一旦當機,就可能有比較多的資料丟失。如果頻率太高,又會產生額外開銷,那麼,還有什麼方法既能利用 RDB 的快速恢復,又能以較小的開銷做到儘量少丟資料呢?

在 Redis4.0 提出了混合使用 AOF 和 RDB 快照的方法,也就是兩次 RDB 快照期間的所有命令操作由 AOF 日誌檔案進行記錄。這樣的好處是 RDB 快照不需要很頻繁的執行,可以避免頻繁 fork 對主執行緒的影響,而且 AOF 日誌也只記錄兩次快照期間的操作,不用記錄所有操作,也不會出現檔案過大的情況,避免了重寫開銷。

透過上述方法既可以享受 RDB 快速恢復的好處,也可以享受 AOF 記錄簡單命令的優勢。

對於 AOF 和 RDB 的選擇問題:

  • 資料不能丟失時,記憶體快照和 AOF 的混合使用是一個很好的選擇;
  • 如果允許分鐘級別的資料丟失,可以只使用 RDB;
  • 如果只用 AOF,優先使用 everysec 的配置選項,因為它在可靠性和效能之間取了一個平衡。

三、Redis 資料同步

當 Redis 發生當機的時候,可以透過 AOF 和 RDB 檔案的方式恢復資料,從而保證資料的丟失從而提高穩定性。但如果 Redis 例項當機了,在恢復期間就無法服務新來的資料請求;AOF 和 RDB 雖然可以保證資料儘量的少丟失,但無法保證服務儘量少中斷,這就會影響業務的使用,不能保證 Redis 的高可靠性。

Redis 其實採用了主從庫的模式,以保證資料副本的一致性,主從庫採用讀寫分離的方式:從庫和主庫都可以接受讀操作;對於寫操作,首先要到主庫執行,然後主庫再將寫操作同步到從庫;

只有主庫接收寫操作可以避免客戶端將資料修改到不同的 Redis 例項上,其他客戶端進行讀取時可能就會讀取到舊的值;當然,如果非要所有的庫都可以進行寫操作,就要涉及到鎖、例項間協商是否完成修改等一系列操作,會帶來額外的開銷;

3.1 主從庫如何進行第一次資料同步

當存在多個 Redis 例項的時候,可以透過 replicaof 命令形成主庫和從庫的關係,在從庫中輸入:replicaof 主庫 ip 6379 就可以在主庫中複製資料,具體有三個階段:

  • 首先是主從庫建立連線、協商同步的過程,具體的從庫向主庫傳送 psync 命令,代表要進行資料同步;psync 中包含了主庫的 runID(Redis 啟動時生成的隨機 ID,初始值為:?)和複製進度 offset(設為-1,代表第一次複製)兩個引數,主庫接收到 psync 命令會,會用 FULLRESYNC 命令返回給從庫,包含兩個引數:主庫 runID 和複製進度 offset;其中 FULLRESYNC 代表的全量複製,會將主庫所有的資料都複製給從庫;
  • 待從庫接收到資料後,在本地完成資料載入,具體的主庫執行 bgsave 命令,生成 RDB 檔案,然後將檔案發給從庫,從庫接收到 RDB 檔案後,首先清空當前資料,然後再載入 RDB 檔案;這個過程主庫不會被阻塞,仍然可以接受請求,如果存在寫操作,剛剛生成的 RDB 檔案中是不包含這些新資料的,此時主庫會在記憶體中用專門的 replication buffer 記錄 RDB 檔案生成後所有的寫操作;
  • 最後,主庫會把 replication buffer 中的修改操作發給從庫,從庫重新執行這些操作,就可以實現主從庫同步了。

如果從庫的例項過多,對於主庫來說有一定的壓力,主庫會頻繁 fork 子程式以生成 RDB 檔案,fork 這個操作會阻塞主執行緒處理正常請求,導致響應變慢,Redis 採用了主-從-從的模式,可以手動選擇一個從庫,用來同步其他從庫的資料,以減少主庫生成 RDB 檔案和傳輸 RDB 檔案的壓力;如下圖:

帶你走進 Redis

這樣從庫就可以知道在進行資料同步的時候,不需要和主庫直接互動,只需要和選擇的從庫進行寫操作同步就可以了,從而減少主庫的壓力。

3.2 主庫如果掛了呢?

Redis 採用主從庫的模式保證資料副本的一致性,在這個模式下如果從庫發生故障,客戶端可以向其他主庫或者從庫傳送請求,但如果主庫掛了,客戶端就沒法進行寫操作了,也無法對從庫進行相應的資料複製操作;

不管是寫服務中斷還是從庫無法進行資料同步,都是不能接受的,所以當主庫掛了以後,需要一個新的主庫來代替掛掉的主庫,這樣就就會產生三個問題:

  1. 怎麼判斷主庫是真的掛了,而不是網路異常?
  2. 主庫如果掛了,該選擇哪個從庫作為新的主庫?
  3. 怎麼把新主庫的相關資訊通知給從庫和客戶端?

Redis 採用了哨兵機制應對這些問題,哨兵機制是實現主從庫自動切換的關鍵機制,在主從庫執行的同時,它也在進行監控、選擇主庫和通知的操作;

  • 監控。哨兵在執行時,週期性的給所有的主從庫傳送 PING 命令,檢測是否仍在執行。如果從庫沒有響應哨兵的 PING 命令,哨兵就會將它標記為下線狀態;如果主庫沒有在規定時間內響應哨兵的 PING 命令,哨兵也會判斷主庫下限,然後開始自動切換主庫的流程。
  • 選主。主庫掛了之後,哨兵需要按照一定的規則選擇一個從庫,並將他作為新的主庫。
  • 通知。選取了新的主庫後,哨兵會把新主庫的連線資訊發給其他從庫,讓它們執行 replicaof 命令和新主庫建立連線,並進行資料複製;同時哨兵也會將新主庫的訊息發給客戶端;

下圖展示了哨兵的幾個操作的任務:

帶你走進 Redis

但這樣也會存在一個問題,哨兵判斷主從庫是否下線如果出現失誤呢?

對於從庫,下線影響不大,叢集的對外服務也不會間斷。但是如果哨兵誤判主庫下線,可能是因為網路擁塞或者主庫壓力大的情況,這時候也就需要進行選主並讓從庫和新的主庫進行資料同步,這個過程是有一定的開銷的,所以我們要儘可能的避免誤判的情況。哨兵機制也考慮了這一點,它通常採用多例項組成的叢集模式進行部署,也被稱為哨兵叢集;透過引入多個哨兵例項一起判斷,就可以儘可能的避免單個哨兵產生的誤判問題。這時候判斷主庫是否下線不是由一個哨兵決定的,只有大多數哨兵認為該主庫下線,主庫才會標記為“客觀下線”。

簡單的來說”客觀下線“的標準是當 N 個哨兵例項,有 N/2 + 1 個例項認為該主庫為下線狀態,該主庫就會被認定為“客觀下線”。這樣就可以儘量的避免單個哨兵產生的誤判問題(N/2 + 1 這個值也可以透過引數改變);

如果判斷了主庫為主觀下線,怎麼選取新的主庫呢?

上面有說道,這一部分也是由哨兵機制來完成的,選取主庫的過程分為“篩選 和 打分”。主要是按照一定的規則過濾掉不符合的從庫,再按照一定的規則給其餘的從庫打分,將最高分的從庫作為新的主庫。

  • 篩選。首先從庫一定是正在執行的,還要判斷從庫之前的網路連線狀態,如果總是斷連並且超過了一定的閾值,哨兵會認為該從庫的網路不好,也會將其篩掉。

  • 打分。哨兵機制根據三個規則依次進行打分:從庫優先順序、從庫複製進度以及從庫 ID 號。在某一輪有從庫得分最高,那麼它就是新的主庫了,選主過程結束。如果該輪沒有出現最高的,繼續下一輪。

  1. 優先順序最高的從庫。使用者可以透過 slave-priority 配置項,給不同的從庫設定優先順序。選主庫的時候哨兵會給優先順序高的從庫打高分,如果一個從庫優先順序高,那麼就是新主庫;
  2. 從庫複製進度最接近。主庫的 slave_repl_offset 和從庫 master_repl_offset 越接近,得分越高;
  3. ID 小的從庫得分高。如果上面兩輪也沒有選出新主庫,就會根據從庫例項的 ID 來判斷,ID 越小的從庫得分越高。

由此哨兵可以選擇出一個新的主庫。

由哪個哨兵來執行主從庫切換呢?

這個過程和判斷主庫“客觀下線”類似,也是一個投票的過程。如果某個哨兵判斷了主庫為下線狀態,就會給其他的哨兵例項傳送is-master-down-by-addr的命令,其他例項會根據自己和主庫的連線狀態作出 Y 或 N 的響應,Y 相當於贊成票,N 為反對票。一個哨兵獲得一定的票數後,就可以標記主庫為“客觀下線”,這個票數是由引數 quorum 設定的。如下圖:

帶你走進 Redis

例如:現在有 3 個哨兵,quorum 配置的是 2,那麼,一個哨兵需要 2 張贊成票,就可以標記主庫為“客觀下線”了。這 2 張贊成票包括哨兵自己的一張贊成票和另外兩個哨兵的贊成票。

這個時候哨兵就可以給其他哨兵傳送訊息,表示希望自己來執行主從切換,並讓所有的哨兵進行投票,這個過程稱為“Leader 選舉”,進行主從切換的哨兵稱為 Leader。任何一個想成為 Leader 的哨兵都需要滿足兩個條件:

  • 拿到半數以上的哨兵贊成票;
  • 拿到的票數需要大於等於 quorum 的值。

以上就可以選出 Leader 然後進行主從庫切換了。


四、Redis 叢集

資料量過多如何處理?

當資料量過多的情況下,一種簡單的方式是升級 Redis 例項的資源配置,包括增加記憶體容量、磁碟容量、更好配置的 CPU 等,但這種情況下 Redis 使用 RDB 進行持久化的時候響應會變慢,Redis 透過 fork 子程式來完成資料持久化,但 fork 在執行時會阻塞主執行緒,資料量越大,fork 的阻塞時間就越長,從而導致 Redis 響應變慢。

Redis 的切片叢集可以解決這個問題,也就是啟動多個 Redis 例項來組成一個叢集,再按照一定的規則把資料劃分為多份,每一份用一個例項來儲存,這樣客戶端只需要訪問對應的例項就可以獲取資料。在這種情況下 fork 子程式一般不會給主執行緒帶來較長時間的阻塞,如下圖:

帶你走進 Redis

將 20GB 的資料分為 4 分,每份包含 5GB 資料,客戶端只需要找到對應的例項就可以獲取資料,從而減少主執行緒阻塞的時間。

當資料量過多的時候,可以透過升級 Redis 例項的資源配置或者透過切片叢集的方式。前者實現起來簡單粗暴,但這資料量增加的時候,需要的記憶體也在不斷增加,主執行緒 fork 子程式就有可能會阻塞,而且該方案受到硬體和成本的限制。相比之下第二種方案是一種擴充套件性更好的方案,如果想儲存更多的資料,僅需要增加 Redis 例項的個數,不用擔心單個例項的硬體和成本限制。在面向百萬、千萬級別的使用者規模時,橫向擴充套件的 Redis 切片叢集會是一個非常好的選擇。

選擇切片叢集也是需要解決一些問題的:

  • 資料切片後,在多個例項之間怎麼分佈?
  • 客戶端怎麼確定想要訪問的例項是哪一個?

Redis 採用了 Redis Cluster 的方案來實現切片叢集,具體的 Redis Cluster 採用了雜湊槽(Hash Slot)來處理資料和例項之間的對映關係。在 Redis Cluster 中,一個切片叢集共有 16384 個雜湊槽(為什麼 Hash Slot 的個數是 16384),這些雜湊槽類似於資料的分割槽,每個鍵值對都會根據自己的 key 被影射到一個雜湊槽中,對映步驟如下:

  • 根據鍵值對 key,按照 CRC16 演算法計算一個 16bit 的值;
  • 用計算的值對 16384 取模,得到 0 ~ 16383 範圍內的模數,每個模數對應一個雜湊槽。

這時候可以得到一個 key 對應的雜湊槽了,雜湊槽又是如何找到對應的例項的呢?

在部署 Redis Cluster 的時候,可以透過 cluster create 命令建立叢集,此時 Redis 會自動把這些槽分佈在叢集例項上,例如一共有 N 個例項,那麼每個例項包含的槽個數就為 16384/N。當然可能存在 Redis 例項中記憶體大小配置不一的問題,記憶體大的例項具有更大的容量。這種情況下可以透過 cluster addslots 命令手動分配雜湊槽。

redis-cli -h 33.33.33.3 –p 6379 cluster addslots 0,1
redis-cli -h 33.33.33.4 –p 6379 cluster addslots 2,3
redis-cli -h 33.33.33.5 –p 6379 cluster addslots 4

要注意的是,如果採用 cluster addslots 的方式手動分配雜湊槽,需要將 16384 個槽全部分配完,否則 Redis 叢集無法正常工作。現在透過雜湊槽,切片叢集就實現了資料到雜湊槽、雜湊槽到例項的對應關係,那麼客戶端如何確定需要訪問的例項是哪一個呢?

客戶端定位叢集中的資料

客戶端請求的 key 可以透過 CRC16 演算法計算得到,但客戶端還需要知道雜湊槽分佈在哪個例項上。在最開始客戶端和叢集例項建立連線後,例項就會把雜湊槽的分配資訊發給客戶端,例項之間會把自己的雜湊槽資訊發給和它相連的例項,完成雜湊槽的擴散。這樣客戶端訪問任何一個例項的時候,都能獲取所有的雜湊槽資訊。當客戶端收到雜湊槽的資訊後會把雜湊槽對應的資訊快取在本地,當客戶端傳送請求的時候,會先找到 key 對應的雜湊槽,然後就可以給對應的例項傳送請求了。

但是,雜湊槽和例項的對應關係不是一成不變的,可能會存在新增或者刪除的情況,這時候就需要重新分配雜湊槽;也可能為了負載均衡,Redis 需要把所有的例項重新分佈。

雖然例項之間可以互相傳遞訊息以獲取最新的雜湊槽分配資訊,但是客戶端無法感知這個變化,就會導致客戶端訪問的例項可能不是自己所需要的了。

Redis Cluster 提供了重定向的機制,當客戶端給例項傳送資料讀寫操作的時候,如果這個例項上沒有找到對應的資料,此時這個例項就會給客戶端返回 MOVED 命令的相應結果,這個結果中包含了新例項的訪問地址,此時客戶端需要再給新例項傳送操作命令以進行讀寫操作,MOVED 命令如下:

GET hello:key
(error) MOVED 3333 33.33.33.33:6379

返回的資訊代表客戶端請求的 key 所在的雜湊槽為 3333,實際是在 33.33.33.33 這個例項上,此時客戶端只需要向 33.33.33.33 這個例項傳送請求就可以了。

此時也存在一個小問題,雜湊槽中對應的資料過多,導致還沒有遷移到其他例項,此時客戶端就發起了請求,在這種情況下,客戶端就對例項發起了請求,如果資料還在對應的例項中,會給客戶端返回資料;如果請求的資料已經被轉移到其他例項上,客戶端就會收到例項返回的 ASK 命令,該命令表示:雜湊槽中資料還在前一種、ASK 命令把客戶端需要訪問的新例項返回了。此時客戶端需要給新例項傳送 ASKING 命令以進行請求操作;

值得注意的是 ASK 資訊和 MOVED 資訊不一樣,ASK 資訊並不會更新客戶端本地的快取的雜湊槽分配資訊,也就是說如果客戶端再次訪問該雜湊槽還是會請求之前的例項,直到資料遷移完成。

以上就是 Redis 基礎篇的全部內容~

參考

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2930290/,如需轉載,請註明出處,否則將追究法律責任。

相關文章