一、概述
在現有企業中80%公司大部分使用的是redis單機服務,在實際的場景當中單一節點的redis容易面臨風險。
面臨問題
- 機器故障。我們部署到一台 Redis 伺服器,當發生機器故障時,需要遷移到另外一台伺服器并且要保證資料是同步的。而資料是最重要的,如果你不在乎,基本上也就不會使用 Redis 了。
- 容量瓶頸。當我們有需求需要擴容 Redis 記憶體時,從 16G 的記憶體升到 64G,單機肯定是滿足不了。當然,你可以重新買個 128G 的新機器。
解決辦法
要實作分布式資料庫的更大的存儲容量和承受高并發通路量,我們會将原來集中式資料庫的資料分别存儲到其他多個網絡節點上。
Redis 為了解決這個單一節點的問題,會把資料複制多個副本部署到其他節點,實作 Redis的高可用和對資料的備份,進而保證資料和服務的高可用。
二、什麼是主從複制
主從複制,是指将一台Redis伺服器的資料,複制到其他的Redis伺服器。前者稱為主節點(master),後者稱為從節點(slave)。資料的複制是單向的,隻能由主節點到從節點。
預設情況下,每台Redis伺服器都是主節點;且一個主節點可以有多個從節點(或沒有從節點),但一個從節點隻能有一個主節點。
三、主從複制的作用
- 資料備份:主從複制實作了資料的熱備份,是持久化之外的另一種資料備份方式。
- 故障恢複:當主節點出現問題時,可以由從節點提供服務,實作快速的故障恢複。
- 讀寫分離:可以用于實作讀寫分離,主庫寫、從庫讀,讀寫分離不僅可以提高伺服器的負載能力,同時可根據需求的變化,改變從庫的數量;
- 負載均衡:在主從複制的基礎上,配合讀寫分離,可以由主節點提供寫服務,由從節點提供讀服務(即寫Redis資料時應用連接配接主節點,讀Redis資料時應用連接配接從節點),分擔伺服器負載;尤其是在寫少讀多的場景下,通過多個從節點分擔讀負載,可以大大提高Redis伺服器的并發量。
- 高可用基石:除了上述作用以外,主從複制還是哨兵和叢集能夠實施的基礎,是以說主從複制是Redis高可用的基礎。
四、如何實作主從同步
有兩種方式可以用來完成主從Redis伺服器的同步設定,都需要針對slave伺服器上進行設定,指定slave需要連接配接的Redis伺服器(可能是master,也可能是slave)。
在配置檔案中設定
在作為slave的Redis伺服器的配置檔案(/etc/redis/6379.conf)中設定。
slaveof 127.0.0.1 6379 #指定master的ip和端口
很明顯,這種設定方式非常簡單,但是需要修改配置檔案,并且配置檔案是在伺服器啟動時加載的。是以伺服器不啟動無法修改,操作不靈活。這種配置方式适合于作為部署時的初始配置。
通過用戶端指令設定
Redis伺服器啟動後,直接通過用戶端執行指令:slaveof,則該Redis執行個體會成為從節點。
127.0.0.1:12345> SLAVEOF 127.0.0.1 6379
OK
執行上述指令後,伺服器127.0.0.1:12345将成為127.0.0.1:6379的從伺服器,而伺服器127.0.0.1:6379則會成為127.0.0.1:12345的主伺服器。
五、主從同步的原理
主從同步分為 2 個步驟:同步(全量同步)和指令傳播(增量同步)
- 同步:将從伺服器的資料庫狀态更新成主伺服器目前的資料庫狀态。
- 指令傳播:當主伺服器資料庫狀态被修改後,導緻主從伺服器資料庫狀态不一緻,需要讓主從資料同步到一緻狀态。
上面就是主從同步 2 個步驟的作用,下面我打算稍微細說這兩個步驟的實作過程。
這裡需要提前說明一下:在 Redis 2.8 版本之前,進行主從複制時一定會順序執行上述兩個步驟,而從 2.8 開始則可能隻需要執行指令傳播即可。在下文也會解釋為什麼會這樣?
同步(全量同步)
當用戶端向從伺服器發送SLAVEOF指令,要求從伺服器複制主伺服器時,從伺服器首先需要執行同步操作,将從伺服器的資料庫狀态更新至主伺服器目前所處的資料庫狀态。
從伺服器對主伺服器的同步操作需要通過向主伺服器發送SYNC指令來完成,具體步驟如下:
1)從伺服器連接配接主伺服器,發送SYNC指令;
2)主伺服器接收到SYNC命名後,開始執行BGSAVE指令生成RDB檔案,并使用緩沖區記錄此後執行的所有寫指令;
3)主伺服器BGSAVE執行完後,向所有從伺服器發送快照檔案,并在發送期間繼續記錄被執行的寫指令;
4)從伺服器收到快照檔案後丢棄所有舊資料,載入收到的快照;
5)主伺服器快照發送完畢後開始向從伺服器發送緩沖區中的寫指令;
6)從伺服器完成對快照的載入,開始接收指令請求,并執行來自主伺服器緩沖區的寫指令;

指令傳播(增量同步)
在執行完同步操作之後,主從伺服器之間資料庫狀态已經相同了。但這個狀态并非一成不變,如果主伺服器執行了寫操作,那麼主伺服器的資料庫狀态就會修改,并導緻主從伺服器狀态不再一緻。
是以為了讓主從伺服器再次回到一緻狀态,主伺服器需要對從伺服器執行指令傳播操作:主伺服器會将自己執行的寫指令,也就是造成主從伺服器不一緻的那條寫指令,發送給從伺服器執行,當從伺服器執行了相同的寫指令之後,主從伺服器将再次回到一緻狀态。注:Redis 同步的是指令流。
六、優化版同步操作
舊版複制功能的缺陷
在Redis中,從伺服器對主伺服器的複制可以分為以下兩種情況:
- 初次複制:從伺服器以前沒有複制過任何主伺服器,或者從伺服器目前要複制的主伺服器和上一次複制的主伺服器不同;
- 斷線後重複制:處于指令傳播階段的主從伺服器因為網絡原因而中斷了複制,但從伺服器通過自動重連接配接重新連上了主伺服器,并繼續複制主伺服器。
對于初次複制來說,舊版複制功能能夠很好地完成任務,但對于斷線後重複制來說,舊版複制功能雖然也能讓主從伺服器重新回到一緻狀态,但效率卻非常低。
我們給出一個例子進行說明:
從伺服器終于重新連接配接上主伺服器,因為這時主從伺服器的狀态已經不再一緻,是以從伺服器将向主伺服器發送SYNC指令,而主伺服器會将包含鍵k1至鍵k10089的RDB檔案發送給從伺服器,從伺服器通過接收和載入這個RDB檔案來将自己的資料庫更新至主伺服器資料庫目前所處的狀态。
上面給出的例子可能有一點理想化,因為在主從伺服器斷線期間,主伺服器執行的寫指令可能會有成百上千個之多,而不僅僅是兩三個寫指令。但總的來說,主從伺服器斷開的時間越短,主伺服器在斷線期間執行的寫指令就越少,而執行少量寫指令所産生的資料量通常比整個資料庫的資料量要少得多,在這種情況下,為了讓從伺服器補足一小部分缺失的資料,卻要讓主從伺服器重新執行一次SYNC指令,這種做法無疑是非常低效的。
SYNC指令是一個非常耗費資源的操作
SYNC指令是非常消耗資源的,因為每次執行SYNC指令,主從伺服器需要執行以下操作:
- 主伺服器需要執行BGSAVE指令來生成RDB檔案,這個生成操作會耗費主伺服器大量的CPU、記憶體和磁盤I/O資源;
- 主伺服器需要将自己生成的RDB檔案發送給從伺服器,這個發送操作會耗費主從伺服器大量的網絡資源(帶寬和流量),并對主伺服器響應指令請求的時間産生影響;
- 接收到RDB檔案的從伺服器需要載入主伺服器發來的RDB檔案,并且在載入期間,從伺服器會因為阻塞而沒辦法處理指令請求。
SYNC是一個如此消耗資源的指令,是以Redis最好在真需要的時候才需要執行SYNC指令。
新版複制功能的實作
為了解決舊版複制功能在處理斷線重複制情況時的低效問題,Redis從2.8版本開始,使用PSYNC指令代替SYNC指令來執行複制時的同步操作。
PSYNC指令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)兩種模式:
- 完整重同步:用于處理初次複制情況,執行步驟和SYNC指令的執行步驟基本一樣,它們都是通過讓主伺服器建立并發送RDB檔案,以及向從伺服器發送儲存在緩沖區裡面的寫指令來進行同步;
- 部分重同步:用于處理斷線後重複制情況,當從伺服器在斷線後重新連接配接主伺服器時,如果條件允許,主伺服器可以将主從伺服器連接配接斷開期間執行的寫指令發送給從伺服器,從伺服器隻要接收并執行這些寫指令,就可以将資料庫更新至主伺服器目前所處的狀态。
上面的介紹中,出現了「如果條件允許」,那又是鬼什麼條件呢?—— 其實就是一個偏移量的比較,具體可以繼續往下看。
部分重同步的實作
部分重同步功能由以下三個部分構成:
- 主伺服器的複制偏移量(replication offset)和從伺服器的複制偏移量;
- 主伺服器的複制積壓緩沖區(replication backlog);
- 伺服器的運作ID(run ID)。
複制偏移量
執行複制的雙方——主伺服器和從伺服器會分别維護一個複制偏移量:
- 主伺服器每次向從伺服器傳播N個位元組的資料時,就将自己的複制偏移量的值加上N;
- 從伺服器每次收到主伺服器傳播來的N個位元組的資料時,就将自己的複制偏移量的值加上N;
通過對比主從伺服器的複制偏移量,程式可以很容易地知道主從伺服器是否處于一緻狀态:
- 如果主從伺服器處于一緻狀态,那麼主從伺服器兩者的偏移量總是相同的;
- 相反,如果主從伺服器兩者的偏移量并不相同,那麼說明主從伺服器并未處于一緻狀态。
如下面的情況:
假設從伺服器A在斷線之後就立即重新連接配接主伺服器,并且成功,那麼接下來,從伺服器将向主伺服器發送PSYNC指令,報告從伺服器A目前的複制偏移量為10086,那麼這時,主伺服器應該對從伺服器執行完整重同步還是部分重同步呢?如果執行部分重同步的話,主伺服器又如何補償從伺服器A在斷線期間丢失的那部分資料呢?以上問題的答案都和複制積壓緩沖區有關。
複制積壓緩沖區
複制積壓緩沖區是由主伺服器維護的一個固定長度(fixed-size)先進先出(FIFO)隊列,預設大小為1MB。
和普通先進先出隊列随着元素的增加和減少而動态調整長度不同,固定長度先進先出隊列的長度是固定的,當入隊元素的數量大于隊列長度時,最先入隊的元素會被彈出,而新元素會被放入隊列。
當主伺服器進行指令傳播時,它不僅會将寫指令發送給所有從伺服器,還會将寫指令入隊到複制積壓緩沖區裡面,如圖所示。
是以,主伺服器的複制積壓緩沖區裡面會儲存着一部分最近傳播的寫指令,并且複制積壓緩沖區會為隊列中的每個位元組記錄相應的複制偏移量,就像下表所示的那樣。
當從伺服器重新連上主伺服器時,從伺服器會通過PSYNC指令将自己的複制偏移量offset發送給主伺服器,主伺服器會根據這個複制偏移量來決定對從伺服器執行何種同步操作:
- 如果offset偏移量之後的資料(也即是偏移量offset+1開始的資料)仍然存在于複制積壓緩沖區裡面,那麼主伺服器将對從伺服器執行部分重同步操作;
- 相反,如果offset偏移量之後的資料已經不存在于複制積壓緩沖區,那麼主伺服器将對從伺服器執行完整重同步操作。
伺服器運作ID
除了複制偏移量和複制積壓緩沖區之外,實作部分重同步還需要用到伺服器運作ID(run ID):
- 每個Redis伺服器,不論主伺服器還是從服務,都會有自己的運作ID;
- 運作ID在伺服器啟動時自動生成,由40個随機的十六進制字元組成,例如53b9b28df8042fdc9ab5e3fcbbbabff1d5dce2b3;
當從伺服器對主伺服器進行初次複制時,主伺服器會将自己的運作ID傳送給從伺服器,而從伺服器則會将這個運作ID儲存起來(注意哦,是從伺服器儲存了主伺服器的ID)。
當從伺服器斷線并重新連上一個主伺服器時,從伺服器将向目前連接配接的主伺服器發送之前儲存的運作ID:
- 如果從伺服器儲存的運作ID和目前連接配接的主伺服器的運作ID相同,那麼說明從伺服器斷線之前複制的就是目前連接配接的這個主伺服器,主伺服器可以繼續嘗試執行部分重同步操作;
- 相反地,如果從伺服器儲存的運作ID和目前連接配接的主伺服器的運作ID并不相同,那麼說明從伺服器斷線之前複制的主伺服器并不是目前連接配接的這個主伺服器,主伺服器将對從伺服器執行完整重同步操作。
PSYNC指令的實作
PSYNC指令的調用方法有兩種:
- 如果從伺服器以前沒有複制過任何主伺服器,或者之前執行過SLAVEOF no one指令,那麼從伺服器在開始一次新的複制時将向主伺服器發送PSYNC ? -1指令,主動請求主伺服器進行完整重同步(因為這時不可能執行部分重同步);
- 如果從伺服器已經複制過某個主伺服器,那麼從伺服器在開始一次新的複制時将向主伺服器發送PSYNC <runid> <offset>指令:其中runid是上一次複制的主伺服器的運作ID,而offset則是從伺服器目前的複制偏移量,接收到這個指令的主伺服器會通過這兩個參數來判斷應該對從伺服器執行哪種同步操作。
根據情況,接收到PSYNC指令的主伺服器會向從伺服器傳回以下三種回複的其中一種:
- 如果主伺服器傳回+FULLRESYNC <runid> <offset>回複,那麼表示主伺服器将與從伺服器執行完整重同步操作:其中runid是這個主伺服器的運作ID,從伺服器會将這個ID儲存起來,在下一次發送PSYNC指令時使用;而offset則是主伺服器目前的複制偏移量,從伺服器會将這個值作為自己的初始化偏移量;
- 如果主伺服器傳回+CONTINUE回複,那麼表示主伺服器将與從伺服器執行部分重同步操作,從伺服器隻要等着主伺服器将自己缺少的那部分資料發送過來就可以了;
- 如果主伺服器傳回-ERR回複,那麼表示主伺服器的版本低于Redis 2.8,它識别不了PSYNC指令,從伺服器将向主伺服器發送SYNC指令,并與主伺服器執行完整同步操作。
七、心跳檢測
剛才提到,主從同步有同步和指令傳播 2 個步驟,當完成了同步之後,主從伺服器就會進入指令傳播階段,在指令傳播階段,從伺服器預設會以每秒一次的頻率,向主伺服器發送指令:REPLCONF ACK <replication_offset>,其中replication_offset是從伺服器目前的複制偏移量。
發送REPLCONF ACK指令對于主從伺服器有三個作用:
- 檢測主從伺服器的網絡連接配接狀态;
- 輔助實作min-slaves選項;
- 檢測指令丢失。
檢測主從伺服器的網絡連接配接狀态
如果主伺服器超過一秒鐘沒有收到從伺服器發來的REPLCONF ACK指令,那麼主伺服器就知道主從伺服器之間的連接配接出現問題了。
通過向主伺服器發送INFO replication指令,在列出的從伺服器清單的lag一欄中,我們可以看到相應從伺服器最後一次向主伺服器發送REPLCONF ACK指令距離現在過了多少秒:
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=12345,state=online,offset=211,lag=0
#剛剛發送過 REPLCONF ACK指令
slave1:ip=127.0.0.1,port=56789,state=online,offset=197,lag=15
#15秒之前發送過REPLCONF ACK指令
master_repl_offset:211
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:210
在一般情況下,lag的值應該在0秒或者1秒之間跳動,如果超過1秒的話,那麼說明主從伺服器之間的連接配接出現了故障。
輔助實作min-slaves配置選項
Redis的min-slaves-to-write和min-slaves-max-lag兩個選項可以防止主伺服器在不安全的情況下執行寫指令。
舉個例子,如果我們向主伺服器提供以下設定:
min-slaves-to-write 3
min-slaves-max-lag 10
那麼在從伺服器的數量少于3個,或者三個從伺服器的延遲(lag)值都大于或等于10秒時,主伺服器将拒絕執行寫指令,這裡的延遲值就是上面提到的INFO replication指令的lag值。
檢測指令丢失
我們從指令:REPLCONF ACK <replication_offset>就可以知道,每發送一次這個指令從伺服器都會向主伺服器報告一次自己的複制偏移量。那此時盡管主伺服器發送給從伺服器的SET key value丢失了。也無所謂,主伺服器馬上就知道了。
八、主從同步具體過程
步驟1:設定主伺服器的位址和端口
當用戶端向從伺服器發送以下指令時:
127.0.0.1:12345> SLAVEOF 127.0.0.1 6379
OK
從伺服器首先要做的就是将用戶端給定的主伺服器IP位址127.0.0.1以及端口6379儲存到伺服器狀态的masterhost屬性和masterport屬性裡面:
struct redisServer {
// ...
// 主伺服器的位址
char *masterhost;
// 主伺服器的端口
int masterport;
// ...
};
SLAVEOF指令是一個異步指令,在完成masterhost屬性和masterport屬性的設定工作之後,從伺服器将向發送SLAVEOF指令的用戶端傳回OK,表示複制指令已經被接收,而實際的複制工作将在OK傳回之後才真正開始執行。
步驟2:建立套接字連接配接
在SLAVEOF指令執行之後,從伺服器将根據指令所設定的IP位址和端口,建立連向主伺服器的套接字連接配接,如圖15-14所示。
如果從伺服器建立的套接字能成功連接配接(connect)到主伺服器,那麼從伺服器将為這個套接字關聯一個專門用于處理複制工作的檔案事件處理器,這個處理器将負責執行後續的複制工作,比如接收RDB檔案,以及接收主伺服器傳播來的寫指令,諸如此類。
而主伺服器在接受(accept)從伺服器的套接字連接配接之後,将為該套接字建立相應的用戶端狀态,并将從伺服器看作是一個連接配接到主伺服器的用戶端來對待,這時從伺服器将同時具有伺服器(server)和用戶端(client)兩個身份:從伺服器可以向主伺服器發送指令請求,而主伺服器則會向從伺服器傳回指令回複。
步驟3:發送PING指令
從伺服器成為主伺服器的用戶端之後,做的第一件事就是向主伺服器發送一個PING指令。
這個PING指令主要是為了:
- 通過發送PING指令檢查套接字的讀寫狀态;
- 通過PING指令可以檢查主伺服器能否正常處理指令。
從伺服器在發送PING指令之後可能遇到以下三種情況:
- 主伺服器向從伺服器傳回了一個指令回複,但從伺服器卻不能在規定的時限内讀取指令回複的内容(timeout),說明網絡連接配接狀态不佳,從伺服器将斷開并重新建立連向主伺服器的套接字;
- 如果主伺服器傳回一個錯誤,那麼表示主伺服器暫時沒有辦法處理從伺服器的指令請求,,從伺服器也将斷開并重新建立連向主伺服器的套接字;
- 如果從伺服器讀取到"PONG"回複,那麼表示主從伺服器之間的網絡連接配接狀态正常,那就繼續執行下面的複制步驟。
步驟4:身份驗證
從伺服器在收到主伺服器傳回的"PONG"回複之後,下一步要做的就是決定是否進行身份驗證:
- 如果從伺服器設定了masterauth選項,那麼進行身份驗證。否則不進行身份認證;
在需要進行身份驗證的情況下,從伺服器将向主伺服器發送一條AUTH指令,指令的參數為從伺服器masterauth選項的值。
從伺服器在身份驗證階段可能遇到的情況有以下幾種:
- 主伺服器沒有設定requirepass選項,從伺服器沒有設定masterauth,那麼就繼續後面的複制工作;
- 如果從伺服器的通過AUTH指令發送的密碼和主伺服器requirepass選項所設定的密碼相同,那麼也繼續後面的工作,否則傳回錯誤invaild password;
- 如果主伺服器設定了requireoass選項,但從伺服器沒有設定masterauth選項,那麼伺服器将傳回NOAUTH錯誤。反過來如果主伺服器沒有設定requirepass選項,但是從伺服器卻設定了materauth選項,那麼主伺服器傳回no password is set錯誤;
所有錯誤隻有一個結果:中止目前的複制工作,并從建立套接字開始重新執行複制,直到身份驗證通過,或者從伺服器放棄執行複制為止。
步驟5:發送端口資訊
身份驗證步驟之後,從伺服器将執行指令REPLCONF listening-port <port-number>,向主伺服器發送從伺服器的監聽端口号。
主伺服器在接收到這個指令之後,會将端口号記錄在從伺服器所對應的用戶端狀态的slave_listening_port屬性中:
typedef struct redisClient {
// ...
// 從伺服器的監聽端口号
int slave_listening_port;
// ...
}redisClient;
slave_listening_port屬性目前唯一的作用就是在主伺服器執行INFO replication指令時列印出從伺服器的端口号。
步驟6:同步
在這一步,從伺服器将向主伺服器發送PSYNC指令,執行同步操作,并将自己的資料庫更新至主伺服器資料庫目前所處的狀态。
需要注意的是在執行同步操作前,隻有從伺服器是主伺服器的用戶端。但是執行同步操作之後,主伺服器也會稱為從伺服器的用戶端:
- 如果PSYNC指令執行的是完整同步操作,那麼主伺服器隻有成為了從伺服器的用戶端才能将儲存在緩沖區中的寫指令發送給從伺服器執行;
- 如果PSYNC指令執行的是部分同步操作,那麼主伺服器隻有成為了從伺服器的用戶端才能将儲存在複制積壓緩沖區中的寫指令發送給從伺服器執行;
步驟7:指令傳播
當完成了同步之後,主從伺服器就會進入指令傳播階段,這時主伺服器隻要一直将自己執行的寫指令發送給從伺服器,而從伺服器隻要一直接收并執行主伺服器發來的寫指令,就可以保證主從伺服器一直保持一緻了。
參考:
https://www.cnblogs.com/lukexwang/p/4711977.html
https://blog.csdn.net/weixin_42711549/article/details/83061052
https://www.jianshu.com/p/9b3136a1d021
https://mp.weixin.qq.com/s/8jmPwEZ7CPg0pVd79in_Tg