面試必問的 Redis:主從復(fù)制
前言
在分布式環(huán)境中,數(shù)據(jù)副本?(Replica)?和復(fù)制?(Replication)?作為提升系統(tǒng)可用性和讀寫性能的有效手段被大量應(yīng)用在各種分布式系統(tǒng)中,Redis 也不例外。
雖說現(xiàn)在基本不會直接使用主從復(fù)制來作為 Redis 的高可用方案,但是無論是哨兵還是集群,都會使用到主從復(fù)制,因此,有必要先學(xué)習(xí)下主從復(fù)制的原理。
正文
主從復(fù)制實現(xiàn)原理
在當前最新的 Redis 6.0 中,主從復(fù)制的完整過程如下:
1、開啟主從復(fù)制
通常有以下三種方式:
1)在 slave 直接執(zhí)行命令:slaveof
2)在 slave 配置文件中加入:slaveof
3)使用啟動命令:--slaveof
注:在 Redis 5.0 之后,slaveof 相關(guān)命令和配置已經(jīng)被替換成 replicaof,例如 replicaof
2、建立套接字(socket)連接
slave 將根據(jù)指定的 IP 地址和端口,向 master 發(fā)起套接字(socket)連接,master 在接受(accept) slave 的套接字連接之后,為該套接字創(chuàng)建相應(yīng)的客戶端狀態(tài),此時連接建立完成。
3、發(fā)送PING命令
slave 向 master 發(fā)送一個 PING 命令,以檢査套接字的讀寫狀態(tài)是否正常、 master 能否正常處理命令請求。
如果 slave 收到 "PONG" 回復(fù),那么表示 master 和 slave 之間的網(wǎng)絡(luò)連接狀態(tài)正常, 并且 master 可以正常處理命令請求。
如果是其他回復(fù)或者沒有回復(fù),表示 master 和 slave 之間的網(wǎng)絡(luò)連接狀態(tài)不佳或者 master 暫時沒辦法處理 slave 的命令請求,則 slave 進入 error 流程:slave 斷開當前的連接,之后再進行重試。
4、身份驗證
如果 master 和 slave 都沒有設(shè)置密碼,則無需驗證。
如果 master 和 slave 都設(shè)置了密碼,并且密碼相同,則驗證成功。
否則,master 和 slave 設(shè)置的密碼不同、master 和 slave 一個設(shè)置密碼一個沒設(shè)置密碼都會返回錯誤。
所有錯誤情況都會令 slave 進入 error 流程:slave 斷開當前的連接,之后再進行重試。
5、發(fā)送端口信息
在身份驗證通過后后, slave 將向 master 發(fā)送自己的監(jiān)聽端口號, master 收到后記錄在 slave 所對應(yīng)的客戶端狀態(tài)的 slave_listening_port 屬性中。
6、發(fā)送IP地址
如果配置了 slave_announce_ip,則 slave 向 master 發(fā)送 slave_announce_ip 配置的 IP 地址, master 收到后記錄在 slave 所對應(yīng)的客戶端狀態(tài)的 slave_ip 屬性。
該配置是用于解決服務(wù)器返回內(nèi)網(wǎng) IP 時,其他服務(wù)器無法訪問的情況。可以通過該配置直接指定公網(wǎng) IP。
7、發(fā)送CAPA
CAPA 全稱是 capabilities,這邊表示的是同步復(fù)制的能力。
slave 會在這一階段發(fā)送 capa 告訴 master 自己具備的(同步)復(fù)制能力, master 收到后記錄在 slave 所對應(yīng)的客戶端狀態(tài)的 slave_capa 屬性。
CAPA 在最新的 Redis 6.0 版本中有兩種值:eof 和 psync2。
eof 表示 slave 支持直接接收從 socket 發(fā)送過來的 RDB 數(shù)據(jù)流,也就是無盤加載(diskless_load)。
psync2 表示 slave 支持 Redis 4.0 引入的部分重同步 v2 版本,這個在下文會詳細介紹。
8、數(shù)據(jù)同步
slave 將向 master 發(fā)送 PSYNC 命令, master 收到該命令后判斷是進行部分重同步還是完整重同步,然后根據(jù)策略進行數(shù)據(jù)的同步。
1)如果是 slave 第一次執(zhí)行復(fù)制,則向 master 發(fā)送 PSYNC ? -1, master 返回 +FULLRESYNC
2)如果不是第一次執(zhí)行復(fù)制,則向 master 發(fā)送 PSYNC replid offset,其中 replid 是 master 的復(fù)制 ID,而 offset 是 slave 當前的復(fù)制偏移量。master 根據(jù) replid 和 offset 來判斷應(yīng)該執(zhí)行哪種同步操作。
如果是完整重同步,則返回 +FULLRESYNC
9、命令傳播
當完成了同步之后,就會進人命令傳播階段,這時 master 只要一直將自己執(zhí)行的寫命令發(fā)送給 slave ,而 slave 只要一直接收并執(zhí)行 master 發(fā)來的寫命令,就可以保證 master 和 slave 一直保持一致了。
在命令傳播階段, slave 默認會以每秒一次的頻率,向 master 發(fā)送命令:REPLCONF ACK
發(fā)送REPLCONF ACK 命令對于主從服務(wù)器有三個作用:
1)檢測 master 和 slave 的網(wǎng)絡(luò)連接狀態(tài)。
2)匯報自己的復(fù)制偏移量,檢測命令丟失,master 會對比復(fù)制偏移量,如果發(fā)現(xiàn) slave 的復(fù)制偏移量小于自己,會向 slave 發(fā)送未同步的數(shù)據(jù)。
3)輔助實現(xiàn) min-slaves 配置,用于防止 master 在不安全的情況下執(zhí)行寫命令。
例如以下配置表示:當延遲時間小于10秒的 slave 數(shù)量小于3個,則會拒絕執(zhí)行寫命令。而這邊的延遲時間,就是以 slave 最近一次發(fā)送 ACK 時間和當前時間作對比。
min-slaves-to-write?3min-slaves-max-lag?10
以部分重同步為例,主從復(fù)制的核心步驟流程圖如下:

相關(guān)源碼在 replication.c,核心方法是:replicationSetMaster、connectWithMaster、syncWithMaster
舊版同步:SYNC
Redis 2.8 之前的數(shù)據(jù)同步通過 SYNC 命令完成,完整流程如下:
1、slave 向 master 發(fā)送 SYNC 命令。
2、master 收到 SYNC 命令后執(zhí)行 BGSAVE 命令,fork 子進程生成 RDB 文件,同時會使用一個緩沖區(qū)記錄從現(xiàn)在開始執(zhí)行的所有寫命令。
Redis 在這邊使用了 COW(copy-on-write)的特性,這邊簡單介紹一下。
fork 子進程時,一種比較“愚蠢”的做法是將父進程的整個地址空間拷貝一份給子進程,但這是非常耗時的,因此一般不會這么做。
另一種做法是,fork 之后,父子進程共用父進程已有的地址空間,只有當父子進程要進行寫操作時,才將要修改的內(nèi)容復(fù)制一份,再進行寫操作,這也是 copy-on-write 名字的由來。
回到本文,這邊當主進程 fork 出子進程時,因為 COW 的關(guān)系,可以認為在 fork 的這一刻,快照已經(jīng)生成了,只是還沒寫到 RDB 文件。
那這邊就有一個問題,RDB 文件是 fork 這一刻的數(shù)據(jù),從 fork 這一刻到 master 將 RDB 文件發(fā)送給 slave 之間,主進程還在繼續(xù)執(zhí)行寫命令,這期間的寫命令 slave 怎么獲得?
這就用到上面“同時會使用一個緩沖區(qū)記錄從現(xiàn)在開始執(zhí)行的所有寫命令”,這個緩沖區(qū)會記錄 fork 之后的所有寫命令。
后面當 master 將 RDB 文件發(fā)送給 slave 后,master 會繼續(xù)將緩沖區(qū)中的寫命令發(fā)送給 slave,也就是下面的第4步,從而保證 slave 的數(shù)據(jù)是完整的。
3、當 BGSAVE 命令執(zhí)行完畢,master 會將生成的 RDB 文件發(fā)送給 slave。slave 接收 RDB 文件,并載入到內(nèi)存,將數(shù)據(jù)庫狀態(tài)更新至 master 執(zhí)行 BGSAVE 時的數(shù)據(jù)庫狀態(tài)。
這邊發(fā)送 RDB 文件的方式有兩種:1)socket:master 將 RDB 文件流通過 socket 直接發(fā)送到 slave;2)disk:master 將 RDB 文件先持久化到磁盤,再發(fā)送到 slave。
默認使用方式為 disk,可以通過以下配置來使用 socket 方式。
repl-diskless-sync yes同時,相關(guān)的參數(shù)配置還有?diskless-sync-delay:該參數(shù)表示等待一定時長再開始復(fù)制,這樣可以等待多個 slave 節(jié)點重新連接上來。
socket(無磁盤)方式適合磁盤讀寫速度慢但網(wǎng)絡(luò)帶寬非常高的環(huán)境。
另外,這邊主進程檢查子進程 BGSAVE 是否執(zhí)行完畢是通過時間事件定時檢查的。
4、master 將記錄在緩沖區(qū)里面的所有寫命令發(fā)送給 slave,slave 執(zhí)行這些命令,將數(shù)據(jù)庫狀態(tài)更新至 master 當前所處的狀態(tài)。
SYNC 存在的問題:slave 每次斷線重連都需要使用完整重同步,效率低下。
新版同步:SYNC
為了解決 slave 每次斷線重連都需要使用完整重同步,redis 在 2.8 版本引入了 PSYNC,PSYNC 包含完整重同步和部分重同步。
1、完整重同步:和 SYNC 命令基本一致。
2、部分重同步:slave 只需要接收和同步斷線期間丟失的寫命令即可,不需要進行完整重同步。
為了實現(xiàn)部分重同步,Redis 引入了復(fù)制偏移量、復(fù)制積壓緩沖區(qū)和運行 ID 三個概念。
復(fù)制偏移量(offset)
執(zhí)行主從復(fù)制的雙方都會分別維護一個復(fù)制偏移量,master 每次向 slave 傳播 N 個字節(jié),自己的復(fù)制偏移量就增加 N;同理 slave 接收 N 個字節(jié),復(fù)制偏移量也增加 N。通過對比主從之間的復(fù)制偏移量就可以知道主從間的同步狀態(tài)。
復(fù)制積壓緩沖區(qū)(replication backlog buffer)
復(fù)制積壓緩沖區(qū)是 master 維護的一個固定長度的 FIFO 隊列,默認大小為 1MB。
當 master 進行命令傳播時,不僅將寫命令發(fā)給 slave 還會同時寫進復(fù)制積壓緩沖區(qū),因此 master 的復(fù)制積壓緩沖區(qū)會保存一部分最近傳播的寫命令。
當 slave 重連上 master 時會將自己的復(fù)制偏移量通過 PSYNC 命令發(fā)給 master,master 檢查自己的復(fù)制積壓緩沖區(qū),如果發(fā)現(xiàn)這部分未同步的命令還在自己的復(fù)制積壓緩沖區(qū)中的話就可以利用這些保存的命令進行部分同步,反之如果斷線太久這部分命令已經(jīng)不在復(fù)制緩沖區(qū)中了,那沒辦法只能進行全量同步。
運行 ID(runid)
每個 Redis server 都會有自己的運行 ID,由 40 個隨機的十六進制字符組成。當 slave 初次復(fù)制 master 時,master 會將自己的運行 ID 發(fā)給 slave 進行保存,這樣 slave 重連時再將這個運行 ID 發(fā)送給重連上的 master ,master 會接受這個 ID 并與自身的運行 ID 比較進而判斷是否是同一個 master。
引入這三個概念后,數(shù)據(jù)同步過程如下:
1)slave 通過 PSYNC runid offset 命令,將正在復(fù)制的 runid 和 offset 發(fā)送給 master。
2)master 判斷 runid 和自己的 runid 相同,并且 offset 還在復(fù)制積壓緩沖區(qū),則進行部分重同步:通過復(fù)制積壓緩沖區(qū)將 slave 缺失的命令發(fā)送給 slave,slave 執(zhí)行命令,將數(shù)據(jù)庫狀態(tài)更新至 master 所處的狀態(tài)。
3)否則,如果 master 判斷 runid 不相同,或者 offset 已經(jīng)不在復(fù)制積壓緩沖區(qū),則執(zhí)行完整重同步。
PSYNC 的完整流程如下圖:

PSYNC 存在的問題
通過上述流程,我們可以看出,PSYNC 執(zhí)行部分重同步需要滿足兩個條件:1)master runid 不變;2)復(fù)制偏移量在 master 復(fù)制積壓緩沖區(qū)中。一旦不滿足這兩個條件,則仍然需要進行完整重同步,例如以下場景。
1、slave 重啟,緩存的 master runid 和 offset 都會丟失,slave 需進行完整重同步。
2、redis 發(fā)生故障切換,故障切換后 master runid 發(fā)生了變化,slave 需進行完整重同步。
slave 維護性重啟、master 故障切換都是 redis 運維常見場景,因此,PSYNC 的這兩個問題出現(xiàn)概率還是非常高的。
相關(guān)源碼在 replication.c,核心方法是:syncCommand、readSyncBulkPayload、replicationFeedSlaves、backgroundSaveDoneHandler、slaveTryPartialResynchronization 等
PSYNC2
為了解決 PSYNC 在 slave 重啟和故障切換導(dǎo)致完整重同步的問題,Redis 在 4.0 版本中對 PSYNC 進行了優(yōu)化,我們稱為 PSYNC2。
PSYNC2 進行了以下2個主要改動:
1、引入兩組 replid 和 offset 替換原來的 runid 和 offset
第一組:replid 和 master_repl_offset
對于 master,表示為自己的復(fù)制 ID 和復(fù)制偏移量;
對于 slave,表示為自己正在同步的 master 的復(fù)制 ID 和復(fù)制偏移量。
可以認為這一組的兩個字段就是對應(yīng)原來的 runid 和 offset。
第二組:replid2 和 second_repl_offset
對于 master 和 slave,都表示自己的上一個 master 的復(fù)制 ID 和復(fù)制偏移量;主要用于故障切換時支持部分重同步。
值得注意的是,runid 并不是在引入 replid 之后就不存在了。在 4.0 之前,redis 使用 runid 來作為主從復(fù)制的標識,而在 4.0 后引入了 replid 來作為主從復(fù)制的標識,但是,runid 在 redis 中的功能不僅僅是作為主從復(fù)制的標識,runid 仍然有其他的功能,例如:用于作為 redis 服務(wù)器的唯一標識。
2、slave 也會開啟復(fù)制積壓緩沖區(qū)
slave 開啟復(fù)制積壓緩沖區(qū),主要是用于故障切換后,當某個 slave 升級為 master,該 slave 仍然可以通過復(fù)制積壓緩沖區(qū)繼續(xù)支持部分重同步功能。
如果 slave 不開啟復(fù)制積壓緩沖區(qū),當該 slave 升級為 master 后,復(fù)制積壓緩沖區(qū)是空的,就沒法支持部分重同步了。
接下來,讓我們看看 Redis 是如何針對 PSYNC 的兩個問題來進行優(yōu)化。
優(yōu)化場景1:slave 重啟后導(dǎo)致完整同步
產(chǎn)生該問題的根本原因是 slave 重啟后,復(fù)制 ID(運行 ID) 和 復(fù)制偏移量丟失了。解決辦法其實很簡單,就是在關(guān)閉服務(wù)器前將這兩個變量存下來即可。
Redis 的做法如下:slave 在正常關(guān)閉前會調(diào)用 rdbSaveInfoAuxFields 函數(shù)把當前的復(fù)制 ID(replid) 和復(fù)制偏移量(master_repl_offset)作為輔助字段保存到 RDB 文件中,后面該 slave 重啟的時候,就可以從 RDB 文件中讀取復(fù)制 ID 和復(fù)制偏移量,然后使用這兩個變量來進行部分重同步。
優(yōu)化場景2:master 故障切換后導(dǎo)致完整重同步
產(chǎn)生該問題的根本原因是故障切換后出現(xiàn)了新的 master,而新 master 的復(fù)制 ID(運行 ID)發(fā)生改變導(dǎo)致沒法進行部分重同步。
在正常同步的情況下,新 master 的數(shù)據(jù)跟老 master 理論上是完全一致的,包括復(fù)制積壓緩沖區(qū)的數(shù)據(jù)。
因此理論上 slave 是可以進行部分重同步的,現(xiàn)在僅僅是因為復(fù)制 ID 變了而沒法進行。所以,我們的目標就是想辦法讓新 master 和其他 slave 可以串聯(lián)起來。
新 master 和其他沒有晉升的 slave 的共同點是故障切換前的 master 是相同的,因此很容易想到的做法是:利用故障切換前的 master 來串聯(lián)新 master 和剩余 slave。
Redis 的做法如下:當節(jié)點從 slave 晉升為 master 后,會將原來自己保存的第一組復(fù)制 ID 和復(fù)制偏移量(也就是老 master 的),移動到第二組復(fù)制 ID 和復(fù)制偏移量,然后將第一組復(fù)制 ID 重新生成一個新的,也就是屬于自己的復(fù)制 ID。
相當于,slave 晉升為 master 后,replid 保存了自己的復(fù)制 ID,replid2 保存了老 master 的復(fù)制ID。
這樣,新 master 就可以通過 replid2 來判斷 slave 是否之前跟自己從是從同一個 master 復(fù)制數(shù)據(jù),如果是的話,則嘗試使用部分重同步。
PSYNC2 的完整流程如下,可以看出和 PSYNC 很類似,主要區(qū)別在于紫色框部分。

相關(guān)源碼基本同 PSYNC
主從復(fù)制的演變
從 Redis 2.* 到現(xiàn)在,開發(fā)人員對主從復(fù)制流程進行逐步的優(yōu)化,以下是演進過程:
1、2.8 版本之前 Redis 復(fù)制采用 SYNC 命令,無論是第一次復(fù)制還是斷線重連后的復(fù)制都采用完整重同步,成本高。
2、2.8 ~ 4.0 之間復(fù)制采用 PSYNC 命令,主要優(yōu)化了 Redis 在斷線重連時候可通過 runid 和 offset 信息使用部分重同步。
3、4.0 版本之后對 PSYNC 進行了優(yōu)化,通常稱為 PSYNC2,主要優(yōu)化了 PSYNC 在 slave 重啟和故障切換時的完整重同步問題。
最后
當你的才華還撐不起你的野心的時候,你就應(yīng)該靜下心來學(xué)習(xí),愿你在我這里能有所收獲。
如果你覺得本文寫的還不錯,對你有幫助,請通過【點贊】讓我知道,支持我寫出更好的文章。
推薦閱讀
面試必問的 Redis:數(shù)據(jù)結(jié)構(gòu)和基礎(chǔ)概念
兩年Java開發(fā)工作經(jīng)驗面試總結(jié)
4 年 Java 經(jīng)驗面試總結(jié)、心得體會
