Redis系列(四):Redis的複製機制(主從複製)
阿新 • • 發佈:2020-04-07
本篇部落格是Redis系列的第4篇,主要講解下Redis的主從複製機制。
本系列的前3篇可以點選以下連結檢視:
[Redis系列(一):Redis簡介及環境安裝](https://www.cnblogs.com/zwwhnly/p/12185696.html)
[Redis系列(二):Redis的5種資料結構及其常用命令](https://www.cnblogs.com/zwwhnly/p/12216550.html)
[Redis系列(三):Redis的持久化機制(RDB、AOF)](https://www.cnblogs.com/zwwhnly/p/12295692.html)
> Redis的主從複製是面試中經常會被問的,我最近面試的幾家公司只要聊到Redis,都會問我主從複製的原理。
## 1. 為什麼需要主從複製?
在本系列的上一篇部落格中,我們講到了Redis的持久化機制,它很好的解決了**單臺Redis伺服器**由於意外情況導致Redis伺服器程序退出或者Redis伺服器宕機而造成的資料丟失問題。
但持久化機制還原資料有個前提:你的Redis伺服器得能正常啟動。
如果遇到極端的斷電情況(雖然概率小,但是有可能),Redis伺服器啟都啟動不了,怎麼還原資料?怎麼保證它的高可用。
就算Redis伺服器能啟動了,網路連線也有崩掉的可能,我不信你沒看到過電纜被挖斷導致的某些服務不可用的新聞。
正是由於有這樣的風險,所以生產環境Redis伺服器不可能使用單臺的,那既然使用多臺Redis伺服器,多臺Redis伺服器之間的資料如何同步呢?
這就需要用到Redis的複製機制。
還有個原因就是,雖然Redis的效能很好,但單臺畢竟還是有瓶頸的,使用主從複製可以實現讀寫分離,提高Redis的高可用性,即主伺服器用來執行寫命令,多個從伺服器用來執行讀命令,類似於資料庫的讀寫分離。
綜上所述,主從複製主要有以下2個使用場景:
1. 資料備份
2. 讀寫分離
## 2. 主從複製實踐
首先,我在本機開啟2個Redis例項(也可以搞2臺Redis伺服器),分別為127.0.0.1:6379、127.0.0.1:6380。
然後,使用`redis-cli`連線Redis例項127.0.0.1:6380並執行如下命令:
```powershell
SLAVEOF 127.0.0.1 6379
```
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200330_150521.png)
此時,我們稱127.0.0.1:6379為127.0.0.1:6380的**主伺服器(master)**,稱127.0.0.1:6380為127.0.0.1:6379的**從伺服器(slave)**。
2者之間的關係如下所示:
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200330_153259.png)
然後,我們在主伺服器上執行如下寫命令:
```powershell
SET msg "hello world"
```
此時,我們不僅能在主伺服器上獲取到該值,也能在從伺服器上獲取到該值:
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200330_151919.png)
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200330_151936.png)
然後,我們在主伺服器上執行如下刪除命令:
```powershell
DEL msg
```
此時,我們會發現不僅主伺服器上的msg鍵被刪除,從伺服器上的msg也被刪除:
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200330_152332.png)
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200330_152351.png)
所以說,**進行復制中的主從伺服器雙方的資料庫將儲存相同的資料**。
值得注意的是,從伺服器只能執行讀命令,執行寫命令時會報如下錯誤:
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200330_155121.png)
如果從伺服器不想再複製主伺服器,可以執行命令:`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`命令執行期間,主從伺服器的通訊過程如下圖所示:
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200330_181255.png)
### 3.2 命令傳播
在**同步操作**執行完畢後,主從伺服器的資料庫狀態達到一致狀態,當主伺服器執行了客戶端傳送的寫命令時,主伺服器的資料庫就被修改了,導致主從伺服器的資料庫狀態不再一致。
為了讓主從伺服器的資料庫狀態再次回到一致狀態,主伺服器需要對從伺服器執行**命令傳播操作**:主伺服器會將自己執行的寫命令,傳送給從伺服器執行,當從伺服器執行了相同的寫命令後,主從伺服器的資料庫狀態再次回到一致狀態。
舉個具體的例子,比如主從伺服器剛開始都擁有k1、k2、k3、k4、k5這5個鍵,然後客戶端往主伺服器傳送了命令`DEL k3`,此時主伺服器會執行該條命令,並將該條命令傳播給從伺服器執行,從而使主從伺服器的資料庫狀態保持一致。
整個變化過程如下所示:
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200330_192504.png)
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200330_192516.png)
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200330_192529.png)
## 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檔案,效能大大提升。
主從伺服器在執行部分重同步時的通訊過程如下圖所示:
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200404_154222.png)
那麼部分重同步是如何實現的呢?
部分重同步功能由以下3個部分組成:
1. 主伺服器和從伺服器的複製偏移量
2. 主伺服器的複製積壓緩衝區
3. 伺服器的執行ID
接下來我們一一講解。
### 5.1 複製偏移量
執行復制的主伺服器和從伺服器會分別維護一個複製偏移量:
1. 主伺服器每次向從伺服器傳播N個位元組的資料時,就將自己的複製偏移量的值加上N。
2. 從伺服器每次收到主伺服器傳播來的N個位元組的資料時,就將自己的複製偏移量的值加上N。
舉個例子,假設主伺服器有3個從伺服器,它們的複製偏移量都為10086,如下圖所示:
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200404_161559.png)
然後,主伺服器向3個從伺服器傳播了長度為33位元組的資料,那麼主伺服器的複製偏移量會加上33,變為10119,
從伺服器A在這時剛好斷線了,沒有接收到資料,所以偏移量仍然為10086,
從伺服器B和從伺服器C正常接收到了資料,所以偏移量都更新為了10019,如下圖所示:
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200404_162438.png)
很顯然,通過對比主從伺服器的複製偏移量,可以很容易地知道主從伺服器是否處於一致狀態。
然後,從伺服器A通過重試又重新連線到了主伺服器,然後向主伺服器傳送PSYNC命令,並報告了自己當前的複製
偏移量為10086,主伺服器此時需要處理2個問題:
1. 該對從伺服器A執行完整重同步還是部分重同步?
2. 如果執行部分重同步,主伺服器從哪裡獲取到斷線期間從伺服器A丟失的資料?
帶著這2個問題,我們看下複製積壓緩衝區。
### 5.2 複製積壓緩衝區
複製積壓緩衝區是主伺服器維護的一個**固定長度先進先出佇列**,預設大小為1MB。
當主伺服器進行命令傳播時,它不僅會將寫命令傳送給所有從伺服器,還會將寫命令入隊到複製積壓緩衝區,如下圖所示:
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200404_170512.png)
所以,主伺服器的複製積壓緩衝區會儲存著一部分最近傳播的寫命令,並且為佇列中的每個位元組記錄相應的複製偏移量,如下所示:
| 偏移量 | ... | 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位元組的缺失資料,就回到與主伺服器一致的狀態。
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200404_173955.png)
### 5.3 伺服器執行ID
每個Redis伺服器,不論主伺服器還是從伺服器,都會有自己的執行ID,執行ID在伺服器啟動時自動生成,由40個十六進位制字元組成,如下圖所示:
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200404_175331.png)
當從伺服器對主伺服器進行**初次複製**時,主伺服器會將自己的執行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`命令,並與主伺服器執行完整重同步操作。
以上描述流程可以使用以下流程圖來表示:
![](https://images.zwwhnly.com/picture/2020/03/snipaste_20200405_161306.png)
## 6. 原始碼及參考
黃健巨集 《Redis設計與