Redis篇-19--运维篇1-主从复制(主从复制,读写分离,配置实现,实战案例)
1、概述
Redis的主从复制(Master-Slave Replication)是一种数据冗余机制,它允许将一台Redis服务器的数据复制到其他Redis服务器。在主从复制中,有一台主服务器(Master)和一个或多个从服务器(Slave)。主服务器负责写操作,而从服务器可以用于读操作,从而实现读写分离,减轻主服务器的负载压力。
示例图:
2、主从复制中的基本概念
- 主服务器(Master):负责处理写操作(如SET、DEL等),并将这些操作同步给从服务器。
- 从服务器(Slave):只读服务器,默认情况下不允许写操作(可以通过配置修改)。从服务器会定期从主服务器获取最新的数据更新,并保持与主服务器的数据一致。
- 数据流向:数据的复制是单向的,只能由主服务器到从服务器。从服务器不能将数据写回主服务器。
3、主从复制的作用
- 数据冗余:通过主从复制,可以在多台服务器上保存相同的数据副本,提供热备份,防止因单点故障导致数据丢失。
- 故障恢复:当主服务器出现问题时,可以从服务器可以接管服务,确保系统的高可用性。
- 负载均衡:通过读写分离,主服务器处理写操作,从服务器处理读操作,分担主服务器的负载,提升系统的并发处理能力。
- 高可用性基础:主从复制是Redis集群和哨兵(Sentinel)系统的基础,支持自动故障转移和高可用性部署。
4、主从复制的工作原理
Redis的主从复制分为两个主要阶段:
初次同步(全量同步)和命令传播(增量同步)。
(1)、初次同步(Full Resynchronization)
初次同步发生条件:
- 第一次建立主从关系:当从服务器首次连接到主服务器时,需要进行全量同步,以确保从服务器拥有与主服务器完全一致的数据集。
- 部分同步失败:如果从服务器与主服务器之间的连接中断时间过长,导致无法通过部分同步恢复数据一致性,也会触发全量同步。
全量同步(初始同步)的过程:
1、从服务器发起请求:从服务器通过配置文件的slaveof(或replicaof)检查自身需要同步的主节点信息。之后通过PSYNC命令携带replid(唯一身份标识符),offset(处理数据的偏移量)参数向主服务器发起数据同步请求。
2、主服务器确认请求:主服务器会校验从服务器的replid是否存在,不存在即表示第一次同步。主服务器会返回replid和offset和从服务器建立连接。
3、主服务器生成RDB文件:同时,主服务器会在后台执行BGSAVE操作,生成一个RDB快照文件。同时,主服务器会开启一个缓冲区,记录从BGSAVE期间的所有写命令。
4、传输RDB文件:BGSAVE完成后,主服务器将生成的RDB文件发送给从服务器。
5、加载RDB文件:从服务器接收到RDB文件后,会清空本地数据,加载RDB文件,重建与主服务器一致的数据集。
6、重放命令:从服务器加载完RDB文件后,主服务器会将缓冲区中的写命令发送给从服务器,确保数据完全一致。
全量过程原理示例如下:
简单总结:
首次同步是从服务发起,之后RDB文件同步,缓冲区写日志同步,都是主服务主动发起给从服务器的。
(2)、命令传播(Partial Resynchronization)
初次同步完成后,主服务器和从服务器之间会进入命令传播阶段。在这个阶段,主服务器会将所有的写命令实时发送给从服务器,确保两者的数据保持一致。
命令传播(增量同步)的过程:
1、主服务器记录命令:每当主服务器执行写命令时,它会将这些命令记录到一个缓冲区中。
2、从服务器请求命令:从服务器会定期向主服务器发送psync命令,告知主服务器它已经处理到的命令的偏移量offset。
3、主服务器发送命令:主服务器根据从服务器的偏移量offset,将未处理的命令发送给从服务器。
4、从服务器执行命令:从服务器接收到命令后,会立即执行这些命令,确保与主服务器的数据保持一致。
简单总结:
命令传播是从服务发起(携带自己身份和偏移量),主服务根据偏移量获取缓冲区写日志记录,发送给从服务器,这个过程是从服务主动发起数据请求的。
(3)、部分同步(Partial Resynchronization)
Redis从2.8版本开始引入了部分同步机制,以减少全量同步的频率。部分同步允许从服务器在与主服务器断开连接后,重新连接时只获取断开期间丢失的命令,而不是重新进行全量同步。其目的是为了优化全量同步的性能问题,减少全量同步的频率。
部分同步的过程:
1、从服务器保留复制偏移量offset:从服务器会记录最后一次成功同步的命令偏移量(offset)以及主服务器的replid(唯一标识符)。
2、从服务器发起部分同步请求:当从服务器重新连接到主服务器时,它会通过PSYNC 命令请求部分同步。
3、主服务器检查缓冲区:主服务器会检查它的复制积压缓冲区(Replication Backlog),判断是否包含从服务器请求的命令。
4、发送命令:如果缓冲区中包含从服务器请求的命令,主服务器会将这些命令发送给从服务器,完成部分同步;否则,主服务器会触发全量同步。
5、复制积压缓冲区(Replication Backlog)
复制积压缓冲区是一个固定大小的循环缓冲区,主服务器会将所有写命令记录到这个缓冲区中。它主要用于支持部分同步机制,确保从服务器在断开连接后能够快速恢复数据一致性。
- 默认大小:1MB(可以通过repl-backlog-size参数调整)。
- 作用:当从服务器与主服务器短暂断开连接时,主服务器可以通过复制积压缓冲区将断开期间的命令发送给从服务器,避免频繁的全量同步。
6、主从复制的优缺点
优点:
- 数据冗余:提供热备份,防止数据丢失。
- 高可用性:支持故障恢复和自动故障转移(结合 Redis Sentinel 使用)。
- 负载均衡:通过读写分离,减轻主服务器的负载,提升系统性能。
- 简单易用:配置简单,易于维护。
缺点:
- 数据一致性问题:由于主从复制是异步的,从服务器可能会存在一定的延迟,导致主从数据不完全一致。
- 单点故障:如果主服务器发生故障且没有及时切换到从服务器,可能会导致服务中断。
- 网络带宽消耗:主服务器需要将写命令实时发送给从服务器,可能会占用较多的网络带宽。
7、主从复制的优化建议
1、启用持久化:为了防止主服务器崩溃后数据丢失,建议为主服务器启用持久化( RDB或AOF),并定期备份数据。
2、合理配置复制积压缓冲区:根据业务需求调整复制积压缓冲区的大小,确保从服务器在断开连接后能够快速恢复。
3、监控主从延迟:定期监控主从服务器之间的延迟,确保从服务器能够及时同步主服务器的数据。
4、使用哨兵系统:结合Redis Sentinel系统,实现自动故障检测和主从切换,提升系统的高可用性。
5、限制从服务器的数量:过多的从服务器可能会增加主服务器的负担,建议根据实际需求合理配置从服务器的数量。
8、配置示例
1、主节点配置
第一步:配置ip和port
第二步:指定配置文件启动redis服务:
启动命令如:
redis-server.exe redis.windows.conf
2、从节点配置
第一步:配置ip和port
第二步:配置密码
如果主服务器设置了密码,从服务需要配置主服务器的密码
masterauth master_password
第三步:配置从节点所属主节点的信息
注意:5.0版本后的redis使用replicaof替代了slaveof。配置和功能是相同的。
第四步:指定配置文件启动从服务redis
如:redis-server.exe redis.windows.conf
启动从节点后,可以在日志中看到主从复制的相关日志,如下图:
3、验证主从复制
如下图:左边为主节点redis-cli,右边为从节点redis-cli。
主节点设置aaa1的key值,从节点没有设置该key。
在从节点redis-cli中,直接查看aaa1的key信息,可以正确访问。
说明主从同步关系配置是正确且正常工作的。
9、代码实现
1、导入依赖
说明下:springboot自带了LettuceConnectionFactory连接工厂,无需再次引入依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、配置类,注入工厂
这里直接在配置类中写死了配置,如果真实开发建议把配置放到配置文件中。
需要注入主节点连接工厂(用于写操作)和从节点连接工厂数组(用于读操作)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
@Configuration
public class RedisConfig {
// 主服务器连接工厂
@Bean(name = "masterConnectionFactory")
public LettuceConnectionFactory masterConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("127.0.0.1", 6379);
// config.setPassword("your_master_password"); // 如果主服务器启用了密码保护
return new LettuceConnectionFactory(config);
}
@Bean(name = "slaveConnectionFactories")
public RedisConnectionFactory[] slaveConnectionFactories() {
RedisStandaloneConfiguration slave1Config = new RedisStandaloneConfiguration("127.0.0.1", 6380);
// slave1Config.setPassword("your_master_password"); // 如果服务器启用了密码保护
RedisStandaloneConfiguration slave2Config = new RedisStandaloneConfiguration("127.0.0.1", 6381);
// slave2Config.setPassword("your_master_password"); // 如果服务器启用了密码保护
return new RedisConnectionFactory[]{
new LettuceConnectionFactory(slave1Config),
new LettuceConnectionFactory(slave2Config)
};
}
}
3、自定义注解
用于标注redis操作的方法上,指明该方法走写连接还是读连接。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadWrite {
boolean read() default true; // 默认为读操作
}
4、定义AOP,实现注解类的功能
AOP用于监听Redis操作方法上的自定义注解内容,将读写的标识保存到线程的ThreadLocal中。之后可以根据这里的标识切换读写连接工厂,从而实现读写分离的效果。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ReadWriteAspect {
// 线程局部变量,用于存储当前操作是否为读操作
private static final ThreadLocal<Boolean> READ_OPERATION = new ThreadLocal<>();
@Around("@annotation(ReadWrite)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取方法上的 @ReadWrite注解
ReadWrite readWrite = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(ReadWrite.class);
try {
// 设置线程局部变量,指示当前操作是读还是写
setReadOperation(readWrite.read());
// 执行方法
return joinPoint.proceed();
} finally {
// 清除线程局部变量
setReadOperation(null); // 清除ThreadLocal的值
}
}
// 设置线程局部变量,指示当前操作是读还是写
public static void setReadOperation(Boolean read) {
READ_OPERATION.set(read); // 保存到ThreadLocal中,用于之后切换数据工厂来源
}
// 获取线程局部变量,判断当前操作是否为读操作
public static Boolean isReadOperation() {
return READ_OPERATION.get();
}
}
5、读写连接切换实现类
实现RedisConnectionFactory类,并标明@Primary方法,相当于告诉spring容器,在所有容器中的redis连接工厂里,以当前工厂为主。
即:redisTemplate默认使用这个工厂创建redis连接实例,这里我们复写getConnection方法,用于读写连接的自动切换(实现:则是通过ThreadLocal中保存的读写标识)。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisClusterConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisSentinelConnection;
import org.springframework.stereotype.Component;
import java.util.Random;
@Component
@Primary // 标记为默认的连接工厂
public class ReadWriteSplittingConnectionFactory implements RedisConnectionFactory {
@Autowired
@Qualifier("masterConnectionFactory")
private RedisConnectionFactory masterConnectionFactory; // 主服务器连接工厂
@Autowired
private RedisConnectionFactory[] slaveConnectionFactories; // 从服务器连接工厂列表
private final Random random = new Random();
@Override
public org.springframework.data.redis.connection.RedisConnection getConnection() {
if (isReadOperation()) { // 根据线程局部变量ThreadLocal中判断是读还是写
// 在从服务器数组中随机选择一个连接
int index = random.nextInt(slaveConnectionFactories.length);
return slaveConnectionFactories[index].getConnection();
} else {
// 写操作使用主服务器连接
return masterConnectionFactory.getConnection();
}
}
// 判断当前操作是否为读操作
private boolean isReadOperation() {
// 获取线程局部变量,指示当前操作是读还是写
Boolean isRead = ReadWriteAspect.isReadOperation();
return isRead != null && isRead;
}
@Override
public boolean getConvertPipelineAndTxResults() {
return masterConnectionFactory.getConvertPipelineAndTxResults();
}
@Override
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
return masterConnectionFactory.translateExceptionIfPossible(ex);
}
@Override
public RedisClusterConnection getClusterConnection() {
return null;
}
@Override
public RedisSentinelConnection getSentinelConnection() {
return null;
}
}
6、定义redis的工具类,使用注解
编写Redis公共方法,使用自定义注解标识读写操作。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class RedisService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 写操作
@ReadWrite(read = false) // 相当于指定了写的连接工厂创建连接
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
// 读操作
@ReadWrite(read = true) // 相当于指定了读的连接工厂创建连接
public String get(String key) {
return (String) redisTemplate.opsForValue().get(key);
}
}
7、测试类
编写接口,调用Redis的读写方法,验证
import com.zw.base.BaseController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "redis", method = {RequestMethod.POST, RequestMethod.GET})
public class RedisController extends BaseController {
@Autowired
private RedisService redisService;
@RequestMapping("/set")
public String setTest() {
redisService.set("aaa3", "zhangsan3");
return null;
}
@RequestMapping("/get")
public String getTest() {
String aaa2 = redisService.get("aaa2");
System.out.println("aaa2:" + aaa2);
return aaa2;
}
}
8、验证结果
测试类中的set和get方法都正常使用。
但是这并不能直接看出读写到底走的那一个redis服务。这里可以断点看一下,当调用set方法时,可以看到走的逻辑是主节点的连接。
当调用get方法,如下可以看到,走的逻辑是从节点中任意一个节点的连接。
如上的验证即可说明,已经达到了读写分离的效果。
学海无涯苦作舟!!!