Redis的读写性能很高,但在面对大规模数据和高发访问的挑战时,单节点的Redis可能无法满足需求,这就引出了Redis集群的概念。本节先介绍一下Redis高可用方案之一的主从复制模式,虽说现在基本不会用这种模式,但是无论是哨兵还是集群(cluster),都会使用到主从复制。
一、什么是主从复制
1.1 概述
Redis 的主从复制是一种 Redis 数据库服务器之间的数据同步机制。在主从复制中,一个Redis服务器作为主服务器(master),其余的Redis服务器作为从服务器(slave),主服务器会实时将数据更新同步到从服务器上,从而实现数据的备份和读写分离,当主服务器出现故障时,从服务器可以直接顶替主服务器继续提供读写服务。
- Matser节点负责读和写操作。
- Slave节点只能负责读操作。
- 所有的Slave节点都会连接Master,并同步数据。
- 也可以实现读写分离:Master只负责写,Slave只负责读。
1.2 开启主从复制
通常有以下三种方式:
-
在slave服务器直接执行命令:slaveof <masterip> <masterport>
-
在slave配置文件中加入:slaveof <masterip> <masterport>
# 主从复制。使用 slaveof 命令让一个 Redis 实例成为另一个 Redis 服务器的副本。关于 Redis 复制,
# 有几件事情需要立即了解。
# 1) Redis复制是异步的,但是您可以配置一个主节点,在没有至少连接到指定数量的从节点时停止接受入。
# 2) 如果复制链路在相对较短的时间内丢失,Redis 从节点可以与主节点执行部分重新同步。您可能需要根据
# 您的需求设置合理的复制积压大小(请参见本文件的后续部分)。
# 3) 复制是自动的,不需要用户干预。在网络分区后,从节点会自动尝试重新连接到主节点并与之重新同步。
# slaveof <masterip> <masterport>
-
使用启动命令:--slaveof <masterip> <masterport>
ps:在Redis5.0之后,slaveof相关命令和配置已经被替换成replicaof <masterip> <masterport>。为了兼容旧版本,配置文件仍然支持slaveof,但命令就不支持了。
二、复制(重点)
2.1 同步(sync)和命令传播(command propagate)
Redis的复制功能主要分为同步(sync)和命令传播(command propagate)两个操作。
-
同步操作用于将slave服务器数据库状态更新至master服务器当前所处的数据库状态。
当客户端salve服务器发送SLAVEOF命令,要求salve服务器复制master服务器时,slave服务器首先需要执行同步操作,通过向master服务器发送SYNC命令(Redis 2.8版本之前)来完成:
- slave服务器向master服务器发送SYNC命令。
- 收到SYNC命令的master服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令。
- 当master服务器的BGSAVE命令执行完毕,master服务器会将BGSAVE命令生成的RDB文件发送给slave服务器,slave服务器接收并载入RDB文件,将自己的数据库状态更新至master服务器执行BGSAVE命令时的状态。
- master将记录在缓冲区里的所有写命令发送给slave服务器,slave服务器执行这些写命令,至此,主从服务器数据库处于一致状态。
-
命令传播操作则用于在主服务器的数据库状态被修改,导致主从服务器的数据库状态出现不一致时,让主从服务器的数据库重新回到一致状态。
在同步操作执行完毕后,主从服务器数据库将达到一致状态,但每当主服务器执行写命令时,它俩就会不一致。为了让主从服务器再次回到一致状态,master服务器需要对slave服务器执行命令传播操作:master服务器会将自己执行的写命令,也就是会造成数据库状态不一致的命令,发送给slave服务器执行,当slave服务器执行了相同的命令之后,主从服务器将再次回到一致状态。
读到这,相信大家有一个疑问,如果主从服务器之间断开连接了,那么在恢复连接后,主从服务器之间是如何同步数据的呢?这就分为完整和部分复制两种方式了,在redis 2.8版本之前和之后是有差异的,下面我们一起来看看。
2.2 Redis 2.8版本之前的旧版复制(SYNC)
在Redis中,slave服务器对master服务器的复制可分为以下两种:
- 初次复制:slave服务器以前没有复制过任何master服务器,或者slave服务当前要复制的master服务器和上一次复制的master服务器不同。
- 断线后重复制:处于命令传播阶段的主从服务器因为某种原因中断,又重新连接。
对于初次复制还好说,旧版复制能很好地完成工作,但对于断线后重复制这种情况,虽说旧版复制也能完成,但效率太低。
假如现在主从服务器处于正常状态,它们之间存储了1000个key,在存第1001个key时,主从服务器断开了,master服务器继续执行写命令,当执行到第1500key时,slave服务器恰好重连上了,那么:
- slave服务器会向master服务器发送SYNC命令。
- master服务器接收到SYNC命令,执行BGSAVE命令,创建k1到k1500的RDB文件,并使用缓冲区记录接下来执行的所有写命令。
- BGSAVE命令执行完毕,向slave服务器发送RDB文件。
- slave服务器接收到RDB文件,载入。
- 载入完毕,master服务器将缓冲区的写命令发送给slave服务器。
- slave执行缓冲区中的写命令,主从服务器重新回到一致状态。
SYNC存在的问题:
我们从这就可以发现,实际slave并不需要重新发送一遍SYNC命令,再从头到尾把master服务器的写命令执行一遍(k1-k1500),这无疑是低效的,而是只需要执行断开连接后的写命令即可(k1001-k1500)。
执行SYNC命令是一个非常耗费资源的操作,每次执行SYNC命令,master服务器都需要执行以下操作:
- master服务器需要执行BGSAVE命令来生成RDB文件,这个生成操作会耗费master服务器大量的CPU、内存和I/O资源。
- master服务器需要将自己生成的RDB文件发送给slave服务器,这个发送操作会耗费主从服务器大量的网络资源(带宽和流量),并对master服务器响应命令请求的时间产生影响。
- 接收到RDB文件的slave服务器需要载入matser服务器发来的RDB文件,并且在载入期间,slave服务器会因为阻塞而没办法处理命令请求。
2.3 Redis 2.8版本起的新版复制(PSYNC)
为了解决slave每次断线重连都需要全量同步的问题,Redis在2.8版本引入了PSYNC命令,PSYNC包含完整重同步和部分重同步两种模式:
- 完整重同步:和SYNC命令基本一致。
- 部分重同步:slave只需要接收和同步短线期间丢失的写命令即可,不需要进行完整重同步(上述例子,只需执行k1001-k1500的写命令即可)
2.4 部分重同步的实现
部分重同步功能由以下三个部分构成:
- master服务器和slave服务器的的复制偏移量(offset)。
- master服务器的复制挤压缓冲区(replication backlog buffer)。
- 服务器的运行ID(run ID)。
下面我们分别介绍一下。
2.4.1 复制偏移量(offset)
执行主从复制的双方都会分别维护一个复制偏移量,master每次向slave传播N字节,自己的复制偏移量就会增加N;同理,slave接收N个字节,复制偏移量也会增加N。通过对比主从之间的复制偏移量就可以知道主从服务器之间的同步状态。
ps:主从服务器复制偏移量相等说明处于一致状态,否则不一致。
2.4.2 复制积压缓冲区(replication backlog buffer)
复制积压缓冲区是master维护的一个固定长度的FIFO队列,默认大小1MB。
当master进行命令传播时,不仅将写命令发给slave服务器,还会同时写进复制积压缓冲区,因此master的复制积压缓冲区会保存一部分最近传播的写命令,并且会为每个队列中的每个字节记录相应的复制偏移量。
当slave服务器重新连上master服务器时,slave服务器会通过PSYNC命令将自己的复制偏移量发送给master服务器,master服务器会根据这个复制偏移量来决定对slave服务器执行哪个模式的同步操作:
- 如果偏移量之后的数据(即offset+1开始的数据)仍然存在复制积压缓冲区里,那么就进行部分重同步操作。
- 反之,只能执行完整重同步操作。
ps:如果master服务器需要执行大量的写命令,又或者主从服务器断线时间比较长,那么这个队列的大小(1MB)或许不合适。如果队列的大小不合适,那么也就一位置PSYNC命令就不能发挥正常的作用,所以,正确估算复制积压缓冲区的大小很重要。可根据公式: 2*(主从服务器断线时长second)*(master服务器每秒产生的写命令数据量)来估算。
2.4.3 服务器运行ID
每个Redis server都会有自己的运行ID,由40个随机的十六进制字符组成。
当slave初次复制master时,matser会将自己的运行ID发给slave进行保存,这样slave重连时再将这个运行ID发送给重连上的master:
- 如果slave服务器保存的ID和当前master服务器的ID相同,那么master可以继续尝试执行部分重同步操作。
- 反之说明salve服务器断线之前复制的master服务器并不是当前连接的master服务器,master服务器将会对slave服务器进行完整重同步操作。
2.4.4 PSYNC命令的实现
了解了上述三个概念后,我们接着介绍PSYNC命令,PSYNC命令的调用方式有两种:
- 如果slave服务器以前没有复制过任何master服务器或之前执行过slave no one命令,那么slave服务器会向master服务器发送PSYNC ? -1 命令,主动请求完整重同步。
- 反之,如果slave服务器已经复制过某个master服务器,那么slave服务器会向master服务器发送PSYNC <runnid> <offset>命令:runnid为上次master服务器的运行id,offset为slave服务器的复制偏移量;接收到这个命令的master服务器会通过这两个参数来决定用哪种同步操作。
接收到命令的master服务器会返回的回复以下三种之一:
- +FULLRESYNC <runnid> <offset>:表示执行完整重同步:runnid为master服务器的运行ID,offset为master服务器的复制偏移量。
- +CONTINUE:表示执行部分重同步操作。
- -ERR:表示master服务器的版本低于Redis 2.8,识别不了PSYNC命令,slave将向master服务器发送SYNC命令,执行完整同步操作。
大致流程如下:
PSYNC存在的问题:
通过上面的流程,我们可以看到,如果要执行部分重同步需要满足两个条件:runnid和offset,一旦两者不能同步满足,则仍需要进行完整重同步,例如以下场景:
- slave重启,保存的master runnid和offset丢失,则需要进行完整重同步。
- redis发生故障需要切换,切换后的runnid发生了变化,也需要完整重同步。
而上述两个场景出现的概率还挺高的,这么一来好像PSYNC命令的作用似乎并不完善了,好在Redis在4.0版本针对这个问题又进行了优化,下面我们一起来看下。
2.5 Redis 4.0版本起的新版复制(PSYNC2)
为了解决PSYNC命令执行部分重同步过分依赖runnid和offset的问题,Redis在4.0版本对PSYNC命令进行了优化,我们通常称之为PSCYN2,主要有两个改动:
-
引入两组replid和offset:
第一组:replid和master_repl_offset(可以理解为原来的runnid和offset)
对于master,分别表示为复制ID和复制偏移量
对于slave,表示正在同步的master的复制ID和自己的复制偏移量。
第二组:replid2和second_repl_offset
对于master和slave,都表示自己的上一个master的复制ID和复制偏移量。主要用于故障切换时支持部分重同步。
-
slave开启复制积压缓冲区:
salve开启复制积压缓冲区,主要用于故障切换后,当某个slave升级为master,该slave仍然可以通过复制积压缓冲区继续支持部分重同步。
以上是改动,下面我们看下它对PSYNC命令的优化:
-
slave重启问题优化:
该问题的主要原因是slave重启后runnid和offset丢失了,解决也很简单,就是在重启之前把这两个变量想办法存下来就行。
而Redis的做法是:在服务正常关闭前会调用rdbSaveInfoAuxFields函数把当前的复制ID(replid)和复制偏移量(offset)保存到RDB文件中,后续就可以从RDB文件中读取到这两个变量。
-
master故障切换后问题优化:
该问题的主要原因是master故障切换后runnid会发生改变,从而导致执行完整重同步。
Redis的做法是:
当节点从slave晋升为master后,会将原来自己保存的第一组复制ID和复制偏移量,移动到第二组复制ID和复制偏移量,然后将第一组复制ID重新生成一个新的,也就是属于自己的复制ID。
相当于,slave晋升为master后,replid保存了自己的复制ID,replid2保存了老master的复制ID。
这样一来,新的master就可以通过判断replid2来判断slave之前是否跟自己是从同一个master复制数据,如果是的话,则尝试使用部分重同步。
流程如下:
2.6 总结
从Redis2.*到现在,主从复制流程进行了逐步的优化:
- Redis 2.8之前复制采用SYNC命令,无论是第一次还是断线重连,都采用完整重同步方式,效率很低。
- Redis 2.8~Redis 4.0版本之间采用PSYNC命令,主要优化了断线重连后可以通过runid和offset使用部分重同步,效率提高,但存在slave重启和master故障切换问题导致执行完整重同步的问题。
- Redis 4.0版本之后对PSYNC命令进行了优化——PSYNC2,主要优化了PSYNC在slave重启和master故障切换后执行完整重同步的问题。
三、复制功能实现的八个步骤
这里以Redis 2.8版本为例,主从复制的完整过程如下:
3.1 开启主从复制
开启主从复制的方式主要有以下三种:
- 在slave服务器直接执行命令:slaveof <masterip> <masterport>
- 在slave配置文件中加入:slaveof <masterip> <masterport>
- 使用启动命令:--slaveof <masterip> <masterport>
3.2 设置主服务器的地址和端口
当slave服务器执行命令后,slave服务器首先要做的是将master服务器的IP地址和端口号保存到服务器状态的masterhost和masterport属性里:
truct redisServer {
// ...
//主服务器的地址
char *masterhost;
//主服务器的端口
int masterport;
// ...
};
在完成ip和port的设置后,slave会向发送slaveof命令的客户端返回OK,表示复制指令已被接收,实际的复制工作在OK返回之后才真正开始执行。
3.3 建立套接字连接
3.2执行完之后,slave将根据设置的ip地址和端口,创建连向master服务器的套接字(socket)连接。如果连接成功,slave服务器将为这个套接字(socket)关联一个专门处理复制工作的文件事件处理器,这个处理器负责执行后续的复制工作,比如接收RDB文件和master服务器传播来的写命令。
3.4 发送PING命令
建立套接字连接后,slave会向master发送一个PING命令,用来检查套接字的读写状态是否正常,master能否正常处理命令请求:
- 如果slave收到“PONG”回复,则表示master和slave之间连接正常。
- 反之,如果没回复或是其它回复,表示 master 和 slave 之间的网络连接状态不佳或者 master 暂时没办法处理 slave 的命令请求,则 slave 进入 error 流程:slave 断开当前的连接,之后再进行重试。
3.5 身份验证
slave服务器接收到master服务器返回的“PONG”回复后,下一步要做的就是决定是否进行身份验证,如果需要认证,slave服务器会向master服务器发送一条AUTH <password>(slave自己设置的密码)命令:
- 如果master和slave都没有设置密码,则无需验证。
- 如果都设置了密码,并且密码相同,则验证成功。
- 如果都设置了密码但密码都不同或master和slave一个设置了密码一个没设置都会返回错误。从而使slave服务器进入error流程:slave断开当前连接,之后再进行重试
3.6 发送端口信息
在身份验证后,slave服务器将执行REPLCONF listening-port <port-number>命令,向master服务器发送slave服务器的端口号,master服务器收到后会将该端口号记录到slave服务器所对应的客户端状态的slave_listening_port属性中:
typedef struct redisClient {
// ...
// 从服务器的监听端口号
int slave_listening_port;
// ...
} redisClient;
ps:slave_listening_port属性目前唯一的作用就是在主服务器执行INFO replication命令时打印出从服务器的端口号。
3.7 同步
在这一步,slave服务器会向master服务器发送PSYNC命令,执行同步操作,并将自己和master服务器的数据库状态更新至一致状态。
3.8 命令传播
当完成同步之后,主从服务器就会进入命令传播阶段,这时master服务器只要一直将自己执行的写命令发送给slave服务器,而slave服务器只要一直接收并执行写命令,就可以保证主从服务器保持一致了。
在命令传播阶段,slave默认会以每秒一次的频率,向master发送命令:REPLCONF ACK <reploff>,其中reploff是slave当前的复制偏移量。
发送REPLCONF ACK命令对于主从服务器有三个作用:
- 检测master和slave的网络连接状态。
- 汇报自己的复制偏移量,检测命令丢失,master会对比复制偏移量,如果发现slave的复制偏移量小于自己,则会向slave发送未同步的数据。
- 辅助实现min-slaves配置,用于防止master在不安全的情况下执行写命令。
例如redis.conf文件配置表示,当延迟时间小于10秒的 slave 数量小于3个,则会拒绝执行写命令。而这边的延迟时间,就是以 slave 最近一次发送 ACK 时间和当前时间作对比。
# 如果主节点连接的从节点数量少于 N 个,并且延迟小于等于 M 秒,则主节点可以停止接受写入。
# 这 N 个从节点需要处于“在线”状态。
# 延迟秒数必须小于等于指定值,是从上次接收从节点发送的 ping 命令开始计算的,通常每秒发送一次。
# 该选项并不保证 N 个副本将接受写入,但会限制暴露丢失写入的时间窗口,以指定的秒数为界。
# 例如,要求至少有 3 个延迟小于等于 10 秒的从节点,请使用:
# min-slaves-to-write 3
# min-slaves-max-lag 10
四、优缺点
4.1 优点
提高系统的可靠性和容灾能力:通过将主服务器的数据复制到从服务器上,实现数据的备份和容灾,主服务器宕机时可以快速切换到从服务器,确保系统的稳定运行。
读写分离:主从复制可以实现读写分离,主服务器负责处理写操作,从服务器负责处理读操作,有效分担服务器负载,提升系统的性能和并发能力。
横向扩展:可以通过增加从服务器来实现横向扩展,提升系统的并发处理能力和数据存储容量。
降低网络延迟:从服务器可以随时接手主服务器的工作,降低网络传输延迟,提升数据访问速度。
4.2 缺点
网络传输开销:主从复制需要在主从服务器之间进行数据同步,会产生一定的网络传输开销,特别是在数据量较大的情况下,可能影响系统的性能。
数据一致性问题:在网络异常或配置错误的情况下,可能导致主从数据不一致的问题,需要进行额外的监控和维护。
复制延迟:由于主从复制是异步的,存在一定程度的复制延迟,从服务器的数据可能不是实时同步的,可能会影响数据的时效性。
- 故障转移不是自动的:在没有使用Sentinel或Redis Cluster的情况下,发生故障时需要手动进行故障转移。
End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。