用户执行slaveof命令或者在配置文件中设置slaveof选项来开启复制功能。
例如,现在有两台服务器 , 服务器127.0.0.1:6379发送下面命令:
127.0.0.1:6379>slaveof 127.0.0.1 7000
此时服务器127.0.0.1:6379会成为服务器127.0.0.1:7000的从服务器(slaver),服务器127.0.0.1:7000会成为服务器127.0.0.1:6379的主服务器(master);
通过复制功能,从服务器数据可以和主服务器数据保持同步。
21.1 主从复制功能实现
主要作用
1)读写分离,单台服务器能支撑的QPS是有上限的,我们可以部署一台主服务器、多台从服务器,主服务器只处理写请求,从服务器通过复制功能同步主服务器数据,只处理读请求,以此提升Redis服务能力;另外我们还可以通过复制功能来让主服务器免于执行持久化操作:只要关闭主服务器的持久化功能,然后由从服务器去执行持久化操作即可。
2)数据容灾,任何服务器都有宕机的可能,我们同样可以通过主从复制功能提升Redis服务的可靠性;由于从服务器与主服务器数据保持同步,一旦主服务器宕机,可以立即将请求切换到从服务器,从而避免Redis服务中断。
slaveof命令的主要流程
Redis 2.8以前实现:
1)从服务器127.0.0.1:6379向主服务器127.0.0.1:7000发送sync命令,请求同步数据。
2)主服务器接收到sync命令请求,开始执行bgsave命令持久化数据到RDB文件,并且在持久化数据期间会将所有新执行的写入命令都保存到一个缓冲区。
3)当持久化数据执行完毕,主服务器将该RDB文件发送给从服务器,从服务器接收并将文件中的数据加载到内存。
4)主服务器将缓冲区中的命令请求发送给从服务器。
5)每当主服务器接收到写命令请求时,都会将命令按照Redis协议格式发送给从服务器,从服务器接收并处理主服务器发送过来的命令请求。
步骤2中持久化操作(bgsave),非常耗费资源的操作;
新的主从复制解决方案:
从服务器会记录已经从主服务器接收到的数据量(复制偏移量);
而主服务器会维护一个复制缓冲区,记录自己已执行且待发送给从服务器的命令请求,
同时还需要记录复制缓冲区第一个字节的复制偏移量。
从服务器请求同步主服务器的命令也改为了psync。
当从服务器连接到主服务器时,会向主服务器发送psync命令请求同步数据,同时告诉已经接收到的复制偏移量,
主服务器判断该复制偏移量是否还包含在复制缓冲区;
如果包含,不需要执行持久化操作,直接向从服务器发送复制缓冲区中命令请求,这称为部分重同步;
如果不包含,执行持久化操作,同时将所有新执行的写命令缓存在复制缓冲区中,并重置复制缓冲区第一个字节的复制偏移量,这称为完整重同步。
另外每台Redis服务器都有一个运行ID,从服务器每次发送psync请求同步数据时,会携带自己需要同步主服务器的运行ID。主服务器接收到psync命令时,需要判断命令参数运行ID自己运行ID是否相等,只有相等才有可能执行部分重同步。而当从服务器首次请求主服务器同步数据时,从服务器不知道主服务器的运行ID,此时运行ID以"?"填充,同时复制偏移量初始为-1。
psync命令格式为 : "psync<MASTER_RUN_ID><OFFSET>",
主从复制初始化流程如图21-1所示。
图21-1 主从复制初始化流程图
从图21-1可以看到,当主服务器判断可以执行部分重同步时向从服务器返回"+CONTINUE";需要执行完整重同步时向从服务器返回"+FULLRESYNC RUN_ID OFFSET",其中RUN_ID为主服务器自己的运行ID,OFFSET为复制偏移量。
执行部分重同步的要求比较严格的:
1)RUN_ID必须相等;
2)复制偏移量必须包含在复制缓冲区中。
在生产环境中,经常会出现以下两种情况:
·从服务器重启(复制信息丢失);
·主服务器故障导致主从切换(从多个从服务器重新选举出一台机器作为主服务器,主服务器运行ID发生改变)。
这时候无法执行部分重同步的,而这两种情况又很常见,因此Redis 4.0针对主从复制又提出了两点优化,提出了psync2协议。
方案1:持久化主从复制信息。
Redis服务器关闭时,将主从复制信息(复制的主服务器RUN_ID与复制偏移量)作为辅助字段存储在RDB文件中;
Redis启动加载RDB文件,恢复主从复制信息,重新同步主服务器时携带持久化主从复制信息 ;
if (rdbSaveAuxFieldStrStr(rdb,"repl-id",server.replid)
== -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"repl-offset",server.master_repl_offset)
== -1) return -1;
方案2:存储上一个主服务器复制信息。
/* Replication (master) */
char replid[CONFIG_RUN_ID_SIZE+1]; /* My current replication ID. */
char replid2[CONFIG_RUN_ID_SIZE+1]; /* 初始化replid2为空字符串*/
long long master_repl_offset; /* My current replication offset */
long long second_replid_offset; /*初始化 -1. */
当主服务器发生故障,自己成为新的主服务器时,便使用replid2和second_replid_offset存储之前主服务器的运行ID与复制偏移量;
void shiftReplicationId(void) {
memcpy(server.replid2,server.replid,sizeof(server.replid));
server.second_replid_offset = server.master_repl_offset+1;
changeReplicationId();
}
判断是否能执行部分重同步的条件也改变为:
if (strcasecmp(master_replid, server.replid) &&
(strcasecmp(master_replid, server.replid2) ||
psync_offset > server.second_replid_offset))
{ ...
goto need_full_resync;
}
假设m为主服务器(运行ID为M_ID),A、B和C为三个从服务器;某一时刻主服务器m发生故障,从服务器A升级为主服务器(同时会记录replid2=M_ID),从服务器B和C重新向主服务器A发送"psync M_ID psync_offset"请求;显然根据上面条件,只要psync_offset满足条件,就可以执行部分重同步。
21.2 主从复制源码基础
主从复制相关变量大部分都定义在redisServer结构体中:
struct redisServer {
/* Replication (master) */
char replid[CONFIG_RUN_ID_SIZE+1]; /* */
Redis服务器运行ID,长度为(40)的随机字符串 . 对于主服务器,replid表示当前服务器的运行ID;对于从服务器,replid表示其复制的主服务器运行ID。生成方法:
void changeReplicationId(void) {
getRandomHexChars(server.replid,CONFIG_RUN_ID_SIZE);
server.replid[CONFIG_RUN_ID_SIZE] = '