搭建Redis“主-从-从”模式集群并使用 RedisTemplate 实现读写分离

news/2024/10/6 13:31:51

一、理论相关

我们知道,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 nobind 0.0.0.0save 900 1
save 300 10
save 60 10000rdbcompression yesdbfilename dump.rdbdir /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 nobind 0.0.0.0save 900 1
save 300 10
save 60 10000rdbcompression yesdbfilename dump.rdbdir /data# 关闭 aof 日志备份
appendonly no# 启动端口
port 6479# 将当前 redis 作为 redis0 的 slave
# 由于 docker 使用 host 模式,使用的是宿主机的 ip
replicaof 192.168.88.130 6379# 自定义密码
requirepass root# 访问 master 节点时需要提供的密码
masterauth rootmasteruser redis0replica-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 nobind 0.0.0.0save 900 1
save 300 10
save 60 10000rdbcompression yesdbfilename dump.rdbdir /data# 关闭 aof 日志备份
appendonly no# 启动端口
port 6579# 将当前 redis 作为 redis0 的 slave
# 由于 docker 使用 host 模式,使用的是宿主机的 ip
replicaof 192.168.88.130 6379# 自定义密码
requirepass root# 访问 master 节点时需要提供的密码
masterauth rootreplica-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 nobind 0.0.0.0save 900 1
save 300 10
save 60 10000rdbcompression yesdbfilename dump.rdbdir /data# 关闭 aof 日志备份
appendonly no# 启动端口
port 6679# 将当前 redis 作为 redis2 的 slave
# 由于 docker 使用 host 模式,使用的是宿主机的 ip
replicaof 192.168.88.130 6579# 自定义密码
requirepass root# 访问 master 节点时需要提供的密码
masterauth rootreplica-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 nobind 0.0.0.0save 900 1
save 300 10
save 60 10000rdbcompression yesdbfilename dump.rdbdir /data# 关闭 aof 日志备份
appendonly no# 启动端口
port 6779# 将当前 redis 作为 redis2 的 slave
# 由于 docker 使用 host 模式,使用的是宿主机的 ip
replicaof 192.168.88.130 6579# 自定义密码
requirepass root# 访问 master 节点时需要提供的密码
masterauth rootreplica-announce-ip 192.168.88.130

接下来,我们在目录redis-cluster下新建文件docker-compose.yml

services:redis0:image: rediscontainer_name: redis0restart: alwaysprivileged: truenetwork_mode: "host"volumes:- /root/docker/redis-cluster/redis0/data:/data- /root/docker/redis-cluster/redis0/redis.conf:/etc/redis.confcommand:redis-server /etc/redis.confredis1:image: rediscontainer_name: redis1restart: alwaysprivileged: truenetwork_mode: "host"volumes:- /root/docker/redis-cluster/redis1/data:/data- /root/docker/redis-cluster/redis1/redis.conf:/etc/redis.confcommand:redis-server /etc/redis.confdepends_on:- redis0redis2:image: rediscontainer_name: redis2restart: alwaysprivileged: truenetwork_mode: "host"volumes:- /root/docker/redis-cluster/redis2/data:/data- /root/docker/redis-cluster/redis2/redis.conf:/etc/redis.confcommand:redis-server /etc/redis.confdepends_on:- redis0redis3:image: rediscontainer_name: redis3restart: alwaysprivileged: truenetwork_mode: "host"volumes:- /root/docker/redis-cluster/redis3/data:/data- /root/docker/redis-cluster/redis3/redis.conf:/etc/redis.confcommand:redis-server /etc/redis.confdepends_on:- redis2redis4:image: rediscontainer_name: redis4restart: alwaysprivileged: truenetwork_mode: "host"volumes:- /root/docker/redis-cluster/redis4/data:/data- /root/docker/redis-cluster/redis4/redis.conf:/etc/redis.confcommand:redis-server /etc/redis.confdepends_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:(可读可写)

redis1、redis2:(可读不可写)
并且我们发现,redis1和redis2进行了主从库同步操作,即使我们没有在redis1和redis2中写入name:Monica,但它们和redis0建立连接后,主库会将数据同步给从库

redis3、redis4作为redis2的从库,同理,包含redis2的所有数据。

从RDM中我们也可以直观地看出,我们只对主库进行了一次写操作,但其连接的所有从库(包括从库的从库)都包含了这个数据:

通过以上验证表明:redis 的“主-从-从”模式集群已经搭建成功。

三、RedisTemplate 操作 Redis 集群实现读写分离

1、新建项目

我们新建一个SpringBoot项目,项目结构如下:

  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.130port: 6379password: rootjedis: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 节点全部停掉,然后看看是否能够读取成功@Beanpublic LettuceClientConfigurationBuilderCustomizer redisClientConfig() {//配置 redisTemplate 优先从 slave 节点读取数据,如果 slave 都宕机了,则从 master 读取return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);//配置 redisTemplate 优先从 slave 节点读取数据,如果 slave 都宕机了,则抛出异常//return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA);}@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();//默认的Key序列化器为:JdkSerializationRedisSerializerredisTemplate.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 {@Autowiredprivate RedisTemplate redisTemplate;@Testvoid writeTest() {redisTemplate.opsForValue().set("name", "Ross");}@Testvoid 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()测试类,发现报错:

  1. 接下来,我们启动redis3或redis4中的任意一个:

我们发现,如果主节点和从节点全部宕机,只要启动其中一个从节点,主节点就会同时启动

  1. 我们再次启动测试类getTest():
此时已经可以读取了。

说明 RedisTemplate 就是从 Slave 节点中读取数据的。

测试完毕。


个人问题记录:
在进行部署后发现主从库连接失败,详情如下:

redis0:

redis1:

通过docker logs redis0查看日志,排查错误后发现是端口6379被占用。因为在之前我部署过单机redis,使用了端口6379,但没有将其kill,导致端口被占用
本人采用最粗暴的方法就是直接把容器rm了^^


参考博文:
Redis 主从集群搭建并使用 RedisTemplate 实现读写分离

参考书籍:
《Redis核心技术与实战》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/68305.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

折腾笔记[2]-跨平台打包tauri程序

在macOS(arm64)平台打包tauri程序到Windows(amd64)平台. Packaging a Tauri application for the Windows (amd64) platform from macOS (arm64).摘要 在macOS(arm64)平台打包tauri程序到Windows(amd64)平台. Abstract Packaging a Tauri application for the Windows (amd64) …

博客格式-Markdown学习

标题 (#+空格+标题名字 一级标题) (##+空格+标题名字 二级标题) (###+空格+标题名字 三级标题) 字体 加粗 斜体 加粗斜体 划线 引用名人名言分割线图片超链接 百度 表格姓名 年龄 性别骆同学 15 男代码 int main(){cout<<"helloworld"return 0; }a,n=lis…

Java内存模型

1. 硬件的效率与一致性 物理机遇到的并发问题与虚拟机中的情况有很多相似之处,物理机对并发的处理方案对虚拟机的实现也有相当大的参考意义。 “让计算机并发执行若干个运算任务”与“更充分地利用计算机处理器的效能”之间的因果关系,看起来理所当然,实际上它们之间的关系并…

vue3 computed

computed 的作用就是监测变量是否发生改变。如果变量发生了改变,那么computed定义的方法就会执行。 在vue3中computed新增get 和set方法。分别对应修改和设置值

洪海洋的博客自我介绍

欢迎来到洪海洋的博客 我个人的基本信息 1.你的姓名? 如标题所示,洪海洋。英文名则是OCEAN,“海洋”,这一般也会作为我的网名。 2.为什么起这样的名字? emmm...五行缺水 3.描述一下自己? 多元、社恐、耐心 4.为什么这样描述自己? 对于我来说,多元包含很多个领域,比如我…

树上深度和问题 - 换根DP

问题引出: 给出 \(n\) 个点的树,求出分别以不同的 \(i\) 为根时,所有结点深度的和,根节点的深度为 \(0\)。 首先我们有个自然的暴力思路, 也就是以每个节点为根节点做一遍 \(dfs\) 这样的复杂度是 \(O(n^2)\) 级别的, 所以要进行优化 看下图:我们首先假设每个节点具有点权, …

珂朵莉树(ODT)

前言 主要是一种暴力思想。。。 本文来自 wiki 与洛谷题解的整合。 应用 主要是应付随机数据(区间操作) 实现 有几个核心操作。 set实现方法 定义 struct node {intt l,r;//intt:long longmutable intt v;node(const intt &ll,const intt &rr,const intt &vv) : …