Redis 主从复制 + 读写分离

  • 1. Redis 主从复制 + 读写分离介绍
    • 1.1 从数据持久化到服务高可用
    • 1.2 主从复制
    • 1.3 如何保证主从数据一致性?
    • 1.4 为何采用读写分离模式?
  • 2. 一主两从环境准备
    • 2.1 配置文件
    • 2.2 启动 Redis
  • 3. 主从复制原理
    • 3.1 全量同步
      • 3.1.1 建立连接
      • 3.1.2 主库同步数据给从库
      • 3.1.3 发送新写命令到从库
    • 3.2 增量同步
      • 3.2.1 主从网络断开之后的同步方式
      • 3.2.2 repl_backlog_buffer
      • 3.2.3 基于长连接的命令传播
        • (1)主 -> 从:PING
        • (2)从 -> 主:REPLCONF ACK
  • 4. 总结

1. Redis 主从复制 + 读写分离介绍

1.1 从数据持久化到服务高可用

Redis 的数据持久化技术,可以解决机器宕机,数据丢失的问题,并没有从根本上解决 Redis 的可用性。我们需要的是解决 Redis 的高可用,减少甚至避免 Redis 服务发生宕机的可能,来从根本上解决数据丢失问题。

如果对 Redis 如何进行数据持久化不是很了解的同学,可以读一下:Redis 数据持久化:AOF和RDB

1.2 主从复制

目前实现Redis高可用的模式主要有三种: 主从模式、哨兵模式、集群模式。这篇文章我们来一起学习主从模式。

Redis 提供的主从模式,是通过复制的方式,将主服务器上的 Redis 的数据同步复制一份到从 Redis 服务器,这种做法很常见,MySQL 的主从也是这么做的。

主节点的 Redis 我们称之为 master,从节点的 Redis 我们称之为 slave,主从复制为单向复制,只能由主到从,不能由从到主。可以有多个从节点,比如1主2从甚至n从,从节点的多少根据实际的业务需求来判断。

1.3 如何保证主从数据一致性?

采取读写分离的模式:

  • 读操作:主、从库都可以执行,一般是在从库上读数据,对实时性和准确性有100%高真要求的部分业务,可以谨慎评估之后读主库;

  • 写操作:只在主库上写数据,写完之后将写操作指令同步到从库。

1.4 为何采用读写分离模式?

你可以设想一下,不管是主库还是从库,都能接收客户端的写操作。那么,一个直接的问题就是:如果客户端对同一个数据(例如 k1)前后修改了三次,每一次的修改请求都发送到不同的实例上,在不同的实例上执行,那么,这个数据在这三个实例上的副本就不一致了(分别是 v1、v2 和 v3)。在读取这个数据的时候,就可能读取到旧的值

如果我们非要保持这个数据在三个实例上一致,就要涉及到加锁、实例间协商是否完成修改等一系列操作,但这会带来巨额的开销,当然是不太能接受的。

而主从库模式一旦采用了读写分离,所有数据的修改只会在主库上进行,不用协调三个实例。主库有了最新的数据后,会同步给从库,这样,主从库的数据就是一致的。

那么,主从库同步是如何完成的呢?主库数据是一次性传给从库,还是分批同步?要是主从库间的网络断连了,数据还能保持一致吗?下面我们来一起学习。

2. 一主两从环境准备

2.1 配置文件

  1. 创建目录
mkdir -p /data/redis/master/datamkdir -p /data/redis/slave1/datamkdir -p /data/redis/slave2/data
  1. 主配置文件:
bind 0.0.0.0port 6379daemonize yesrequirepass "123456"logfile "/usr/local/redis/log/redis1.log"dbfilename "pointer1.rdb"dir "/usr/local/redis/data"appendonly yesappendfilename "appendonly1.aof"masterauth "123456"
  1. 两个从配置文件:
bind 0.0.0.0port 6380daemonize yesrequirepass "123456"logfile "/usr/local/redis/log/redis2.log"dbfilename "pointer2.rdb"dir "/usr/local/redis/data"appendonly yesappendfilename "appendonly2.aof"# 从本机 6379 的 redis 实例复制数据,Redis 5.0 之前使用 slaveofreplicaof 8.129.113.233 6379# 从节点开启只读模式(默认)replica‐read‐only yes# 从节点访问主节点的密码,和 requirepass ⼀样masterauth "123456"
bind 0.0.0.0port 6381daemonize yesrequirepass "123456"logfile "/usr/local/redis/log/redis3.log"dbfilename "pointer3.rdb"dir "/usr/local/redis/data"appendonly yesappendfilename "appendonly3.aof"# 从本机 6379 的 redis 实例复制数据,Redis 5.0 之前使用 slaveofreplicaof 8.129.113.233 6379# 从节点开启只读模式(默认)replica‐read‐only yes# 从节点访问主节点的密码,和 requirepass ⼀样masterauth "123456"

2.2 启动 Redis

# 启动主./redis-server /data/redis/master/data/redis.conf# 启动从./redis-server /data/redis/slave1/data/redis.conf# 启动从./redis-server /data/redis/slave2/data/redis.conf

3. 主从复制原理

主从复制分两种:主从刚连接的时候,进⾏全量同步;全量同步结束后,进⾏增量同步

  • 全量同步 – 首次配置完成,主从库连接之后;

  • 增量同步 – 全量同步结束后

    • 准实时同步 – 主从正常运行期间;
    • Append增量数据 + 准实时同步 – 主从库间网络断开重连。

3.1 全量同步

主从库第一次复制过程大体可以分为 3 个阶段:准备阶段(即建立连接准备)、主库同步数据到从库阶段、发送同步期间增量指令到从库的阶段。

3.1.1 建立连接

这个阶段的主要作用是建立主从之间的连接,连接成立之后,才能够做数据全量同步。主要包含如下步骤:

  1. 从节点的配置文件中的 replicaof 配置项中配置了主节点的 ip 和 port ,配置完成之后,从节点就知道要跟哪个主节点进行连接;

  2. 当连接成功之后,从库开启 replicaof 操作,同时发送 psync 指令告诉主库,我准备开始同步了。命令包含了主库的 runID 和 复制进度 offset 两个参数;

    • runID:每个 Redis 实例启动都会自动生成一个唯一标识 ID,第一次主从复制,还不知道主库 runID,所以参数会默认设置为:” />

      三个步骤完成了 Redis 主从的全量复制。这边需要注意的是,Redis 中的通信,无论是主库跟从库之间,还是与客户端之间的数据交互。本质上都是通过分配内存 buffer 来进行的,Master 会先把数据写到 buffer 中,再通过网络发送出去,从而完成数据交互。

      RDB 文件作为二进制文件,无论是网络传输还是写入时的磁盘IO,效率都要比 AOF 高很多。同样的,从库进行数据恢复的时候,效率也会高一些。所以我们会选择 RDB 文件做同步而不是 AOF 模式。

      3.2 增量同步

      3.2.1 主从网络断开之后的同步方式

      在网络断开之后或者从实例服务故障恢复之后,主从库会采用增量复制的方式继续同步,而不是全量同步的模式,这样会大大降低开销,提升效率。

      增量复制: 就是指网络中断或者从库重启等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。

      3.2.2 repl_backlog_buffer

      主从库重新连接之后可以实现增量复制。关键就在 repl_backlog_buffer 缓冲区上面。

      因为 master 会将写指令操作记录在 repl_backlog_buffer 缓冲区中,并使用 master_repl_offset 记录 master 写入的位置偏移量,slave 则使用 slave_repl_offset 记录读的偏移量。master 新增写操作的时候,偏移量则会增加。从库持续执行同步的写指令后,slave_repl_offset 也会不断增加。一般情况下,这两个偏移量会保持同步,如下图左。

      但是网络断开或者从库故障期间,主实例 Redis 一般会收到新的写操作命令,但从实例则暂停执行,所以 master_repl_offset 会大于 slave_repl_offset。如下图右。

      需要注意的是, repl_backlog_buffer 并不是如图中显示的貌似无限队列的模式,而是一个类似环形数组,如果数组内容满了,就会从头开始覆盖前面的内容,因为给到的内存空间是有限的。

      在主从之间重新连接之后,slave 会先发送 psync 命令给 master,同时将自己的 {runID,slave_repl_offset} 两个参数发送给 master。master 只需要把 master_repl_offset slave_repl_offset 之间的命令同步给从库即可。增量复制的流程类似如下:


      在配置repl_backlog_buffer 的时候,需要综合考虑各种因素,太大了会导致增量执行周期比较长,还不如RDB全量覆盖。太小了有可能从库还没读取到就被 Master 的新写操作覆盖了,那样也只能执行全量复制。

      所以我们需要给出一个合理缓冲区size。一般有如下的计算公式共参考:repl_backlog_buffer_size = seconds * write_size_per_second

      • seconds:正常情况下从库断开,到重连主库所需的平均时间,秒为单位。

      • write_size_per_second:主库平均每秒产生的写命令数据量大小。

      如:主服务器大约每秒产生 0.5MB 的写指令数据,而断开到重连一般需要 30s,那么缓冲区的大小就是 0.5 * 30s = 15 MB。

      但是我们一般会保留一点buffer,比如预留 0.5 倍,那就是 : 1.5 * 15 MB = 22.5 MB 。

      3.2.3 基于长连接的命令传播

      上面的工作都是为了完成完整复制,那在完成全量复制之后,主从开始进入正常有序的同步了,具体应该怎么做呢?

      主从完成全量复制之后,他们之间需要保持连接。当主库收到操作指令的时候,通过这个连接同步给从库,这个过程称之为:基于长连接的命令传播

      为了保证传播的有效性和稳定性,从节点采用心跳机制进行侦测,发送命令:PINGREPLCONF ACK

      (1)主 -> 从:PING

      每隔指定的时间(比如 1 分钟,可配置),主节点会向从节点发送 PING 命令,侦测从节点有无超时来判断从节点的健康情况。

      (2)从 -> 主:REPLCONF ACK

      命令执行传播的阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令,将复制的偏移量发送过去。

      • REPLCONF ACK

      replication_offset 的属性指的是当前从实例服务器的复制偏移量。

      • 从实例发送 REPLCONF ACK 命令对于主要实例,主要有以下作用:

        • 检测主从服务器的网络通路是否正常。

        • 辅助实现 min-slaves 选项,使用 Redis 的 min-slaves-to-write(少于n个从实例时,拒绝执行写命令) 和 min-slaves-max-lag(主从延迟大于等于n秒时,拒绝执行写命令)两个选项可以防止主服务器在不安全的情况下执行写命令。

        • 检测命令丢失, 从节点发送了 slave_replication_offset,主节点会对比 master_replication_offset ,如果不一致,说明从节点数据缺失,主节点会从 repl_backlog_buffer 缓冲区中找到并推送缺失的数据。

      4. 总结

      主从复制的作用一个是为分担读写压力,均衡负载,另一个是为了保证部分实例宕机之后服务的持续可用性,所以 Redis 演变出主从架构读写分离

      主从复制的步骤包括:建立连接的阶段、数据同步的阶段、基于长连接的命令传播阶段

      数据同步可以分为全量复制部分复制,全量复制一般为第一次全量或者长时间主从连接断开。

      命令传播阶段主从节点之间有 PING(主到从的的探测) 和 `REPLCONF ACK(从到主的ack应答) 命令,这种互相确认心跳的模式保证数据同步的稳定性。

      主从模式是比较低级的可用性优化,要做到故障自动转移,异常预警,高保活,还需要更为复杂的哨兵或者集群模式。