news 2026/5/7 8:33:46

Redis主从复制与数据固化-从原理到实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis主从复制与数据固化-从原理到实战

Redis 主从复制 + 数据固化:从"单机裸奔"到"高可用"的实战之路

最近帮一个兄弟排查线上 Redis 挂了导致缓存雪崩的问题,结果发现他们还在用单机 Redis 跑核心业务… 这让我意识到,Redis 的高可用和数据持久化,很多人其实没真正搞明白。今天咱们就聊聊 Redis 主从复制和数据固化,顺便把坑都踩一遍。


一、问题引入:单机 Redis 的"裸奔"风险

假设你正在做一个电商系统,Redis 里存着商品库存、用户会话、热点数据。一切看起来很美好,直到有一天…

场景一:半夜两点,Redis 挂了

[ERROR] Redis connection timeout... [ERROR] Cache miss rate: 99.9% [ERROR] DB connection pool exhausted...

所有请求直接打到 MySQL,数据库瞬间被打挂,老板的电话也来了。

场景二:运维小哥手滑,执行了FLUSHALL

127.0.0.1:6379>FLUSHALL OK# 整个世界都安静了...

几百万条缓存数据,说没就没。没有备份,没有副本,只有你和运维小哥面面相觑。

场景三:流量突增,单节点扛不住了

大促来了,QPS 从 5000 飙到 50000,Redis CPU 直接 100%,响应时间从 1ms 变成 1s。

说白了,单机 Redis 就像把所有鸡蛋放一个篮子里——篮子掉了,蛋全碎。

那怎么办?答案就是:主从复制 + 数据固化。咱们一个一个来。


二、Redis 主从复制:给 Redis 找个"备胎"

2.1 什么是主从复制?

主从复制(Master-Slave Replication)就是搞多个 Redis 实例,一个当"老大"(Master),负责写;其他当"小弟"(Slave),负责读。小弟实时同步老大的数据,老大挂了,小弟还能顶上。

┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Client │────────▶│ Master │◀───────│ Client │ │ (Write) │ │ (读写均可) │ │ (Read) │ └─────────────┘ └──────┬──────┘ └─────────────┘ │ ┌──────────┴──────────┐ │ Replication │ │ (全量/增量) │ └──────────┬──────────┘ │ ┌────────────────┼────────────────┐ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Slave 1 │ │ Slave 2 │ │ Slave N │ │ (只读) │ │ (只读) │ │ (只读) │ └─────────┘ └─────────┘ └─────────┘

2.2 主从复制的三种模式

模式说明适用场景
一主一从1 个 Master + 1 个 Slave数据备份,读写分离
一主多从1 个 Master + N 个 Slave读多写少,横向扩展读能力
树状复制Slave 下面再挂 Slave节点很多时,减轻 Master 压力

2.3 动手搭一个主从架构

咱们用 Docker 快速搭一个一主两从的环境,5 分钟搞定。

Step 1:准备配置文件

# 创建目录mkdir-predis-cluster/{master,slave1,slave2}# Master 配置cat>redis-cluster/master/redis.conf<<'EOF' port 6379 bind 0.0.0.0 daemonize no appendonly yes appendfilename "appendonly.aof" # 开启 RDB 持久化 save 900 1 save 300 10 save 60 10000 dbfilename dump.rdb dir /data EOF# Slave1 配置cat>redis-cluster/slave1/redis.conf<<'EOF' port 6380 bind 0.0.0.0 daemonize no # 关键:指定主节点 replicaof redis-master 6379 # 如果主节点有密码 # masterauth yourpassword appendonly yes dir /data EOF# Slave2 配置cat>redis-cluster/slave2/redis.conf<<'EOF' port 6381 bind 0.0.0.0 daemonize no replicaof redis-master 6379 appendonly yes dir /data EOF

Step 2:Docker Compose 一键启动

# docker-compose.ymlversion:'3.8'services:redis-master:image:redis:7.0container_name:redis-masterports:-"6379:6379"volumes:-./master/redis.conf:/usr/local/etc/redis/redis.conf-master-data:/datacommand:redis-server /usr/local/etc/redis/redis.confnetworks:-redis-netredis-slave1:image:redis:7.0container_name:redis-slave1ports:-"6380:6380"volumes:-./slave1/redis.conf:/usr/local/etc/redis/redis.conf-slave1-data:/datacommand:redis-server /usr/local/etc/redis/redis.confdepends_on:-redis-masternetworks:-redis-netredis-slave2:image:redis:7.0container_name:redis-slave2ports:-"6381:6381"volumes:-./slave2/redis.conf:/usr/local/etc/redis/redis.conf-slave2-data:/datacommand:redis-server /usr/local/etc/redis/redis.confdepends_on:-redis-masternetworks:-redis-netvolumes:master-data:slave1-data:slave2-data:networks:redis-net:driver:bridge

启动:

docker-composeup-d

Step 3:验证主从关系

# 连接 Master,查看从节点信息redis-cli-p6379info replication

你会看到类似这样的输出:

# Replication role:master connected_slaves:2 slave0:ip=172.20.0.3,port=6380,state=online,offset=112,lag=0 slave1:ip=172.20.0.4,port=6381,state=online,offset=112,lag=0 master_replid:8e0a7b3c5d2f1a9e4b6c8d0e2f4a6b8c0d2e4f6a master_replid2:0000000000000000000000000000000000000000 master_repl_offset:112 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:112

看到connected_slaves:2,说明两个小弟都连上了。

Step 4:测试数据同步

# 在 Master 写入数据redis-cli-p6379SET user:1001"张三"# OKredis-cli-p6379SET product:2001"iPhone 15"# OK# 在 Slave 读取数据redis-cli-p6380GET user:1001# "张三"redis-cli-p6381GET product:2001# "iPhone 15"# 尝试在 Slave 写入(应该失败)redis-cli-p6380SETtest"value"# (error) READONLY You can't write against a read only replica.

看到没?数据自动同步了,而且 Slave 默认是只读的,防止误写导致数据不一致。

2.4 主从复制的原理:全量同步 vs 增量同步

全量同步(Full Resynchronization)

当 Slave 第一次连接 Master,或者复制偏移量差距太大时,会触发全量同步:

Slave: "老大,我要同步数据!" Master: "好,你先等着,我先保存个 RDB 快照..." [bgsave 后台生成 RDB 文件] Master: "快照好了,发给你!" [发送 RDB 文件] Slave: "收到,我正在加载..." [加载 RDB 到内存] Master: "这是生成快照之后的新数据,也给你" [发送缓冲区的写命令] Slave: "搞定,以后增量同步就行啦!"

增量同步(Partial Resynchronization)

如果 Slave 只是短暂断开又重连,Master 只需要把断开期间的写命令发给 Slave 就行,不需要再传整个 RDB。

Redis 用repl_backlog(复制积压缓冲区)来实现这个机制:

Master 的 repl_backlog: ┌─────────────────────────────────────────┐ │ cmd1 │ cmd2 │ cmd3 │ cmd4 │ cmd5 │ ... │ │ │ │ │ │ │ │ └──────┴──────┴──────┴──────┴──────┴─────┘ offset: 100 200 300 400 500 Slave 重连,告诉 Master: "我上次同步到 offset=300" Master 一看,300 还在 backlog 里: "行,我把 300 之后的 cmd4、cmd5 发给你"

关键点:repl-backlog-size默认 1MB,如果断开时间太长,offset 超出了 backlog,就只能全量同步了。生产环境建议调大这个值。

2.5 Java 代码中怎么使用主从?

用 Lettuce 客户端,配置读写分离:

@ConfigurationpublicclassRedisConfig{@BeanpublicRedisConnectionFactoryredisConnectionFactory(){// 主节点:负责写RedisStaticMasterReplicaConfigurationconfig=newRedisStaticMasterReplicaConfiguration("localhost",6379);// 从节点:负责读config.addNode("localhost",6380);config.addNode("localhost",6381);LettuceClientConfigurationclientConfig=LettuceClientConfiguration.builder().readFrom(ReadFrom.REPLICA_PREFERRED)// 优先从从节点读.build();returnnewLettuceConnectionFactory(config,clientConfig);}}
@ServicepublicclassUserService{@AutowiredprivateStringRedisTemplateredisTemplate;// 写操作:自动路由到 MasterpublicvoidsaveUser(StringuserId,StringuserInfo){redisTemplate.opsForValue().set("user:"+userId,userInfo);}// 读操作:自动路由到 SlavepublicStringgetUser(StringuserId){returnredisTemplate.opsForValue().get("user:"+userId);}}

注意:主从架构本身不能自动故障转移!Master 挂了,你得手动把 Slave 提升为 Master。要自动故障转移,得上 Redis Sentinel 或 Redis Cluster。这个咱们下次再聊。


三、Redis 数据固化:别让数据"随风而逝"

3.1 为什么需要数据固化?

Redis 是内存数据库,数据都在内存里。一旦进程退出、服务器重启、或者电源断了,数据就全没了。

场景: ┌─────────────────┐ │ Redis 进程 │ │ ┌───────────┐ │ │ │ 内存数据 │ │ ← 快,但断电就丢 │ │ key=value │ │ │ └───────────┘ │ └─────────────────┘ │ ▼ 进程挂了 数据全没了!

数据固化(Persistence)就是把内存数据保存到磁盘,重启后可以恢复。

3.2 两种方式:RDB 和 AOF

Redis 提供两种持久化方式,可以单独用,也可以一起用。

特性RDBAOF
原理定时生成内存快照记录每个写命令
文件体积紧凑,体积小较大,可压缩
恢复速度快(直接加载二进制)慢(重放命令)
数据安全可能丢最后一次快照后的数据最多丢 1 秒数据(默认)
性能影响fork 子进程时短暂阻塞持续 fsync 有 IO 开销
可读性二进制,不可读文本格式,可读

我一般的建议是:两者都用。RDB 做快速恢复,AOF 保证数据安全。

3.3 RDB 持久化详解

原理:fork 一个子进程,把当前内存数据写入一个临时 RDB 文件,写完替换旧的。

┌─────────────┐ │ Redis 主进程 │ │ │ │ fork() │──────▶ ┌─────────────┐ │ │ │ 子进程 │ └─────────────┘ │ │ │ 生成 RDB 文件 │ │ (不影响主进程) │ └─────────────┘

配置方式

# redis.conf# 自动触发:900秒内至少1次修改,就保存save9001# 300秒内至少10次修改save30010# 60秒内至少10000次修改save6010000# RDB 文件名dbfilename dump.rdb# 保存目录dir/data# 压缩(推荐开启)rdbcompressionyes# 校验和rdbchecksumyes

手动触发

# 同步保存(阻塞,生产环境慎用)SAVE# 异步保存(后台执行,推荐)BGSAVE

RDB 的坑

  1. fork 耗时:数据量大时,fork 子进程可能耗时几百毫秒,导致主进程阻塞
  2. 数据丢失:如果两次 save 之间挂了,中间的数据就没了
  3. COW(写时复制)开销:fork 后如果写操作多,内存会翻倍

3.4 AOF 持久化详解

原理:每个写命令都追加到 AOF 文件,重启时重新执行这些命令恢复数据。

Client: SET key1 value1 ──────▶ Redis: "好,我写入内存,同时追加到 AOF" ┌─────────────────┐ │ appendonly.aof │ │ *2\r\n$3\r\nSET\r\n... │ └─────────────────┘

配置方式

# redis.conf# 开启 AOFappendonlyyes# AOF 文件名appendfilename"appendonly.aof"# 同步策略(三选一)# always:每次写都 fsync,最安全,最慢# everysec:每秒 fsync 一次,推荐,最多丢 1 秒数据# no:让操作系统决定,最快,最不安全appendfsync everysec# AOF 重写(压缩)auto-aof-rewrite-percentage100auto-aof-rewrite-min-size 64mb

AOF 重写(Rewrite)

AOF 文件会越来越大,比如你对一个 key 改了 100 次,AOF 里就有 100 条记录。但其实只需要最后一条。

AOF 重写就是创建一个新的精简版 AOF 文件,只保留最终状态:

原始 AOF: SET counter 0 INCR counter INCR counter INCR counter ... (1000次) 重写后 AOF: SET counter 1000
# 手动触发重写BGREWRITEAOF

重写也是 fork 子进程进行的,不影响主进程。

AOF 的坑

  1. 文件体积大:同样的数据,AOF 通常比 RDB 大 2-3 倍
  2. 恢复速度慢:重启时要重放所有命令,大文件可能要好几分钟
  3. fsync 性能appendfsync always性能很差,everysec是折中方案

3.5 RDB + AOF 混合模式(Redis 4.0+)

Redis 4.0 引入了混合持久化,AOF 重写时把当前内存数据以 RDB 格式写入 AOF 开头,后面的增量部分再用命令格式:

混合 AOF 文件结构: ┌─────────────────────────────────────────┐ │ RDB 格式部分(全量数据,二进制) │ ← 快速加载 │ ┌─────────────────────────────────┐ │ │ │ key1 -> value1 │ │ │ │ key2 -> value2 │ │ │ │ ... │ │ │ └─────────────────────────────────┘ │ │ │ │ AOF 格式部分(增量命令,文本) │ ← 保证数据安全 │ ┌─────────────────────────────────┐ │ │ │ SET key3 value3 │ │ │ │ DEL key1 │ │ │ │ ... │ │ │ └─────────────────────────────────┘ │ └─────────────────────────────────────────┘

配置:

# 开启 AOFappendonlyyes# 开启混合持久化aof-use-rdb-preambleyes

这是目前最推荐的方案:既有 RDB 的快速恢复,又有 AOF 的数据安全。

3.6 动手验证持久化

验证 RDB

# 1. 写入一些数据redis-cli-p6379SET test:rdb"验证RDB持久化"redis-cli-p6379SET user:1"{\"name\":\"张三\",\"age\":25}"# 2. 手动触发保存redis-cli-p6379BGSAVE# Background saving started# 3. 查看 RDB 文件ls-lh/data/dump.rdb# -rw-r--r-- 1 redis redis 120K dump.rdb# 4. 重启 Redisredis-cli-p6379SHUTDOWN# 重新启动...# 5. 验证数据还在redis-cli-p6379GET test:rdb# "验证RDB持久化"

验证 AOF

# 1. 开启 AOF 后写入数据redis-cli-p6379SET test:aof"验证AOF持久化"# 2. 查看 AOF 文件redis-cli-p6379BGREWRITEAOFcat/data/appendonly.aof# 可以看到类似这样的内容:# *2# $6# SELECT# $1# 0# *3# $3# SET# $8# test:aof# $14# 验证AOF持久化# 3. 重启后验证redis-cli-p6379SHUTDOWN# 重启...redis-cli-p6379GET test:aof# "验证AOF持久化"

3.7 生产环境持久化配置建议

# redis.conf 生产环境推荐配置# ========== RDB 配置 ==========# 自动保存策略(根据业务调整)save9001save30010save6010000# RDB 文件名和路径dbfilename dump.rdbdir/data/redis# 开启压缩rdbcompressionyes# ========== AOF 配置 ==========# 开启 AOFappendonlyyesappendfilename"appendonly.aof"# 每秒同步,平衡性能和安全appendfsync everysec# 重写策略auto-aof-rewrite-percentage100auto-aof-rewrite-min-size 128mb# 开启混合持久化(Redis 4.0+ 强烈推荐)aof-use-rdb-preambleyes# ========== 其他优化 ==========# 后台保存出错时停止写入(防止数据不一致)stop-writes-on-bgsave-erroryes# 加载 AOF 时如果文件截断,尽量加载可用数据aof-load-truncatedyes

四、主从 + 持久化的完整实战

4.1 架构图

┌─────────────┐ │ Client │ └──────┬──────┘ │ ┌────────────┼────────────┐ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Write │ │ Read │ │ Read │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Master │──▶│ Slave 1 │ │ Slave 2 │ │ │ │ │ │ │ │ dump.rdb│ │dump.rdb │ │dump.rdb │ │append.aof│ │append.aof│ │append.aof│ └─────────┘ └─────────┘ └─────────┘ │ ▼ ┌─────────────────┐ │ 定时备份到云存储 │ │ (S3/OSS/COS) │ └─────────────────┘

4.2 带持久化的 Docker Compose 配置

version:'3.8'services:redis-master:image:redis:7.0container_name:redis-masterports:-"6379:6379"volumes:-./master/redis.conf:/usr/local/etc/redis/redis.conf-master-data:/data-./backup:/backup# 挂载备份目录command:redis-server /usr/local/etc/redis/redis.confnetworks:-redis-netredis-slave1:image:redis:7.0container_name:redis-slave1ports:-"6380:6380"volumes:-./slave1/redis.conf:/usr/local/etc/redis/redis.conf-slave1-data:/datacommand:redis-server /usr/local/etc/redis/redis.confdepends_on:-redis-masternetworks:-redis-net# 定时备份服务(用 cron 或脚本)redis-backup:image:redis:7.0container_name:redis-backupvolumes:-master-data:/data:ro-./backup:/backupentrypoint:>sh -c " while true; do sleep 86400; cp /data/dump.rdb /backup/dump-$$(date +%Y%m%d).rdb; echo \"Backup completed at $$(date)\"; done "depends_on:-redis-masternetworks:-redis-netvolumes:master-data:slave1-data:networks:redis-net:

4.3 Spring Boot 集成示例

@ConfigurationpublicclassRedisHAConfig{@BeanpublicRedisConnectionFactoryredisConnectionFactory(){// 主从配置RedisStaticMasterReplicaConfigurationconfig=newRedisStaticMasterReplicaConfiguration("localhost",6379);config.addNode("localhost",6380);config.addNode("localhost",6381);// 客户端配置ClientOptionsclientOptions=ClientOptions.builder().autoReconnect(true)// 自动重连.pingBeforeActivateConnection(true).build();LettuceClientConfigurationclientConfig=LettuceClientConfiguration.builder().clientOptions(clientOptions).readFrom(ReadFrom.REPLICA_PREFERRED).commandTimeout(Duration.ofSeconds(3)).build();LettuceConnectionFactoryfactory=newLettuceConnectionFactory(config,clientConfig);factory.setValidateConnection(true);returnfactory;}@BeanpublicRedisTemplate<String,Object>redisTemplate(RedisConnectionFactoryfactory){RedisTemplate<String,Object>template=newRedisTemplate<>();template.setConnectionFactory(factory);// 序列化配置StringRedisSerializerstringSerializer=newStringRedisSerializer();GenericJackson2JsonRedisSerializerjsonSerializer=newGenericJackson2JsonRedisSerializer();template.setKeySerializer(stringSerializer);template.setValueSerializer(jsonSerializer);template.setHashKeySerializer(stringSerializer);template.setHashValueSerializer(jsonSerializer);template.afterPropertiesSet();returntemplate;}}
@Service@Slf4jpublicclassCacheService{@AutowiredprivateRedisTemplate<String,Object>redisTemplate;@AutowiredprivateStringRedisTemplatestringRedisTemplate;/** * 缓存数据,带过期时间 */public<T>voidset(Stringkey,Tvalue,longtimeout,TimeUnitunit){try{redisTemplate.opsForValue().set(key,value,timeout,unit);}catch(RedisConnectionFailureExceptione){log.error("Redis写入失败,key={}",key,e);// 降级:写入本地缓存或数据库}}/** * 读取缓存 */@SuppressWarnings("unchecked")public<T>Tget(Stringkey){try{return(T)redisTemplate.opsForValue().get(key);}catch(RedisConnectionFailureExceptione){log.error("Redis读取失败,key={}",key,e);returnnull;}}/** * 删除缓存 */publicvoiddelete(Stringkey){redisTemplate.delete(key);}/** * 批量写入(Pipeline 优化) */publicvoidbatchSet(Map<String,String>data){stringRedisTemplate.executePipelined((RedisCallback<Object>)connection->{data.forEach((key,value)->connection.stringCommands().set(key.getBytes(),value.getBytes(),Expiration.from(1,TimeUnit.HOURS),RedisStringCommands.SetOption.UPSERT));returnnull;});}}

五、常见问题排查

5.1 主从同步延迟

# 查看复制延迟redis-cli-p6380info replication|grepoffset# master_repl_offset:1234567# slave_repl_offset:1234000# 计算延迟# 如果 offset 差距很大,说明延迟严重

解决方案

  • 检查网络带宽
  • 减少大 key(比如一个 hash 存了几十万字段)
  • 考虑拆分多个小 key

5.2 AOF 文件过大

# 查看 AOF 大小ls-lh/data/appendonly.aof# 手动触发重写redis-cli BGREWRITEAOF

5.3 RDB 生成时内存暴涨

# 检查 Redis 内存使用redis-cli INFO memory|grepused_memory# 如果 used_memory_rss 远大于 used_memory,说明 COW 开销大

解决方案

  • 避免在高峰期做 BGSAVE
  • 控制单个实例数据量(建议不超过 10GB)
  • 考虑用 AOF 替代 RDB

六、总结

咱们今天聊了 Redis 的两个核心能力:

能力解决的问题关键配置
主从复制单点故障、读性能瓶颈replicaofReadFrom.REPLICA_PREFERRED
RDB 持久化快速恢复、全量备份savedbfilename
AOF 持久化数据安全、最小丢失appendonlyappendfsync everysec
混合持久化兼顾恢复速度和数据安全aof-use-rdb-preamble yes

最佳实践 checklist

  • 生产环境至少一主一从,不要单机裸奔
  • 同时开启 RDB 和 AOF(混合模式)
  • AOF 用appendfsync everysec
  • 定期把 RDB 备份到远程存储
  • 监控主从延迟和复制状态
  • 控制单个 Redis 实例大小(< 10GB)
  • 需要自动故障转移?上 Redis Sentinel 或 Cluster

最后说一句:Redis 很快,但快不代表不会挂。数据安全和系统可用性,永远是第一位的。别等到数据丢了、服务挂了,才想起来做高可用。

你在生产环境是怎么配 Redis 的?有没有踩过什么坑?欢迎在评论区交流!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/7 8:31:30

CheckAI:自动化代码与文本质量评估工具实战指南

1. 项目概述与核心价值最近在折腾一些自动化脚本和AI应用时&#xff0c;发现一个挺普遍但又容易被忽视的问题&#xff1a;我们写的代码、生成的文本&#xff0c;甚至是AI模型给出的回答&#xff0c;其质量到底怎么样&#xff1f;有没有一个快速、客观的评估方法&#xff1f;很多…

作者头像 李华
网站建设 2026/5/7 8:29:33

Mermaid Live Editor 终极指南:如何用代码轻松创建专业图表

Mermaid Live Editor 终极指南&#xff1a;如何用代码轻松创建专业图表 【免费下载链接】mermaid-live-editor Edit, preview and share mermaid charts/diagrams. New implementation of the live editor. 项目地址: https://gitcode.com/GitHub_Trending/me/mermaid-live-e…

作者头像 李华
网站建设 2026/5/7 8:28:36

别再只调lr了!PyTorch Adam优化器里betas、eps这些参数到底怎么设?

突破Adam优化器调参瓶颈&#xff1a;betas、eps与weight_decay的深度实践指南 当你的神经网络训练陷入停滞&#xff0c;验证集指标像过山车一样上下波动时&#xff0c;大多数开发者会条件反射地调整学习率(lr)。但真正高效的优化器调参远不止于此——就像赛车手不会只通过油门踏…

作者头像 李华
网站建设 2026/5/7 8:20:03

Arduino项目避坑:L298N驱动12V电磁铁时,如何解决电流过大与发热问题?

Arduino项目实战&#xff1a;L298N驱动12V电磁铁的电流控制与散热优化 电磁铁在机器人、自动化控制等领域应用广泛&#xff0c;但很多创客在使用L298N模块驱动12V电磁铁时&#xff0c;常常遇到模块发烫、动力不足甚至烧毁Arduino主板的问题。上周我在工作室测试一个自动锁装置时…

作者头像 李华