1. 程式人生 > 實用技巧 >16. Redis主從同步

16. Redis主從同步

楔子

主從同步(主從複製)是 Redis 高可用服務的基石,也是多機執行中最基礎的一個。我們把主要儲存資料的節點叫做主節點 (master),把其他通過複製主節點資料的副本節點叫做從節點 (slave),如下圖所示:

在 Redis 中一個主節點可以擁有多個從節點,一個從節點也可以是其他伺服器的主節點,如下圖所示:

主從同步

主從同步的優點

優點有以下三個:

  • 效能方面:有了主從同步之後,可以把查詢任務分配給從伺服器,用主伺服器來執行寫操作,這樣極大的提高了程式執行的效率,把所有壓力分攤到各個伺服器了;
  • 高可用:當有了主從同步之後,當主伺服器節點宕機之後,可以很迅速的把從節點提升為主節點,為 Redis 伺服器的宕機恢復節省了寶貴的時間;
  • 防止資料丟失:當主伺服器磁碟壞掉之後,其他從伺服器還保留著相關的資料,不至於資料全部丟失。

既然主從同步有這麼多的優點,那接下來我們來看如何開啟和使用主從同步功能。

開啟主從同步

由於我只有一臺阿里雲伺服器,所以無法用實際的三臺機器,不過萬幸的是我們有docker,我們執行三個容器不就行啦。

這裡我們啟動三個容器,繫結宿主機的埠分別是6379、6380、6381,其中6379是master,6380和6381是slave,三個容器名分別叫redis_master、redis_slave1、redis_slave2。

但是注意:docker根據redis映象啟動的容器是沒有配置檔案的,因為docker根據redis映象建立的容器預設是無配置檔案啟動Redis的。如果想指定Redis的配置,方法有以下三種:

  • 建立容器的時候通過資料卷的方式,我們將宿主機中的redis.conf傳遞到容器裡面,然後啟動Redis的時候手動指定配置檔案。
  • 建立容器的時候,在命令列中指定配置,我們知道Redis的配置檔案中可配置的東西很多,但如果我們只需要修改一兩個,那麼也可以在命令列中中進行設定。比如:docker run -d --name redis_master -p 6379:6379 redis --slaveof 127.0.0.1 6379 --masterauth 123456
  • Redis的一個特點是可以在配置檔案中修改,重啟生效;也可以在命令列中修改,會立即生效,只不過重啟Redis的時候就沒有了。因此我們在啟動Redis進入控制檯之後,在控制檯中設定也是可以的。

而Redis開啟主從同步非常簡單,只需要在從節點的配置檔案中指定slaveof <master_ip> <master_port>即可,當然如果主節點設定的密碼,那麼從節點還需要指定masterauth <master的密碼>

或者我們還有一種方式,我們將宿主機上的Redis配置檔案拷貝一下,得到三份配置檔案,分別監聽6379、6380、6381,然後指定不同的配置檔案分別啟動也是可以的,我們就用這種方式啟動吧。主要是docker啟動之後,三個容器的頁面上顯示的都是127.0.0.1:6379>,不好區分。

# 注意要將配置檔案中的daemonize設定為yes,否則不會後臺啟動,而是會卡主
redis-server ~/master/redis.conf
redis-server ~/slave1/redis.conf
redis-server ~/slave2/redis.conf

然後我們檢視程序:

我們看到此時啟動三個redis-server程序,然後通過redis-cli啟動控制檯顯然我們此時要開啟三個終端,進入控制檯之後可以通過通過info replication來檢視狀態。

# master節點
127.0.0.1:6379> info replication
# Replication
role:master  # 角色為master
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6379> 


# slave1節點
127.0.0.1:6380> info replication
# Replication
role:master  # 角色為master
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6380> 


# slave2節點
127.0.0.1:6381> info replication
# Replication
role:master  # 角色為master
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6381> 

我們看到此時的三個redis-server都是master,原因就在於我們沒有在配置檔案中指定slaveof,所以它們都是獨立的,之間沒有任何關係。下面我們來設定叢集:

# 我們先以6380、也就是slave1為例
127.0.0.1:6380> set name nana  # 設定一個key
OK
127.0.0.1:6380> get name  # 獲取key
"nana"
127.0.0.1:6380> slaveof 127.0.0.1 6379  # 將自身作為127.0.0.1 6379的子節點
OK
127.0.0.1:6380> info replication  # 檢視狀態
# Replication
role:slave  # 發現角色從master變成了slave
master_host:127.0.0.1  # master的ip
master_port:6379  # master的host
master_link_status:up  # master的狀態,如果master線上,那麼為up,否則是down
master_last_io_seconds_ago:8
master_sync_in_progress:0
slave_repl_offset:1
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6380> 
127.0.0.1:6380> get name  # 此時再次獲取name,發現為nil?為什麼,我們之前不是設定name了嗎?原因下面分析
(nil)
127.0.0.1:6380> 

一旦執行了slaveof(由於slave這個單詞存在歧視,所以Redis中也可以使用replicaof,是等價的),那麼這臺Redis主節點就變成了其它主節點的從節點,然後從節點上的資料會被清空,主節點將自身的資料副本同步給從節點。儘管6381這個節點設定了name這個key,但是當它執行了slaveof之後就變成了從節點,所以自身的資料就沒清空了,而主節點又沒有name這個key,所以最後get name的結果為nil。

然後將6381、也就是slave2也設定成從節點。

127.0.0.1:6381> role  # 除了info replication之外,還可以使用role來檢視,得到結果更精簡一些。
1) "master"  # 此時是master
2) (integer) 1161
3) (empty list or set)
127.0.0.1:6381> slaveof 127.0.0.1 6379  # 設定為從節點
OK
127.0.0.1:6381> role
1) "slave"  # 顯示變成了slave
2) "127.0.0.1"  # 主節點的ip
3) (integer) 6379  # 主節點的埠
4) "connected"  # 連線狀態,此時表示連線
5) (integer) 1189
127.0.0.1:6381> 

最後再來看看主節點master的狀態:

127.0.0.1:6379> role  # 檢視角色
1) "master"  # 顯示為master
2) (integer) 1329 
3) 1) 1) "127.0.0.1"  # 下面是兩位從節點的資訊
      2) "6380"
      3) "1329"
   2) 1) "127.0.0.1"
      2) "6381"
      3) "1329"
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379> info replication  # 使用更詳細的命令檢視
# Replication
role:master  # 角色是master
connected_slaves:2  # 連線的從節點有兩個
slave0:ip=127.0.0.1,port=6380,state=online,offset=1329,lag=1  # 從節點的資訊
slave1:ip=127.0.0.1,port=6381,state=online,offset=1343,lag=0
master_repl_offset:1343
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:1342
127.0.0.1:6379> 

主從同步是如何工作的

主從同步分為兩種:全量同步和增量同步。

全量同步

"從伺服器"傳送一個SYNC命令,不管它是第一次連線還是再次連線都無所謂,然後"主伺服器"會執行bgsave進行後臺儲存生成RDB檔案,並且開始快取新連線進來的修改資料的命令。當主伺服器完成後臺儲存後,主伺服器會把RDB檔案傳送給從伺服器,從伺服器將其儲存在磁碟上,然後載入到記憶體中。然後主伺服器會再把剛才快取的命令傳送到從伺服器,這是作為命令流來完成的,並且和Redis協議本身格式相同。

增量同步

在 Redis 2.8 之前每次從伺服器離線再重新上線之前,主伺服器會進行一次完整的資料同步,然後這種情況如果發生在離線時間比較短的情況下,只有少量的資料不同步卻要同步所有的資料是非常笨拙和不划算的,在 Redis 2.8 這個功能得到了優化。

Redis 2.8 的優化方法是當從伺服器離線之後,主伺服器會把離線之後的寫入命令,儲存在一個特定大小的佇列中,佇列是可以保證先進先出的執行順序的,當從伺服器重寫恢復上線之後,主服務會判斷離線這段時間內的命令是否還在佇列中,如果在就直接把佇列中的資料傳送給從伺服器,這樣就避免了完整同步的資源浪費。儲存離線命令的佇列大小預設是 1MB,使用者可以自行修改佇列大小的配置項 repl-backlog-size。

無盤資料同步

從前面的內容我們可以得知,在第一次主從連線的時候,會先產生一個 RDB 檔案,再把 RDB 檔案傳送給從伺服器,如果主伺服器是非固態硬碟的時候,系統的 I/O 操作是非常高的,為了緩解這個問題,Redis 2.8.18 新增了無盤複製功能,無盤複製功能不會在本地建立 RDB 檔案,而是會派生出一個子程序,然後由子程序通過 Socket 的方式,直接將 RDB 檔案寫入到從伺服器,這樣主伺服器就可以在不建立RDB檔案的情況下,完成與從伺服器的資料同步。要使用該功能,只需把配置項 repl-diskless-sync 的值設定為 yes 即可,它預設配置值為 no。

測試同步功能

我們在主機設定key,然後看看在從機上能不能獲取得到。

127.0.0.1:6380> get name
(nil)  # 顯然此時從機是沒有name這個key的
127.0.0.1:6380> 
127.0.0.1:6379> set name nana
OK     # 在主機中設定name
127.0.0.1:6379> 
127.0.0.1:6380> get name  # 在從機中進行獲取
"nana"
127.0.0.1:6380> # 同理我們在6381上獲取也是獲取的到的

我們知道一旦成為從機之後,自身的資料就會被情況,然後同步主機的資料,因為此時已經和指定的master建立聯絡了。

127.0.0.1:6380> slaveof 127.0.0.1 6379  # 如果繼續連線,會提示我們已經連線到指定的master了
OK Already connected to specified master
127.0.0.1:6380> 

但是問題來了, 如果我們在從機上寫資料,會不會同步到主機上面呢?

127.0.0.1:6380> set name mea
(error) READONLY You can't write against a read only slave.
127.0.0.1:6380> # 答案是根本不允許在從機上進行寫操作

一旦成為從機,那麼它就不能寫入資料了,只能老老實實地從主機備份資料。所以在預設情況下,處於複製模式的主伺服器(或者主機)既可以執行寫操作也可以執行讀操作,而從伺服器(或者從機)則只能執行讀操作。如果想讓從機也支援寫操作,那麼設定config set replica-read-only no即可,或者在配置檔案中修改,便可以使從伺服器也開啟寫操作,但是需要注意以下幾點:

  • 在從伺服器上寫的資料不會同步到主伺服器;
  • 當鍵值相同時主伺服器上的資料可以覆蓋從伺服器;
  • 在進行完整資料同步時,從伺服器資料會被清空。

關閉主從同步

我們看到實現主從同步還是很簡單的,就是指定一個slaveof,可以在配置檔案中指定,可以在啟動時通過命令列的方式指定,還可以在進入控制檯的時候設定。當然如果主機設定了密碼,那麼從機也要指定masterauth,整體沒什麼難度。

那麼如何關閉主從同步呢?關閉主從同步有兩種方式:第一種是通過slave <other_master_ip> <other_master_port>,將該從節點指向其它的主節點,但是該機器依舊是master;另一種方式是通過slaveof no one,這種方式就是關閉主從同步,然後該機器也會有slave變成master。

127.0.0.1:6380> slaveof 127.0.0.1 6666  # 隨便指向一個節點
OK
127.0.0.1:6380> info replication
# Replication
role:slave  # 依舊是slave
master_host:127.0.0.1
master_port:6666
master_link_status:down  # 但是master不是up,而是down,因為6666是我們隨便指定的
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_repl_offset:1
master_link_down_since_seconds:1595144745
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6380> 
127.0.0.1:6381> slaveof no one  # 取消主從同步
OK
127.0.0.1:6381> role
1) "master"  # 此時變成了master
2) (integer) 4101
3) (empty list or set)
127.0.0.1:6381> 
127.0.0.1:6379> role  # 6379依舊是master,但是它的下面已經沒有slave了。
1) "master"
2) (integer) 4143
3) (empty list or set)
127.0.0.1:6379> 

小結

這一節我們介紹了主從同步,使用了叢集的方式,當然我們這裡只有一臺機器,算是偽叢集吧。不過它的操作方式和真正的叢集是一樣的,只需要設定好master的ip和port即可。

另外,由於我在演示叢集的時候使用的不是docker,而是宿主機的Redis,原因就是容器對應的控制檯顯示的全部是6379,而宿主機顯示的是6379、6380、6381,會更直觀一些,可以直接就知道模擬的是哪一個節點。而我當前機器的Redis版本不高,所以使用的命令是slaveof,但是在 Redis 5.0 之後複製命令被改為了 replicaof,所以在高版本(Redis 5+)中我們應該儘量使用 replicaof,因為 slaveof 命令可能會被隨時廢棄掉(目前的話slaveof還是支援的)