基于前面介绍的 Redis 内容,Redis 只能做为一个单机内存数据库,一旦服务器宕机即不能提供服务,即使能经过持久化策略重启恢复数据,每每也作不到百分之百还原。再一个就是,单机的 Redis 须要处理全部的客户端请求,包括读和写操做,压力很大。java
说了这么多,Redis 固然也提供了解决方案,主从复制技术是实现 Redis 集群的最基本架构,集群中有一台或多台主节点服务器(master),多台从节点服务器(slave),slave 持续不断的同步 master 上的数据。一旦 master 宕机,咱们能够切换 salve 成为新的 master 稳定提供服务,也不用担忧 master 宕机致使的数据丢失。git
下面咱们就一块儿来看看主从复制技术的设计与应用,先看理论再看源码实现。程序员
主从复制技术有两个版本,2.8 之前的版本,设计上有缺陷,在 slave 断线后重连依然须要 master 从新发送 RDB 从新进行数据更新,效率很是低。2.8 版本之后作了从新设计,经过引入偏移量同步,相对而言很是的高效,咱们这里不去讨论旧版本的设计了,直接看新版本的主从复制技术设计。github
每个 Redis 启动后,都会认为本身是一个 master 节点,你能够经过如下命令通知它成为 slave 并向 master 同步数据:redis
slaveof [masterip] [masterport]
复制代码
另外一种方式就是在 Redis 启动配置文件中直接指明让它做为一个 slave 节点启动,并在启动后同步 master 节点数据。配置项和命令是同样的。数据库
若是 master 配置了密码链接,那么还须要在 slave 的配置文件中指明 master 的链接密码:bash
masterauth <password>
复制代码
除此以外,salve 节点默认是只读的,不容许写入数据,由于若是支持写入数据,那么与 master 就没法保持数据一致性,因此咱们通常会把 slave 节点做为读写分离中读服务提供者。固然,你也能够修改是否容许 slave 写入数据:服务器
slave-read-only yes/no
复制代码
固然若是你的 master 宕机了,你须要把某个 slave 上线成 master,你能够经过命令取消 slave 的数据同步,成为单独的一个 master:微信
slaveof no one
复制代码
slave 同步 master 的数据主要分为两个大步骤,全量复制和部分复制。当咱们执行 slaveof 命令的时候,咱们的 slave 会做为一个客户端链接上 master 并向 master 发送 PSYNC 命令。markdown
master 收到命令后,会调用 bgsave fork 一个后台子进程生产 RDB 文件,待合适的时候,在 serverCron 循环的时候发送给 slave节点。
slave 收到 RDB 文件后,丢弃目前内存中全部的数据并阻塞本身,专心作 RDB 读取,数据恢复。
以上就是主从复制的一个全量复制的大概流程,可是一次全量复制并不能永远的保持主从节点数据一致,master 还须要将实时的修改命令同步到从节点才行,这就是部分复制。
在介绍部分复制以前,这里先介绍几个概念。第一个是复制缓冲区(repl_backlog),这是一个 FIFO 的队列,里面存的是最近的一些写命令,大小默认在 1M,复制偏移量(offset),这个偏移量实际上是对应复制缓冲区中的字符偏移。复制缓冲区的结构大体是这样的:
在主从节点完成第一轮全量复制之后,主从节点之间已经初步实现了数据同步,日后的 master,会将收到的每一条写命令发送给 slave 并 添加到复制缓冲区并根据字节数计算更新本身的偏移量,slave 收到传输过来的命令后也同样更新本身的偏移量。
这样,只要主从节点的偏移量相同就说明主从节点之间的数据是同步的。复制缓冲区大小是固定的,新的写命令进来之后,旧的数据就会出队列。若是某个 slave 断线重连以后,依然向 master 发送 PSYNC 命令并携带本身的偏移量,master 判断该偏移量是否还在缓冲区区间内,若是在则直接将该偏移量日后的全部偏移量对应的命令发送给 slave,无需从新进行全量复制。
这是新版同步复制的一个优化的设计,若是该断线重连的 slave 的偏移量已经不在缓冲区区间内,那么说明 master 可能已经没法找到自上次断线后的完整更新记录了,因而进行全量复制并将最新的偏移量发到 slave,算是完成了新的数据同步。
这就是主从复制的一个完整的设计逻辑,设计思路很是的优秀,很值得咱们借鉴,下面咱们看源码的一些实现状况。
serverCron 定时函数中有这么一段代码:
run_with_period(1000) replicationCron();
复制代码
按照默认的 server.hz 配置,每秒就须要执行一次 replicationCron。咱们就来看看这个方法究竟作了什么。
void replicationCron(void) { static long long replication_cron_loops = 0; //slave 链接 master 超时,取消链接 if (server.masterhost && (server.repl_state == REPL_STATE_CONNECTING || slaveIsInHandshakeState()) && (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout) { serverLog(LL_WARNING,"Timeout connecting to the MASTER..."); cancelReplicationHandshake(); } //.rdb 文件响应超时,取消链接 if (server.masterhost && server.repl_state == REPL_STATE_TRANSFER && (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout) { serverLog(LL_WARNING,"Timeout receiving bulk data from MASTER... If the problem persists try to set the 'repl-timeout' parameter in redis.conf to a larger value."); cancelReplicationHandshake(); } //已经创建链接的状况下,某个操做超时,断开链接 if (server.masterhost && server.repl_state == REPL_STATE_CONNECTED && (time(NULL)-server.master->lastinteraction) > server.repl_timeout) { serverLog(LL_WARNING,"MASTER timeout: no data nor PING received..."); freeClient(server.master); } //检查配置项,是否须要向 master 发起链接 if (server.repl_state == REPL_STATE_CONNECT) { serverLog(LL_NOTICE,"Connecting to MASTER %s:%d", server.masterhost, server.masterport); if (connectWithMaster() == C_OK) { serverLog(LL_NOTICE,"MASTER <-> SLAVE sync started"); } } //向 master 发送本身的偏移量 //master 判断是否须要进行命令传播给 slave if (server.masterhost && server.master && !(server.master->flags & CLIENT_PRE_PSYNC)) replicationSendAck(); 。。。。。。。 } 复制代码
由于无论 master 仍是 slave,都是一个服务端的 Redis 程序,他们既能够成为主节点,又能够成为从节点。以上的代码段是当前 redis 做为一个 slave 时须要作的操做,replicationCron 后面的代码是当前 redis 做为一个主节点须要作的处理逻辑。
void replicationCron(void) { 。。。。。 listIter li; listNode *ln; robj *ping_argv[1]; //给全部的 slave 发送 ping if ((replication_cron_loops % server.repl_ping_slave_period) == 0 && listLength(server.slaves)) { ping_argv[0] = createStringObject("PING",4); replicationFeedSlaves(server.slaves, server.slaveseldb, ping_argv, 1); decrRefCount(ping_argv[0]); } //发送 '\n' 给全部正在等待 rdb 文件的 slave,防止他们断定 master 超时 listRewind(server.slaves,&li); while((ln = listNext(&li))) { client *slave = ln->value; int is_presync = (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_START || (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_END && server.rdb_child_type != RDB_CHILD_TYPE_SOCKET)); if (is_presync) { if (write(slave->fd, "\n", 1) == -1) { /* Don't worry about socket errors, it's just a ping. */ } } } //全部的 slave 并断开全部超时的 slave if (listLength(server.slaves)) { listIter li; listNode *ln; listRewind(server.slaves,&li); while((ln = listNext(&li))) { client *slave = ln->value; if (slave->replstate != SLAVE_STATE_ONLINE) continue; if (slave->flags & CLIENT_PRE_PSYNC) continue; if ((server.unixtime - slave->repl_ack_time) > server.repl_timeout) { serverLog(LL_WARNING, "Disconnecting timedout slave: %s", replicationGetSlaveName(slave)); freeClient(slave); } } } //在没有 slave 节点链接后的 N 秒,释放复制缓冲区 if (listLength(server.slaves) == 0 && server.repl_backlog_time_limit && server.repl_backlog && server.masterhost == NULL) { time_t idle = server.unixtime - server.repl_no_slaves_since; if (idle > server.repl_backlog_time_limit) { changeReplicationId(); clearReplicationId2(); freeReplicationBacklog(); serverLog(LL_NOTICE, "Replication backlog freed after %d seconds " "without connected slaves.", (int) server.repl_backlog_time_limit); } } 。。。。。。 } 复制代码
总结一下,replicationCron 默认每一秒调用一次,分为两个部分,本身若是是 slave 节点的话,那么会判断与 master 之间的链接状况,若是等待 rdb 超时或其余链接超时,那么 slave 会断开与 master 的链接,若是发现配置文件中配置了 slaveof ,则会主动链接 master 发送 PSYNC 命令而且会发送本身的偏移量,期待 master 向本身传播命令。
若是本身是一个 master 的话,它会首先向全部的 slave 发送 ping,以避免 slave 由于超时断开与本身的链接,而且还会主动断开一些超时链接的 slave。
除此以外咱们须要补充一点的就是 redis 中很是重要的函数调用 call 函数,这个函数是全部命令对应的实现函数的前置调用。这个函数的具体逻辑我这里暂时不去详细介绍,可是其中有两个重要的步骤你须要明确,一是会调用执行命令的实现函数,二是会将修改命令添加到 AOF 文件并传播给全部的 slave 节点。
这样,咱们关于主从复制的完整逻辑就基本解释通了,以上还只是一个基本的雏形,后面咱们还将基于此介绍高可用的主从复制,借助哨兵(Sentinel)完成主从节点的高可用切换,故障转移等等,敬请期待~
关注公众不迷路,一个爱分享的程序员。 公众号回复「1024」加做者微信一块儿探讨学习! 每篇文章用到的全部案例代码素材都会上传我我的 github github.com/SingleYam/o… 欢迎来踩!