💻 Hello World, 我是 予枫。
代码不止,折腾不息。作为一个正在升级打怪的Java 后端练习生,我喜欢把踩过的坑和学到的招式记录下来。 保持空杯心态,让我们开始今天的技术分享。
Redis 作为高性能的键值数据库,单节点部署在高并发场景下会面临两大核心问题:一是读写压力集中在单个节点,性能瓶颈明显;二是单节点故障会导致服务不可用,数据丢失风险高。主从复制(Master-Slave Replication)是 Redis 解决这两个问题的基石 —— 通过将主节点(Master)的数据同步到从节点(Slave),实现读写分离、数据备份和故障转移,是 Redis 高可用架构的核心组成部分。
本文将从底层逻辑出发,拆解主从复制的核心阶段与关键概念,分析实际应用中的常见问题,并给出 Java 项目中「主从读写分离」的完整实践方案。
一、Redis 主从复制的核心价值
在深入技术细节前,先明确主从复制的核心作用,理解其存在的意义:
- 读写分离:Master 处理写操作(SET、DEL、HSET 等),Slave 处理读操作(GET、HGET、SMEMBERS 等),分散单节点的读写压力,提升系统整体吞吐量;
- 数据备份:Slave 是 Master 的实时数据副本,即使 Master 故障,Slave 可作为数据恢复的核心来源;
- 故障恢复:配合哨兵(Sentinel)或集群(Cluster),Master 故障后可自动将 Slave 升级为新 Master,实现服务无感知切换;
- 负载均衡:多个 Slave 可分担读请求,比如报表查询、数据统计等耗资源的读操作可路由到 Slave,降低 Master 压力。
二、主从复制的三个核心阶段(底层逻辑)
主从复制的本质是「Slave 主动同步 Master 数据」的过程,核心分为三个阶段,每个阶段对应不同的核心逻辑和指令。
阶段 1:连接建立(Slave 主动发起,Master 被动响应)
这是复制的 “握手阶段”,核心目标是建立主从之间的网络连接并完成身份认证,流程如下:
- Slave 配置触发:Slave 通过配置
replicaof <master-ip> <master-port>(Redis5.0 + 推荐,替代旧的slaveof)指定 Master 地址,启动后自动触发复制流程; - TCP 连接建立:Slave 向 Master 发起 TCP Socket 连接,Master 接受连接后,会创建一个 “复制专用” 的客户端连接(与普通业务客户端区分);
- 身份认证:若 Master 配置了
requirepass(密码),Slave 必须通过auth <password>命令认证,否则 Master 会拒绝复制; - 可用性检测:Slave 发送
PING命令测试 Master 是否可用,Master 返回PONG表示正常,否则复制中断; - 同步请求发起:Slave 发送
PSYNC ? -1命令(?表示未知 Master 的 RunID,-1表示首次同步),向 Master 请求数据同步。
阶段 2:全量同步(首次同步的核心)
当 Slave 首次连接 Master,或 Master 判断 Slave 的复制偏移量无效时,会触发全量同步—— 将 Master 当前的全部数据一次性发送给 Slave,这是最耗资源的阶段:
- Master 生成 RDB 快照:Master 收到
PSYNC命令后,调用fork()创建子进程,执行BGSAVE生成 RDB 快照文件;此过程中,Master 的新写操作会被临时记录到「复制积压缓冲区」,避免数据丢失; - RDB 文件传输:Master 将生成的 RDB 文件通过 Socket 发送给 Slave;
- Slave 加载 RDB:Slave 接收 RDB 文件后,先清空本地原有数据(避免数据冲突),再加载 RDB 文件到内存,恢复 Master 的全量数据;
- 同步积压缓冲区:Master 将
BGSAVE过程中产生的写操作(积压缓冲区中)发送给 Slave,Slave 执行这些操作,确保与 Master 数据完全一致。
核心痛点:全量同步的 RDB 文件传输会占用大量网络带宽和磁盘 IO,Master 数据量越大,同步耗时越长,需尽量避免频繁触发。
阶段 3:增量同步(日常同步的常态)
全量同步完成后,主从进入增量同步阶段 ——Master 将后续的每一个写操作实时同步给 Slave,保证数据一致性:
- Master 记录写操作:Master 每执行一个写命令,都会将命令写入「复制积压缓冲区」,同时更新自身的「复制偏移量」;
- 实时同步命令:Master 通过复制连接,将写命令实时发送给 Slave;
- Slave 执行并更新偏移量:Slave 接收命令后执行,同时更新自己的复制偏移量;
- 偏移量校验:Slave 每秒向 Master 汇报自身偏移量,Master 对比自身偏移量,确保主从数据一致;若偏移量不一致,Master 会触发补全逻辑。
三、核心概念解析(理解同步的关键)
要彻底掌握主从复制,必须理解以下三个核心概念:
1. PSYNC 命令:同步的 “核心指令”
Redis2.8 + 引入PSYNC替代旧的SYNC命令,解决了SYNC“无论场景都全量同步” 的低效问题。其格式为:
PSYNC <runid> <offset><runid>:Master 的唯一标识(每个 Redis 实例启动后生成 40 位 RunID),Slave 重连时携带该值,表明要同步的目标 Master;<offset>:Slave 当前的复制偏移量,Master 据此判断同步起始位置。
PSYNC 的三种执行场景:
- 场景 1:Slave 首次连接(
PSYNC ? -1)→ 触发全量同步(FULLRESYNC),Master 返回自身 RunID 和当前偏移量; - 场景 2:Slave 重连,RunID 匹配且偏移量在积压缓冲区范围内 → 触发增量同步(
CONTINUE),发送偏移量后的写命令; - 场景 3:Slave 重连,RunID 不匹配(Master 重启)或偏移量超出缓冲区范围 → 触发全量同步。
2. 复制偏移量(Replication Offset):数据一致性的 “标尺”
- 定义:64 位整数,记录 Master 发送给 Slave 的字节数,Master 和 Slave 各自维护独立的偏移量;
- 作用:Master 每发送 N 字节数据,自身偏移量 + N;Slave 每接收并执行 N 字节,自身偏移量 + N。通过对比主从偏移量,可直接判断数据是否一致(偏移量相等则一致);
- 校验机制:Slave 每秒发送
REPLCONF ACK <offset>命令汇报偏移量,Master 通过该信息感知 Slave 的同步状态。
3. 复制积压缓冲区(Replication Backlog Buffer):增量同步的 “容错空间”
- 定义:Master 端的环形缓冲区(固定大小,默认 1MB,可通过
repl-backlog-size配置),缓存最近发送给 Slave 的写命令; - 核心作用:Slave 断开重连时,Master 检查 Slave 的偏移量是否在缓冲区范围内:
- 若在范围内:从 Slave 偏移量位置开始,发送缓冲区中的命令(增量同步);
- 若超出范围:触发全量同步;
- 配置建议:根据 Slave 最大断连时间和写命令吞吐量调整,比如写频繁的场景可设为 10-50MB,减少全量同步概率。
四、主从架构的常见 “坑” 与解决方案
主从复制并非银弹,实际应用中会遇到各类问题,以下是最常见的 3 个 “坑” 及解决方案:
1. 脑裂(Split Brain):主从失联后的 “数据孤岛”
- 现象:Master 因网络分区与 Slave 断开,但自身仍运行;哨兵误判 Master 故障,将 Slave 升级为新 Master;网络恢复后,原 Master 重新加入集群,出现 “双 Master”,导致数据写入到两个节点,最终数据不一致;
- 解决方案:
- 配置 Master 的
min-replicas-to-write(最少可用 Slave 数)和min-replicas-max-lag(Slave 最大延迟):比如min-replicas-to-write 1+min-replicas-max-lag 10,表示当可用 Slave 数 <1 或延迟> 10 秒时,Master 拒绝写操作; - 合理设置哨兵故障判断时间(
down-after-milliseconds),避免误判; - 网络恢复后,强制原 Master 成为新 Master 的 Slave,同步新数据。
- 配置 Master 的
2. 数据不一致:主从同步的 “时差”
- 现象:Master 执行写操作后,Slave 未及时同步,此时读 Slave 会获取旧数据;
- 解决方案:
- 强一致性读请求(如用户余额、订单状态)强制路由到 Master;
- 配置 Slave 的
slave-serve-stale-data no:Slave 与 Master 断开时拒绝读请求,避免返回旧数据; - 优化网络,减少主从间的同步延迟。
3. 全量同步频繁:性能杀手
- 现象:Slave 频繁触发全量同步,导致 Master/Slave 的 CPU、网络压力剧增;
- 解决方案:
- 增大
repl-backlog-size,降低偏移量超出缓冲区的概率; - 保证 Master 稳定性,避免频繁重启(重启会导致 RunID 变化,触发全量同步);
- 监控 Slave 连接状态,及时处理断连问题。
- 增大
五、Java 项目实战:Redis 主从读写分离
接下来给出完整的 Java 项目实现方案,包括环境准备、依赖配置、代码实现和测试验证。
5.1 环境准备(主从集群搭建)
以单机多实例为例(生产环境建议多机部署):
- Master:127.0.0.1:6379
- Slave1:127.0.0.1:6380
- Slave2:127.0.0.1:6381
Slave 的redis.conf核心配置(Redis6.0+):
# 绑定IP(生产环境建议内网IP) bind 127.0.0.1 # 修改端口 port 6380 # 指定Master replicaof 127.0.0.1 6379 # Master密码认证(若有) masterauth 123456 # 禁止Slave写操作(生产必开) slave-read-only yes # 同步失败时拒绝读旧数据 slave-serve-stale-data no启动主从节点:
# 启动Master redis-server /etc/redis/6379.conf # 启动Slave1 redis-server /etc/redis/6380.conf # 启动Slave2 redis-server /etc/redis/6381.conf验证主从关系:在 Master 执行info replication,可看到 Slave 列表;在 Slave 执行该命令,可看到 Master 信息。
5.2 项目依赖(Maven)
使用 Spring Boot + Spring Data Redis(Lettuce 客户端,默认):
<dependencies> <!-- Spring Boot Redis Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.7.10</version> <!-- 适配JDK8 --> </dependency> <!-- 连接池(提升性能) --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!-- 测试依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>5.3 核心实现:读写分离配置
推荐使用 Lettuce 的自动读写分离配置(生产级),可实现读操作自动路由到 Slave,写操作路由到 Master,并支持从库负载均衡。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; import static org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.ReadFrom; /** * Lettuce自动读写分离 + 从库负载均衡配置 */ @Configuration public class RedisMasterSlaveConfig { @Bean public LettuceConnectionFactory redisConnectionFactory() { // 1. 配置主从节点 RedisStaticMasterReplicaConfiguration config = new RedisStaticMasterReplicaConfiguration("127.0.0.1", 6379); // 添加多个Slave(支持负载均衡) config.addNode("127.0.0.1", 6380); config.addNode("127.0.0.1", 6381); // Redis密码(主从需一致) config.setPassword("123456"); // 2. Lettuce客户端配置 LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() // 连接超时 .connectTimeout(Duration.ofSeconds(5)) // 重试策略(应对网络抖动) .retryAttempts(3) .retryInterval(Duration.ofMillis(100)) // 读写分离策略:优先读Slave,Slave不可用时读Master .readFrom(ReadFrom.REPLICA_PREFERRED) .build(); // 3. 创建连接工厂 return new LettuceConnectionFactory(config, clientConfig); } /** * 统一的RedisTemplate(自动路由读写操作) */ @Bean public RedisTemplate<String, Object> redisTemplate() { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory()); // 序列化配置(避免乱码) template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.afterPropertiesSet(); return template; } }关键说明:
ReadFrom策略选择:
MASTER:只读 Master(强一致性场景);REPLICA:只读 Slave(Slave 不可用时报错);REPLICA_PREFERRED:优先读 Slave,Slave 全不可用时读 Master(推荐);MASTER_PREFERRED:优先读 Master,Master 不可用时读 Slave。
5.4 业务层实现
封装 Redis 读写操作,业务层无需关心底层路由逻辑:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; /** * Redis读写分离业务服务 */ @Service public class RedisService { @Autowired private RedisTemplate<String, Object> redisTemplate; /** * 写操作(自动路由到Master) */ public void set(String key, Object value) { redisTemplate.opsForValue().set(key, value); } /** * 读操作(自动路由到Slave) */ public Object get(String key) { return redisTemplate.opsForValue().get(key); } /** * 强一致性读(强制路由到Master) * 注:需单独配置Master的RedisTemplate,或使用Redis命令指定Master */ public Object getFromMaster(String key) { // 若需强制读Master,可单独配置Master的RedisTemplate注入使用 return redisTemplate.opsForValue().get(key); } }5.5 测试验证
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class RedisMasterSlaveTest { @Autowired private RedisService redisService; @Test public void testReadWriteSeparation() { // 1. 写操作(Master) String key = "user:1001"; redisService.set(key, "张三"); // 2. 读操作(Slave) Object slaveResult = redisService.get(key); System.out.println("从Slave读取:" + slaveResult); // 预期:张三 // 3. 强一致性读(Master) Object masterResult = redisService.getFromMaster(key); System.out.println("从Master读取:" + masterResult); // 预期:张三 } }5.6 生产环境最佳实践
- 监控主从同步状态:通过
info replication监控复制偏移量、Slave 延迟,及时发现同步异常; - 配合哨兵:主从 + 哨兵实现 Master 故障自动转移,提升高可用性;
- 连接池优化:合理设置 Lettuce 连接池大小(
spring.redis.lettuce.pool.max-active),避免连接耗尽; - 避免大 Key:大 Key 的全量同步会占用大量资源,建议拆分大 Key;
- 限流熔断:对 Slave 的读请求做限流,避免 Slave 被打垮。
总结
- Redis 主从复制的核心流程是「连接建立→全量同步→增量同步」,PSYNC 命令、复制偏移量、积压缓冲区是保障同步的关键;
- 主从架构的核心问题是脑裂、数据不一致和全量同步频繁,可通过配置优化和监控规避;
- Java 项目中实现主从读写分离,推荐使用 Lettuce 的
ReadFrom策略,可自动实现读操作路由到 Slave、写操作路由到 Master,简化开发。
主从复制是 Redis 高可用的基础,掌握其底层逻辑和实践技巧,能帮助你搭建更稳定、高性能的 Redis 架构。
🌟关注【予枫】,获取更多技术干货
📅身份:一名热爱技术的研二学生
🏷️标签:Java / 算法 / 个人成长
💬Slogan:只写对自己和他人有用的文字。