Redis 主从复制:主从节点之间如何同步数据?
单机 Redis 存在单点风险问题,也就是说,如果唯一的一个 Redis 节点宕机的话,就会导致大量的请求直接被打到数据库,严重的情况下,数据库很可能会直接被干宕机了。
这个时候,保障 Redis 服务的高可用就成为了我们不得不面对的问题。
如何保证 Redis 服务高可用? 最简单的一种办法就是基于 主从复制 搭建一个 Redis 集群,master(主节点)主要负责处理写请求,slave(从节点)主要负责处理读请求。

如果 master 宕机的话,从 slave 中选出一台作为 master 即可实现故障转移(Failover)。

是不是有点类似于 MySQL 的读写分离?这其实就是一种典型的多副本/备份的思想,经常被用在高可用架构上。
什么是主从复制?
简单来说,主从复制 就是将一台 Redis 主节点的数据复制到其他的 Redis 从节点中,尽最大可能保证 Redis 主节点和从节点的数据是一致的。主从复制是 Redis 高可用的基石,我们后面介绍到的 Redis Sentinel 以及 Redis Cluster 都依赖于主从复制。
主从复制这种方案不仅保障 了 Redis 服务的高可用,还实现了读写分离,提高了系统的并发量,尤其是读并发量。
主从复制这种方案基于 Redis replication(默认使用异步复制),开发者可以通过 replicaof (Redis 5.0 之前是 slaveof 命令)命令来配置各个 Redis 节点的主从关系。
# 在指定 redis 节点上执行 replicaof 命令就可以让其成为 master 的 slave
# ip port 是 master 的
replicaof ip port
配置完成之后,主从节点之间的数据同步会自动进行,不需要人为插手。
至于具体要配置多少 slave 节点,主要取决于项目的读吞吐量,因为 slave 节点分担的是读请求,写请求由 master 节点负责。
主从复制下从节点会主动删除过期数据吗?
类似的问题:
- 主从复制下会读取到过期的数据吗?
- 主从复制下如何避免读取到过期的数据?
这是一个常见的问题,面试中也经常会问到。
我们知道,Redis 中常用的过期数据的删除策略就两个(重要!自己造缓存轮子的时候需要格外考虑的东西):
- 惰性删除 :只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
- 定期删除 : 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性/懒汉式删除 。
那客户端读取从节点会读取到过期数据么? 答案是:有可能,但要看具体的情况。
- Redis 3.2 版本之前,客户端读从库并不会判断数据是否过期,有可能返回过期数据。Redis 3.2 版本及之后,客户端读从库会先判断数据是否过期,如果读取的数据已经过期了,从库虽然不会删除,但是会返回空值,这就避免了客户端读到过期数据。
- 采用
EXPIRE或者PEXPIRE设置过期时间的话,表示的是从执行这个命令开始往后 TTL 时间过期。如果从节点同步执行命令因为网络等原因延迟的话,客户端就可能会读取到过期的数据(如下图所示,客户端在 T3-T4 之间读取到的就是过期数据)。这种情况可以考虑使用EXPIREAT和PEXPIREAT,这两个命令语义和效果和EXPIRE或者PEXPIRE类似,但不是指定 TTL(生存时间)的秒数/毫秒数,而是使用绝对的 Unix 时间戳(自 1970 年 1 月 1 日以来的秒数)。由于设置的是时间点,主从节点的时钟需要保持一致。

主从节点之间如何同步数据?
类似的问题:
- 主从复制的原理是什么?
- master 节点的数据是如何同步给 slave 节点的?
- 复制积压缓冲区的有什么用?
这个问题其实还挺复杂的,Redis 主从复制经历了多次改进,每一次的改进都解决了上一个版本的一些痛点。想要彻底搞懂的话,我们需要耐心地看看 Redis 主从复制的演进历程,这里主要分为下面 3 个阶段:
- Redis 2.8 之前的 SYNC 方案
- Redis 2.8 PSYNC 方案
- Redis 4.0 PSYNC2.0 方案
为了让大家更好理解,我这里已经是经我所能使用大白话讲解了,并且,还配有图解。
需要注意:每一个版本的方案基本都是持续优化改进得到的。
Redis 2.8 之前的 SYNC 方案
Redis 在 2.8 版本之前都是基于 SYNC 命令执行全量同步,整个步骤简化后是这样的:
- slave 向 master 发送 SYNC 命令请求启动复制流程;
- master 收到 SYNC 命令之后执行 BGSAVE 命令(子线程执行,不会阻塞主线程)生成 RDB 文件(dump.rdb);
- master 将生成的 RDB 文件发送给 slave;
- slave 收到 RDB 文件之后就开始加载解析 RDB 同步更新本地数据;
- 更新完成之后,slave 的状态相当于是 master 执行 BGSAVE 命令时的状态。master 会将 BGSAVE 命令之后接受的写命令缓存起来,因为这部分写命令 slave 还未同步;
- master 将自己缓存的这些写命令发送给 slave,slave 执行这些写命令同步 master 的最新状态;
- slave 到这个时候已经完成全量复制,后续会通过和 master 维护的长连接来进行命令传播,同步最新的写命令。

master 为每一个 slave 单独 开辟一块 **replication buffer(复制缓存区)**来记录 RDB 文件生成后 master 收到 的所有写命令。
replication buffer 可以通过下面这些参数来控制,超过指定的阈值后,master 就会强制断开对应 slave 的连接。
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
执行 BGSAVE 命令之后,Redis 主线程会专门 fork 一个子进程,子进程共享主线程的内存数据。子进程会读取主线程中的内存数据写入 RDB 文件中,不会阻塞主线程。
这里说 Redis 主线程而不是主进程的主要是因为 Redis 启动之后主要是通过单线程的方式完成主要的工作。如果你想将其描述为 Redis 主进程,也没毛病。
虽然 BGSAVE 子进程写入 RDB 的工作不会阻塞主线程,但会对机器的 CPU 资源和内存资源产生影响,严重的情况下甚至会直接把 Redis 服务干宕机。
- 写入 RDB 的过程中,为了避免影响写操作,主线程修改的内存数据会被复制一份副本,BGSAVE 子进程把这个副本数据写入 RDB 文件。如果修改内存数据的请求比较多的话,生成内存数据副本产生的内存消耗是非常大的。这个过程称为 Copy On Write(写时复制,COW) ,操作系统层面提供的机制。
- 大量的写时复制会产生大量的分页错误(也叫缺页中断、页缺失),消耗大量的 CPU 资源。
也就是说,通过 BGSAVE 操作属于重量级操作,会对机器的 CPU 资源和内存资源产生影响。在生产环境中,我们应该尽量避免在 master 实例上频繁执行 BGSAVE 命令。
除了上面这个问题之外,Redis 在 2.8 版本之前主从复制还存在下面这些迫切需要解决的问题:
- slave 加载 RDB 的过程中不能对外提供读服务。
- slave 和 master 断开连接之后,slave 重新连上 master 需要重新进行全量同步。
Redis 2.8 PSYNC 方案
Redis 2.8 版本 SYNC 命令被 PSYNC 取代,PSYNC 格式如下(相比较于 SYNC 命令,多了两个参数):
PSYNC replicationid offset