为什么Redis主从模式能保持数据一致。想要知道答案,我们得深入分析Redis实例之间如何进行数据同步。
概述
在具体分析今天的问题之前,我们需要了解 Redis 具有高可靠性,又是什么意思呢?其实,这里有两层含义:一是数据尽量少丢失,二是服务尽量少中断。AOF 和 RDB 保证了前者,而对于后者,Redis 的做法就是增加副本冗余量,将一份数据同时保存在多个实例上。即使有一个实例出现了故障,需要过一段时间才能恢复,其他实例也可以对外提供服务,不会影响业务使用。多实例保存同一份数据,听起来好像很不错,但是,我们必须要考虑一个问题:这么多副本,它们之间的数据如何保持一致呢?数据读写操作可以发给所有的实例吗?实际上,Redis 提供了主从库模式,以保证数据副本的一致,主从库之间采用的是读写分离的方式。
1)读操作:主库、从库都可以接收;
2)写操作:首先到主库执行,然后,主库将写操作同步给从库。
到了这里我们发现几个问题:
1)主从库为什么要采用读写分离的方式;
2)主库如何把数据同步给从库;
3)主从库之前网络断了怎么办;
不过既然Redis具有高可用性,说明这些问题都已经有了答案。
设计
主从库为什么要采用读写分离的方式
你可以设想一下,如果不管是主库还是从库,都能接收客户端的写操作,那么,一个直接的问题就是:如果客户端对同一个数据(例如 k1)前后修改了三次,每一次的修改请求都发送到不同的实例上,在不同的实例上执行,那么,这个数据在这三个实例上的副本就不一致了(分别是 v1、v2 和 v3)。在读取这个数据的时候,就可能读取到旧的值。
如果我们非要保持这个数据在三个实例上一致,就要涉及到加锁、实例间协商是否完成修改等一系列操作,但这会带来巨额的开销,当然是不太能接受的。
主库如何把数据同步给从库
当我们启动多个 Redis 实例的时候,它们相互之间就可以通过 replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系,之后会按照三个阶段完成数据的第一次同步。
数据同步
第一阶段
建立连接,协商同步,psync(runId=?,offest=-1),此时主库会调用FULLRESYNC进行同步。
第二阶段
主库执行 bgsave 命令,生成 RDB 快照文件,接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。这是因为从库在通过 replicaof 命令开始和主库同步前,可能保存了其他数据。为了避免之前数据的影响,从库需要先把当前数据库清空。
可能会有人质疑,主库生成RDB快照文件,不会阻塞主库的读写操作吗?会不会有额外的性能开销?那这些问题我们今天就不展开讨论了,感兴趣的同学可以读下源码。
rdbSaveBackground就是用来处理在后台将数据保存到磁盘上的函数:
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) { pid_t childpid;
if (hasActiveChildProcess()) return C_ERR; ...
if ((childpid = redisFork()) == 0) { int retval;
/* Child */ redisSetProcTitle("redis-rdb-bgsave"); retval = rdbSave(filename,rsi); if (retval == C_OK) { sendChildCOWInfo(CHILD_INFO_TYPE_RDB, "RDB"); } exitFromChild((retval == C_OK) ? 0 : 1); } else { /* Parent */ ... } ...}
第三阶段
增量复制
repl_backlog_buffer
最后,也就是第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。
主从库如果断开连接了,下次建立连接时会通过repl_backlog_buffer环形缓冲区进行增量复制,psync(runId=1,offest=100),offest=100用于标记从库在repl_backlog_buffer中的位置。
总结
为什么Redis主从模式能保持数据一致?
- 采用读写分离,避免加锁、实例间协商是否完成修改等操作,减少不必要的性能损耗;
- 主从实例间通过RDB快照进行数据同步,同步期间主库的写操作额外记录一份到replication buffer中,同步完成时,发送给从库,从库再重新执行这些操作。
- 后续的数据同步通过repl_backlog_buffrt来标记主从实例环形缓冲区中的位置,从库执行这些操作。