搭建Redis“主-從-從”模式叢集並使用 RedisTemplate 實現讀寫分離

尘尘尘發表於2024-10-06

一、理論相關

我們知道,Redis具有高可靠性,其含義包括:

  1. 資料儘量少丟失 - AOF 和 RDB
  2. 服務儘量少中斷 - 增加副本冗餘量,將一份資料同時儲存在多個例項上,即主從庫模式

Redis主從庫模式 - 保證資料副本的一致(讀寫分離):

  1. 讀操作:主庫、從庫都可以接收
  2. 寫操作:首先到主庫執行,然後,主庫將寫操作同步給從庫
Redis主從庫和讀寫分離

採用讀寫分離的原因:

  1. 如果客戶端對同一個資料進行多次修改,每一次的修改請求都傳送到不同的例項上,在不同的例項上執行,那麼這個資料在多個例項上的副本就不一致了
  2. 如果要對不同例項上的資料一致,就涉及到加鎖、例項間協商是否完成修改等操作,會帶來鉅額的開銷

這時我們就引出主從庫同步的原理

1、主從庫間如何進行第一次同步?

當我們啟動多個 Redis 例項的時候,它們相互之間就可以透過 replicaof(Redis 5.0 之前使用 slaveof)命令形成主庫和從庫的關係,之後會按照三個階段完成資料的第一次同步。

  1. 主從庫建立連線、協商同步,為全量複製做準備
replicaof 172.16.19.3 6379
  • 從庫和主庫建立連線,並告訴主庫即將進行同步,主庫確認回覆後,主從庫間開始同步
  1. 主庫將所有資料同步給從庫。從庫收到資料後,在本地完成資料載入 - 依賴於記憶體快照生成的RDB檔案
  • 從庫接收到RDB檔案後,會先清空當前資料庫 - 從庫在透過replicaof命令開始和主庫同步前,可能儲存了其它資料
  • 主庫將資料同步給從庫的過程中,主庫不會被阻塞,仍然可以正常接收請求。為保證主從庫的資料一致性,主庫會在記憶體中用專門的 replication buffer,記錄 RDB 檔案生成後收到的所有寫操作
  1. 主庫把第二階段執行過程中新收到的寫命令,再傳送給從庫
主從庫第一次同步的流程
  • 所有的從庫都是和主庫連線,所有的全量複製都是和主庫進行的。

2、主從級聯模式分擔全量複製時的主庫壓力

一次全量複製中,對於主庫需要完成兩個耗時操作:

  1. 生成RDB檔案 - fork操作會阻塞主執行緒處理正常請求
  2. 傳輸RDB檔案 - 佔用主庫網路頻寬

至此,我們引出:“主 - 從 - 從”模式

  • 分擔主庫壓力
  • 將主庫生成RDB和傳輸RDB的壓力,以級聯的方式分散到從庫上
  • 部署主從叢集時手動選擇一個庫(比如選擇記憶體資源配置較高的從庫),用於級聯其它從庫
  • 在從庫執行命令replicaof 所選從庫IP 6379,建立主從關係
級聯的主從從模式
  • 主從庫間透過全量複製實現資料同步的過程,以及透過“主 - 從 - 從”模式分擔主庫壓力
  • 一旦主從庫完成了全量複製,它們之間就會一直維護一個網路連線,主庫會透過這個連線將後續陸續收到的命令操作再同步給從庫,這個過程也稱為基於長連線的命令傳播,可以避免頻繁建立連線的開銷。
  • 風險:網路斷聯或阻塞

3、主從庫間網路斷了怎麼辦?

在 Redis 2.8 之前,如果主從庫在命令傳播時出現了網路閃斷,那麼,從庫就會和主庫重新進行一次全量複製,開銷非常大。
從 Redis 2.8 開始,網路斷了之後,主從庫會採用增量複製的方式繼續同步。

  • 為避免環形緩衝區造成的主從庫不一致,可以調整repl_backlog_size引數
    • 緩衝空間大小 = 主庫寫入命令速度 * 操作大小 - 主從庫間網路傳輸命令速度 * 操作大小
    • 在實際應用中,考慮到可能存在一些突發的請求壓力,我們通常需要把這個緩衝空間擴大一倍,即 repl_backlog_size = 緩衝空間大小 * 2
    • 也可以採用切片叢集來分擔單個主庫的請求壓力

4、小結

  1. 全量複製
    • 一個Redis例項的資料庫不要太大,一個例項大小在幾GB級別比較合適,可以減少RDB檔案生成、傳輸和重新載入的開銷
    • 避免多個從庫同時和主庫進行全量複製,給主庫過大同步壓力 - “主-從-從”
  2. 基於長連線的命令傳播
  3. 增量複製
    • 留意repl_backlog_size配置引數

二、實踐

執行環境:虛擬機器作業系統:centOS7,IP地址:192.168.88.130
已經安裝好了 docker 和 docker-compose
採用Redis:7.4.0
至此,我們開始在虛擬機器中搭建Redis“主-從-從”模式的主從庫叢集

  1. 我們先建立好目錄:
[root@centos ~]# mkdir /root/docker/redis-cluster
[root@centos ~]# cd /root/docker/redis-cluster
[root@centos redis-cluster]# mkdir redis0
[root@centos redis-cluster]# mkdir redis1
[root@centos redis-cluster]# mkdir redis2
[root@centos redis-cluster]# mkdir redis3
[root@centos redis-cluster]# mkdir redis4

我們將redis0作為主庫

redis1和redis2作為從庫I和從庫II(slave),redis3和redis4作為從庫II的兩個從庫(主-從-從模式)

  1. redis0
[root@centos redis-cluster]# mkdir redis0/data
[root@centos redis-cluster]# vi redis0/redis.conf
protected-mode no

bind 0.0.0.0

save 900 1
save 300 10
save 60 10000

rdbcompression yes

dbfilename dump.rdb

dir /data

# 關閉 aof 日誌備份
appendonly no

# 自定義密碼
requirepass root

# 啟動埠
port 6379

# 換成自己的虛擬機器的IP
replica-announce-ip 192.168.88.130
  1. redis1
[root@centos redis-cluster]# mkdir redis1/data
[root@centos redis-cluster]# vi redis1/redis.conf
  • replicaof [主節點ip] [主節點埠] ,該配置主要是讓當前節點作為從節點,配置具體的主節點的地址和埠(Redis 5.0 之前使用 slaveof [主節點ip] [主節點埠]
  • masterauth [主節點的訪問密碼] ,該配置主要是在主節點設定密碼的情況下,能夠讓從節點透過密碼訪問主節點
protected-mode no

bind 0.0.0.0

save 900 1
save 300 10
save 60 10000

rdbcompression yes

dbfilename dump.rdb

dir /data

# 關閉 aof 日誌備份
appendonly no

# 啟動埠
port 6479

# 將當前 redis 作為 redis0 的 slave
# 由於 docker 使用 host 模式,使用的是宿主機的 ip
replicaof 192.168.88.130 6379

# 自定義密碼
requirepass root

# 訪問 master 節點時需要提供的密碼
masterauth root

masteruser redis0

replica-announce-ip 192.168.88.130
  1. redis2
[root@centos redis-cluster]# mkdir redis2/data
[root@centos redis-cluster]# vi redis2/redis.conf
protected-mode no

bind 0.0.0.0

save 900 1
save 300 10
save 60 10000

rdbcompression yes

dbfilename dump.rdb

dir /data

# 關閉 aof 日誌備份
appendonly no

# 啟動埠
port 6579

# 將當前 redis 作為 redis0 的 slave
# 由於 docker 使用 host 模式,使用的是宿主機的 ip
replicaof 192.168.88.130 6379

# 自定義密碼
requirepass root

# 訪問 master 節點時需要提供的密碼
masterauth root

replica-announce-ip 192.168.88.130
  1. redis3
[root@centos redis-cluster]# mkdir redis3/data
[root@centos redis-cluster]# vi redis3/redis.conf
protected-mode no

bind 0.0.0.0

save 900 1
save 300 10
save 60 10000

rdbcompression yes

dbfilename dump.rdb

dir /data

# 關閉 aof 日誌備份
appendonly no

# 啟動埠
port 6679

# 將當前 redis 作為 redis2 的 slave
# 由於 docker 使用 host 模式,使用的是宿主機的 ip
replicaof 192.168.88.130 6579

# 自定義密碼
requirepass root

# 訪問 master 節點時需要提供的密碼
masterauth root

replica-announce-ip 192.168.88.130
  1. redis4
[root@centos redis-cluster]# mkdir redis4/data
[root@centos redis-cluster]# vi redis4/redis.conf
protected-mode no

bind 0.0.0.0

save 900 1
save 300 10
save 60 10000

rdbcompression yes

dbfilename dump.rdb

dir /data

# 關閉 aof 日誌備份
appendonly no

# 啟動埠
port 6779

# 將當前 redis 作為 redis2 的 slave
# 由於 docker 使用 host 模式,使用的是宿主機的 ip
replicaof 192.168.88.130 6579

# 自定義密碼
requirepass root

# 訪問 master 節點時需要提供的密碼
masterauth root

replica-announce-ip 192.168.88.130

接下來,我們在目錄redis-cluster下新建檔案docker-compose.yml

services:
  redis0:
    image: redis
    container_name: redis0
    restart: always
    privileged: true
    network_mode: "host"
    volumes:
      - /root/docker/redis-cluster/redis0/data:/data
      - /root/docker/redis-cluster/redis0/redis.conf:/etc/redis.conf
    command:
      redis-server /etc/redis.conf

  redis1:
    image: redis
    container_name: redis1
    restart: always
    privileged: true
    network_mode: "host"
    volumes:
      - /root/docker/redis-cluster/redis1/data:/data
      - /root/docker/redis-cluster/redis1/redis.conf:/etc/redis.conf
    command:
      redis-server /etc/redis.conf
    depends_on:
      - redis0

  redis2:
    image: redis
    container_name: redis2
    restart: always
    privileged: true
    network_mode: "host"
    volumes:
      - /root/docker/redis-cluster/redis2/data:/data
      - /root/docker/redis-cluster/redis2/redis.conf:/etc/redis.conf
    command:
      redis-server /etc/redis.conf
    depends_on:
      - redis0

  redis3:
    image: redis
    container_name: redis3
    restart: always
    privileged: true
    network_mode: "host"
    volumes:
      - /root/docker/redis-cluster/redis3/data:/data
      - /root/docker/redis-cluster/redis3/redis.conf:/etc/redis.conf
    command:
      redis-server /etc/redis.conf
    depends_on:
      - redis2

  redis4:
    image: redis
    container_name: redis4
    restart: always
    privileged: true
    network_mode: "host"
    volumes:
      - /root/docker/redis-cluster/redis4/data:/data
      - /root/docker/redis-cluster/redis4/redis.conf:/etc/redis.conf
    command:
      redis-server /etc/redis.conf
    depends_on:
      - redis2
[root@centos redis-cluster]# vi docker-compose.yml
[root@centos redis-cluster]# docker-compose up -d
docker-compose

部署完成後,我們使用RDM連線部署的所有redis:

redis-connection

測試是否連線成功:
redis0:

redis0-replication

redis1:

redis1-replication

redis2:

redis2-replication

redis3、redis4同理。

測試五個主從庫讀寫操作:
redis0:(可讀可寫)

搭建Redis“主-從-從”模式叢集並使用 RedisTemplate 實現讀寫分離

redis1、redis2:(可讀不可寫)
並且我們發現,redis1和redis2進行了主從庫同步操作,即使我們沒有在redis1和redis2中寫入name:Monica,但它們和redis0建立連線後,主庫會將資料同步給從庫

搭建Redis“主-從-從”模式叢集並使用 RedisTemplate 實現讀寫分離
搭建Redis“主-從-從”模式叢集並使用 RedisTemplate 實現讀寫分離

redis3、redis4作為redis2的從庫,同理,包含redis2的所有資料。

從RDM中我們也可以直觀地看出,我們只對主庫進行了一次寫操作,但其連線的所有從庫(包括從庫的從庫)都包含了這個資料:

搭建Redis“主-從-從”模式叢集並使用 RedisTemplate 實現讀寫分離

透過以上驗證表明:redis 的“主-從-從”模式叢集已經搭建成功。

三、RedisTemplate 操作 Redis 叢集實現讀寫分離

1、新建專案

我們新建一個SpringBoot專案,專案結構如下:

搭建Redis“主-從-從”模式叢集並使用 RedisTemplate 實現讀寫分離
  1. 引入依賴
<!--Redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 配置application.yml檔案
spring:
    data:
        redis:
            # 這裡只需配置主節點的資訊即可
            # RedisTemplate可以從主節點資訊中獲取從節點資訊
            host: 192.168.88.130
            port: 6379
            password: root
            jedis:
                pool:
                    # 最大連線數
                    max-active: 10
                    # 最大空閒連線數
                    max-idle: 5
                    # 最小空閒
                    min-idle: 1
                    # 連線超時時間(毫秒)
                    max-wait: 8000
  1. RedisTemplate進行配置
package com.chen.redisdemo.redisConfig;

import io.lettuce.core.ReadFrom;
import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @version 1.0
 * @Author feiye
 * @Date 2024-10-06 12:15
 * @className RedisConfig
 * @since 1.0
 */
@Configuration
public class RedisConfig {

    //你可以將讀取策略,設定為 ReadFrom.REPLICA 表示只從 slave 節點讀取資料
    //然後你把 slave 節點全部停掉,然後看看是否能夠讀取成功
    @Bean
    public LettuceClientConfigurationBuilderCustomizer redisClientConfig() {
        //配置 redisTemplate 優先從 slave 節點讀取資料,如果 slave 都當機了,則從 master 讀取
        return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);

        //配置 redisTemplate 優先從 slave 節點讀取資料,如果 slave 都當機了,則丟擲異常
        //return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

        //預設的Key序列化器為:JdkSerializationRedisSerializer
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setEnableTransactionSupport(true);
        return redisTemplate;
    }
}
  1. 編寫測試類
package com.chen.redisdemo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

@SpringBootTest
class RedisDemoApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void writeTest() {
        redisTemplate.opsForValue().set("name", "Ross");
    }

    @Test
    void getTest() {
        Object name = redisTemplate.opsForValue().get("name");
        if (name != null) {
            System.out.println(name.toString());
        }
    }

}

2、如何證明 RedisTemplate 是從 Slave 節點中獲取資料的?

  1. 首先我們修改一下 RedisConfig 類中的配置,讓 RedisTemplate 只從 Slave 節點讀取資料,不從 master 節點讀取資料。
@Bean
public LettuceClientConfigurationBuilderCustomizer redisClientConfig() {
    //配置 redisTemplate 優先從 slave 節點讀取資料,如果 slave 都當機了,則丟擲異常
    return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA);
}
  1. 然後我們在 Linux 虛擬機器上,執行以下命令,停掉所有 Slave 節點服務:
[root@centos redis-cluster]# docker-compose stop redis3
[+] Stopping 1/1
 ✔ Container redis3  Stopped                                                                                                                                                                                 0.3s 
[root@centos redis-cluster]# docker-compose stop redis4
[+] Stopping 1/1
 ✔ Container redis4  Stopped                                                                                                                                                                                 0.2s 
[root@centos redis-cluster]# docker-compose stop redis2
[+] Stopping 1/1
 ✔ Container redis2  Stopped                                                                                                                                                                                 0.2s 
[root@centos redis-cluster]# docker-compose stop redis1
[+] Stopping 1/0
 ✔ Container redis1  Stopped

然後我們執行getTest()測試類,發現報錯:

搭建Redis“主-從-從”模式叢集並使用 RedisTemplate 實現讀寫分離
  1. 接下來,我們啟動redis3或redis4中的任意一個:
搭建Redis“主-從-從”模式叢集並使用 RedisTemplate 實現讀寫分離

我們發現,如果主節點和從節點全部當機,只要啟動其中一個從節點,主節點就會同時啟動

  1. 我們再次啟動測試類getTest():
搭建Redis“主-從-從”模式叢集並使用 RedisTemplate 實現讀寫分離
此時已經可以讀取了。

說明 RedisTemplate 就是從 Slave 節點中讀取資料的。

測試完畢。


個人問題記錄:
在進行部署後發現主從庫連線失敗,詳情如下:

redis0:

搭建Redis“主-從-從”模式叢集並使用 RedisTemplate 實現讀寫分離

redis1:

搭建Redis“主-從-從”模式叢集並使用 RedisTemplate 實現讀寫分離

透過docker logs redis0檢視日誌,排查錯誤後發現是埠6379被佔用。因為在之前我部署過單機redis,使用了埠6379,但沒有將其kill,導致埠被佔用
本人採用最粗暴的方法就是直接把容器rm了^^


參考博文:
Redis 主從叢集搭建並使用 RedisTemplate 實現讀寫分離

參考書籍:
《Redis核心技術與實戰》

相關文章