Redis系列(四):Redis的複製機制(主從複製)

本篇博客是Redis系列的第4篇,主要講解下Redis的主從複製機制。面試

本系列的前3篇能夠點擊如下連接查看:redis

Redis系列(一):Redis簡介及環境安裝shell

Redis系列(二):Redis的5種數據結構及其經常使用命令數據庫

Redis系列(三):Redis的持久化機制(RDB、AOF)服務器

Redis的主從複製是面試中常常會被問的,我最近面試的幾家公司只要聊到Redis,都會問我主從複製的原理。網絡

1. 爲何須要主從複製?

在本系列的上一篇博客中,咱們講到了Redis的持久化機制,它很好的解決了單臺Redis服務器因爲意外狀況致使Redis服務器進程退出或者Redis服務器宕機而形成的數據丟失問題。數據結構

但持久化機制還原數據有個前提:你的Redis服務器得能正常啓動。併發

若是遇到極端的斷電狀況(雖然機率小,可是有可能),Redis服務器啓都啓動不了,怎麼還原數據?怎麼保證它的高可用。post

就算Redis服務器能啓動了,網絡鏈接也有崩掉的可能,我不信你沒看到過電纜被挖斷致使的某些服務不可用的新聞。性能

正是因爲有這樣的風險,因此生產環境Redis服務器不可能使用單臺的,那既然使用多臺Redis服務器,多臺Redis服務器之間的數據如何同步呢?

這就須要用到Redis的複製機制。

還有個緣由就是,雖然Redis的性能很好,但單臺畢竟仍是有瓶頸的,使用主從複製能夠實現讀寫分離,提升Redis的高可用性,即主服務器用來執行寫命令,多個從服務器用來執行讀命令,相似於數據庫的讀寫分離。

綜上所述,主從複製主要有如下2個使用場景:

  1. 數據備份
  2. 讀寫分離

2. 主從複製實踐

首先,我在本機開啓2個Redis實例(也能夠搞2臺Redis服務器),分別爲127.0.0.1:637九、127.0.0.1:6380。

而後,使用redis-cli鏈接Redis實例127.0.0.1:6380並執行以下命令:

SLAVEOF 127.0.0.1 6379
複製代碼

此時,咱們稱127.0.0.1:6379爲127.0.0.1:6380的主服務器(master),稱127.0.0.1:6380爲127.0.0.1:6379的從服務器(slave)

2者之間的關係以下所示:

而後,咱們在主服務器上執行以下寫命令:

SET msg "hello world"
複製代碼

此時,咱們不只能在主服務器上獲取到該值,也能在從服務器上獲取到該值:

而後,咱們在主服務器上執行以下刪除命令:

DEL msg
複製代碼

此時,咱們會發現不只主服務器上的msg鍵被刪除,從服務器上的msg也被刪除:

因此說,進行復制中的主從服務器雙方的數據庫將保存相同的數據

值得注意的是,從服務器只能執行讀命令,執行寫命令時會報以下錯誤:

若是從服務器不想再複製主服務器,能夠執行命令:SLAVEOF no one

3. 舊版複製功能的實現(SYNC)

這裏的舊版指的是Redis 2.8之前的版本。

Redis的複製功能分爲如下2個操做:

  1. 同步:用於將從服務器的數據庫狀態更新至主服務器當前所處的數據庫狀態。
  2. 命令傳播:用於在主服務器的數據庫狀態被修改,致使主從服務器的數據庫狀態不一致時,讓主從服務器的數據庫狀態從新回到一致狀態。

3.1 同步

當客戶端向從服務器發送SLAVEOF命令,要求從服務器複製主服務器時,從服務器會向主服務器SYNC命令,該命令的執行步驟以下所示:

  1. 從服務器向主服務器發送SYNC命令。
  2. 主服務器收到SYNC命令後,執行BGSAVE命令,在後臺生成RDB文件,並使用一個緩衝區記錄從如今開始執行的全部寫命令。
  3. 當主服務器的BGSAVE命令執行完成,主服務器將生成的RDB文件發送給從服務器,從服務器接收並載入這個RDB文件,至此,從服務器的數據庫狀態和主服務器執行BGSAVE命令時的數據庫狀態一致。
  4. 主服務器將記錄在緩衝區裏面的全部寫命令發送給從服務器,從服務器接收並執行這些寫命令,至此,從服務器的數據庫狀態和主服務器當前的數據庫狀態一致。

SYNC命令執行期間,主從服務器的通訊過程以下圖所示:

3.2 命令傳播

同步操做執行完畢後,主從服務器的數據庫狀態達到一致狀態,當主服務器執行了客戶端發送的寫命令時,主服務器的數據庫就被修改了,致使主從服務器的數據庫狀態再也不一致。

爲了讓主從服務器的數據庫狀態再次回到一致狀態,主服務器須要對從服務器執行命令傳播操做:主服務器會將本身執行的寫命令,發送給從服務器執行,當從服務器執行了相同的寫命令後,主從服務器的數據庫狀態再次回到一致狀態。

舉個具體的例子,好比主從服務器剛開始都擁有k一、k二、k三、k四、k5這5個鍵,而後客戶端往主服務器發送了命令DEL k3,此時主服務器會執行該條命令,並將該條命令傳播給從服務器執行,從而使主從服務器的數據庫狀態保持一致。

整個變化過程以下所示:

4. 舊版複製功能的缺陷

這裏的舊版指的是Redis 2.8之前的版本。

在Redis 2.8之前,從服務器對主服務器的複製分爲如下2種狀況:

  1. 初次複製

    從服務器之前沒有複製過任何主服務器,或者從服務器當前要複製的主服務器和上一次複製的主服務器不一樣。

  2. 斷線後重複製

    處於命令傳播階段的主從服務器由於網絡緣由而中斷了複製,但從服務器經過重試又從新連上了主服務器,並繼續複製主服務器。

舊版複製功能能夠很好的完成初次複製,但完成斷線後重複製的效率卻很低。

舉個具體的例子,從服務器B一直在複製着主服務器A,剛開始都是正常的,主服務器A執行的寫命令也都經過命令

傳播的方式傳遞給了從服務器B執行,但忽然由於網絡緣由,主服務器A和從服務器B之間中斷了複製,在這期間,

假設主服務器又執行了10個寫命令,而後從服務器B經過重試又從新連上了主服務器A,繼續開始複製,那麼它是

怎麼複製的呢?

從服務器B會向主服務器A發送SYNC命令,主服務器A接收到命令後會執行BGSAVE命令,BGSAVE命令執行期間的

全部寫命令會被記錄到緩衝區,待BGSAVE命令執行完畢後,主服務器A會將生成的RDB文件發送給從服務器B,

從服務器B接收並載入這個RDB文件,而後主服務器A將緩衝區裏的寫命令發送給從服務器B執行,至此,主從

服務器的數據庫狀態又恢復一致,後續又進入命令傳播階段。

也就是說,每次斷線後重複製,都要執行一次SYNC命令來一次全量複製,但其實從服務器B須要的只是斷開鏈接期間主服務器A執行的寫命令,按上面的例子,也就是隻須要10個寫命令便可。

SYNC命令又是一個很是耗費資源的操做:

  1. 主服務器須要執行BGSAVE命令生成RDB文件,這會耗費主服務器大量的CPU、內存和磁盤IO資源。
  2. 主服務器須要將生成的RDB文件發送給從服務器,這會耗費主從服務器大量的網絡資源(帶寬和流量)。
  3. 接收到RDB文件的從服務器須要載入RDB文件,在載入期間,從服務器會阻塞,沒辦法處理命令請求。

5. 新版複製功能的實現(PSYNC)

這裏的新版指的是Redis 2.8以及以後的版本。

從Redis 2.8版本開始,Redis使用PSYNC命令代替SYNC命令來執行復制時的同步操做。

PSYNC命令有如下2種場景:

  1. 完整重同步

    完整重同步用於處理初次複製,執行步驟和SYNC命令的執行步驟基本同樣。

  2. 部分重同步

    部分重同步用於處理斷線後重複製,當從服務器在斷線後從新鏈接主服務器時,若是條件容許,主服務器能夠將主從服務器鏈接斷開期間執行的寫命發送給從服務器,從服務器只要接收並執行這些寫命令,就能夠將數據庫更新至主服務器當前所處的狀態。

仍然用上面舉的例子,新版複製,主服務器只須要把斷開期間執行的10個寫命令發送給從服務器便可,而不用生成併發送整個RDB文件,性能大大提高。

主從服務器在執行部分重同步時的通訊過程以下圖所示:

那麼部分重同步是如何實現的呢?

部分重同步功能由如下3個部分組成:

  1. 主服務器和從服務器的複製偏移量
  2. 主服務器的複製積壓緩衝區
  3. 服務器的運行ID

接下來咱們一一講解。

5.1 複製偏移量

執行復制的主服務器和從服務器會分別維護一個複製偏移量:

  1. 主服務器每次向從服務器傳播N個字節的數據時,就將本身的複製偏移量的值加上N。
  2. 從服務器每次收到主服務器傳播來的N個字節的數據時,就將本身的複製偏移量的值加上N。

舉個例子,假設主服務器有3個從服務器,它們的複製偏移量都爲10086,以下圖所示:

而後,主服務器向3個從服務器傳播了長度爲33字節的數據,那麼主服務器的複製偏移量會加上33,變爲10119,

從服務器A在這時恰好斷線了,沒有接收到數據,因此偏移量仍然爲10086,

從服務器B和從服務器C正常接收到了數據,因此偏移量都更新爲了10019,以下圖所示:

很顯然,經過對比主從服務器的複製偏移量,能夠很容易地知道主從服務器是否處於一致狀態。

而後,從服務器A經過重試又從新鏈接到了主服務器,而後向主服務器發送PSYNC命令,並報告了本身當前的複製

偏移量爲10086,主服務器此時須要處理2個問題:

  1. 該對從服務器A執行完整重同步仍是部分重同步?
  2. 若是執行部分重同步,主服務器從哪裏獲取到斷線期間從服務器A丟失的數據?

帶着這2個問題,咱們看下複製積壓緩衝區。

5.2 複製積壓緩衝區

複製積壓緩衝區是主服務器維護的一個固定長度先進先出隊列,默認大小爲1MB。

當主服務器進行命令傳播時,它不只會將寫命令發送給全部從服務器,還會將寫命令入隊到複製積壓緩衝區,以下圖所示:

因此,主服務器的複製積壓緩衝區會保存着一部分最近傳播的寫命令,而且爲隊列中的每一個字節記錄相應的複製偏移量,以下所示:

偏移量 ... 10087 10088 10089 10090 10091 ...
字節值 ... '*' 3 '\r' '\n' '$' ...

當從服務器從新鏈接上主服務器時,會經過PSYNC命令將本身的複製偏移量offset發送給主服務器,主服務器會根據如下規則來決定對從服務器執行何種同步操做:

  • 若是offset偏移量以後的數據仍然存在於複製積壓緩衝區,那麼主服務器將對從服務器執行部分重同步操做。
  • 若是offset偏移量以後的數據已經不存在於複製積壓緩衝區,那麼主服務器將對從服務器執行完整重同步操做。

回到以前的例子:

  1. 從服務器A從新鏈接上主服務器,向主服務器發送PSYNC命令,報告本身的複製偏移量爲10086。
  2. 主服務器收到PSYNC命令以及偏移量10086以後,會檢查偏移量10086以後的數據是否存在於複製積壓緩衝區,結果發現數據還在,因而主服務器向從服務器A發送+CONTINUE回覆,表示數據同步將以部分重同步模式來進行。
  3. 接着主服務器會將複製積壓緩衝區裏10086偏移量以後的全部數據(偏移量爲10087到10119)都發送給從服務器A。
  4. 從服務器A接收這33字節的缺失數據,就回到與主服務器一致的狀態。

5.3 服務器運行ID

每一個Redis服務器,不論主服務器仍是從服務器,都會有本身的運行ID,運行ID在服務器啓動時自動生成,由40個十六進制字符組成,以下圖所示:

當從服務器對主服務器進行初次複製時,主服務器會將本身的運行ID傳送給從服務器,從服務器會將這個運行ID保存起來。

當從服務器斷線並從新鏈接上主服務器時,從服務器會把以前保存的運行ID發送給當前鏈接的主服務器:

  • 若是從服務器以前保存的運行ID和當前鏈接的主服務器的運行ID相同,說明從服務器斷線先後複製的是同一臺主服務器,主服務器能夠繼續嘗試執行部分重同步操做。
  • 若是從服務器以前保存的運行ID和當前鏈接的主服務器的運行ID不相同,說明從服務器斷線先後複製的不是同一臺主服務器,主服務器將對從服務器執行完整重同步操做。

5.4 PSYNC命令執行細節

對於從服務器來講,調用PSYNC命令有如下2種狀況:

  1. 若是從服務器之前沒有複製過任何主服務器,或者以前執行過SLAVEOF on one命令,那麼從服務器在開始一次新的複製時將向主服務器發送PSYNC ? -1命令,主動請求主服務器進行完整重同步。

  2. 若是從服務器已經複製過某個主服務器,那麼從服務器在開始一次新的複製時將向主服務器發送

    PSYNC {runid} {offset}命令,其中runid是上一次複製的主服務器的運行ID,offset是從服務器當前的複製偏移量。

對於主服務器來講,接收到PSYNC命令後會向從服務器返回如下3種回覆中的一種:

  1. 若是主服務器返回+FULLRESYNC {runid} {offset},表示主服務器將與從服務器執行完整重同步操做,其中runid是主服務器的運行ID,從服務器會將這個ID保存起來,在下一次發送PSYNC命令時使用,offset是主服務器當前的複製偏移量,從服務器會將這個值做爲本身的初始化偏移量。
  2. 若是主服務器返回+CONTINUE,表示主服務器將與從服務器執行部分重同步操做,主服務器會將從服務器缺乏的那部分數據發送給從服務器。
  3. 若是主服務器返回-ERROR,表示主服務器的版本低於Redis 2.8,它識別不了PSYNC命令,從服務器將向主服務器發送SYNC命令,並與主服務器執行完整重同步操做。

以上描述流程可使用如下流程圖來表示:

6. 源碼及參考

黃健宏 《Redis設計與實現》

相關文章
相關標籤/搜索