救命!只有我還不明白Redis主從復制的原理嗎?
1. 引言
之前我們聊過 Redis 的數據結構底層原理和持久化機制,這期我們來聊 Redis 的高可用主題。
時光穿梭機:
- Redis持久化都說不明白?那今天先到這吧~
- Redis數據結構的底層原理
眾所周知,一個數據庫系統想要實現高可用,主要從以下兩個方面來考慮:
- 保證數據安全不丟失
- 系統可以正常提供服務
而 Redis 作為一個提供高效緩存服務的數據庫,也不例外。
上期我們提到的 Redis 持久化策略,其實就是為了減少服務宕機后數據丟失,以及快速恢復數據,也算是支持高可用的一種實現。
除此之外,Redis 還提供了其它幾種方式來保證系統高可用,業務中最常用的莫過于主從同步(也稱作主從復制)、Sentinel 哨兵機制以及 Cluster 集群。
同時,這也是面試中出現頻率最高的幾個主題,這期我們先來講講 Redis 的主從復制。
2. 主從復制簡介
Redis 同時支持主從復制和讀寫分離:一個 Redis 實例作為主節點 Master,負責寫操作。其它實例(可能有 1 或多個)作為從節點 Slave,負責復制主節點的數據。
2.1 架構組件
主節點Master
數據更新:Master 負責處理所有的寫操作,包括寫入、更新和刪除等。
數據同步:寫操作在 Master 上執行,然后 Master 將寫操作的結果同步到所有從節點 Slave 上。
從節點Slave
數據讀取:Slave 負責處理讀操作,例如獲取數據、查詢等。
數據同步:Slave 從 Master 復制數據,并在本地保存一份與主節點相同的數據副本。
2.2 為什么要讀寫分離
1)防止并發
從上圖我們可以看出,數據是由主節點向從節點單向復制的,如果主、從節點都可以寫入數據的話,那么數據的一致性如何保證呢?
有聰明的小伙伴可能已經想到了,那就是加鎖!
但是主、從節點分布在不同的服務器上,數據跨節點同步時又會出現分布式一致性的問題。而在高頻并發的場景下,解決加鎖后往往又會帶來其它的分布式問題,例如寫入效率低、吞吐量大幅下降等。
而對于 Redis 這樣一個高效緩存數據庫來說,性能降低是難以忍受的,所以加鎖不是一個優秀的方案。
那如果不加鎖,使用最終一致性方式呢?
這樣 Redis 在主、從庫讀到的數據又可能會不一致,帶來業務上的挑戰,用戶也是難以接受的。
業務為用戶服務,技術為業務服務。
所以,為了權衡數據的并發問題和用戶體驗,我們只允許在主節點上寫入數據,從節點上讀取數據。
不理解分布式一致性的同學可以看我之前的這篇文章:深入淺出:分布式、CAP和BASE理論
2)易于擴展
我們都知道,大部分使用 Redis 的業務都是讀多寫少的。所以,我們可以根據業務量的規模來確定掛載幾個從節點 Slave,當緩存數據增大時,我們可以很方便的擴展從節點的數量,實現彈性擴展。
同時,讀寫分離還可以實現數據備份和負載均衡,從而提高可靠性和性能。
3)高可用保障
不僅如此,Redis 還可以手動切換主從節點,來做故障隔離和恢復。這樣,無論主節點或者從節點宕機,其他節點依然可以保證服務的正常運行。
3. 主從復制實現
3.1 開啟主從復制
要開啟主從復制,我們需要用到 replicaof 命令。
當我們確定好主節點的 IP 地址和端口號,在從庫執行 replicaof <masterIP> <masterPort> 這個命令,就可以開啟主從復制。
注意,在 Redis5.0 之前,該命令為 slaveof
開啟主從復制后,應用層采用讀寫分離,所有的寫操作在主節點進行,所有讀操作在從節點進行。
主從節點會保持數據的最終一致性:主庫更新數據后,會同步給從庫。
3.2 主從復制過程
那主從庫同步什么時候開始和結束呢?
是一次性傳輸還是分批次寫入?Redis 主從節點在同步過程中網絡中斷了,沒傳輸完成的怎么辦?
帶著這些疑問我們來分析下,首先,Redis 第一次數據同步時分 3 個階段。
1)建立連接,請求數據同步
主從節點建立連接,從庫請求數據同步。
從服務器從 replicaof 配置項中獲取主節點的 IP 和 Port,然后進行連接。
連接成功后,從服務器會向主服務器發送 PSYNC 命令,表示要進行同步。同時,命令中包含 runID 和 offset 兩個關鍵字段。
- runID:每個 Redis 實例的唯一標識,當主從復制進行時,該值為 Redis 主節點實例的ID。由于首次同步時還不知道主庫的實例ID,所以該值第一次為 ?
- offset:從庫數據同步的偏移量,當第一次復制時,該值為 -1,表示全量復制
主服務器收到 PSYNC 命令后,會創建一個專門用于復制的后臺線程(replication thread),然后記錄從節點的 offset 參數并開始進行 RDB 同步。
2)RDB 同步
主庫生成 RDB 文件,同步給從庫。
當從服務器連接到主服務器后,主服務器會將自己的數據發送給從服務器,這個過程叫做全量復制。主服務器會執行 bgsave 命令,然后 fork 出一個子進程來遍歷自己的數據集并生成一個 RDB 文件,將這個文件發送給從服務器。
在這期間,為了保證 Redis 的高性能,主節點的主進程不會被阻塞,依舊對外提供服務并接收數據寫入緩沖區中。
從服務器接收到 RDB 文件后,會清空自身數據,然后加載這個文件,將自己的數據集替換成主服務器的數據集。
3)命令同步
在第一次同步過程中,由于是全量同步,所以用時可能比較長,這期間主庫依舊會寫入新數據。
但是,在數據同步一開始就生成的 RDB 文件中顯然是沒有這部分新增數據的,所以第一次數據同步后需要再發送一次這部分新增數據。
這樣,主服務器需要在發送完 RDB 文件后,將期間的寫操作重新發送給從服務器,以保證從服務器的數據集與主服務器保持一致。
3.3 增量同步
1)命令傳播
在完成全量復制后,主從服務器之間會保持一個 TCP 連接,主服務器會將自己的寫操作發送給從服務器,從服務器執行這些寫操作,從而保持數據一致性,這個過程也稱為基于長連接的命令傳播(command propagation)。
增量復制的數據是異步復制的,但通過記錄寫操作,主從服務器之間的數據最終會達到一致狀態。
2)網絡斷開后數據同步
命令傳播的過程中,由于網絡抖動或故障導致連接斷開,此時主節點上新的寫命令將無法同步到從庫。
即便是抖動瞬間又恢復網絡連接,但 TCP 連接已經斷開,所以數據需要重新同步。
從 Redis 2.8 開始,從庫已支持增量同步,只會把斷開的時候沒有發生的寫命令,同步給從庫。
詳細過程如下:
- 網絡恢復后,從庫攜帶之前主庫返回的 runid,還有復制的偏移量 offset 發送 psync runid offset 命令給主庫,請求數據同步;
- 主庫收到命令后,核查 runid 和 offset,確認沒問題將響應 continue 命令;
- 主庫發送網絡斷開期間的寫命令,從庫接收命令并執行。
這時,有細心的小伙伴可能要問了,網絡斷開后,主庫怎么知道哪些數據是新寫入的呢?
這是個好問題,接下來我們詳細說明一下。
3)增量復制的關鍵
Master 在執行寫操作時,會將這些命令記錄在 repl_backlog_buffer (復制積壓緩沖區)里面,并使用 master_repl_offset 記錄寫入的位置偏移量。
而從庫在執行同步的寫命令后,也會用 slave_repl_offset 記錄寫入的位置偏移量。正常情況下,從庫會和主庫的偏移量保持一致。
但是,當網絡斷開后,主庫繼續寫入,而從庫沒有收到新的同步命令,所以偏移量就停止了。所以,master_repl_offset 會大于 slave_repl_offset。
注意:主從庫實現增量復制時,都是在 repl_backlog_buffer 緩沖區上進行。
網絡斷開前后,主從庫的同步圖如下:
repl_backlog_buffer 復制積壓緩沖區是一個環形緩沖區,如果緩沖區慢了(比如超過 1024),則會從頭覆蓋掉前面的內容。
所以,當網絡恢復以后,主節點只需將 master_repl_offset 和 slave_repl_offset 之間的內容同步給從庫即可(圖中 256~512 這部分數據)。
需要注意的是,主庫的積壓緩沖區默認為 1M,如果從庫網絡斷開太久,緩沖區之前的內容已經被覆蓋,這時主從的數據復制就只能采取全量同步了。
所以我們需要根據業務量和實際情況來設置 repl_backlog_buffer 的值。
4. 小結
面讓架構易于擴展,另一方面防止單體故障:當主庫掛了,可以立即拉起從庫,不至于讓業務停滯太久。
而首次主從復制包括建立連接,RDB 同步和命令同步三個階段。
為了保證同步的效率,除了第一次需要全量同步以外,例如當主從節點斷連后,則只需要增量同步,這是由主從庫的復制偏移量以及主庫的 repl_backlog_buffer 復制積壓緩沖區來控制的。