复制,表面意思就是一份拷贝,在redis中即为一个实例数据的备份,主要用于数据的跨主机备份,容灾处理,并且也是redis集群的基础。
redis复制的第一版(同步复制)
伴随着redis的诞生而诞生的,即第一个版本0.091就有复制功能。

整体处理逻辑:
- 当配置文件中配置了如下选项时,此redis实例将成为replica
slaveof <masterip> <masterport> - replica启动后,根据配置的master的IP和端口主动连接master
syncWithMaster() { ... anetTcpConnect(NULL,server.masterhost,server.masterport); ... } - 连接上master后,replica主动发送 SYNC命令,等待master的响应(阻塞读)
syncWithMaster() { ... syncWrite(fd,"SYNC \r\n",7,5) ... syncReadLine(fd,buf,1024,5) //读取文件的大小 ... } - master接收到SYNC命令后,进行内存数据持久化到文件(rdb)
syncCommand() { ... rdbSave(server.dbfilename) ... } - master读取rdb文件,获取文件大小,发送文件大小给replica
syncCommand() { ... fd = open(server.dbfilename, O_RDONLY); fstat(fd,&sb) len = sb.st_size; snprintf(sizebuf,32,"$%d\r\n",len); syncWrite(c->fd,sizebuf,strlen(sizebuf),5) ... } - master读取rdb文件内容,发送内容给replica
syncCommand() { ... while(len) { char buf[1024]; int nread; if (time(NULL)-start > REDIS_MAX_SYNC_TIME) goto closeconn; nread = read(fd,buf,1024); if (nread == -1) goto closeconn; len -= nread; if (syncWrite(c->fd,buf,nread,5) == -1) goto closeconn; } ... } - master发送结束符\r\n
syncCommand() { ... syncWrite(c->fd,"\r\n",2,5) close(fd); ... } - master将replica加入到replica队列中
syncCommand() { ... listAddNodeTail(server.slaves,c))//后续master遍历整个队列,知道有哪些replica ... } - replica从网络上读取文件大小
syncWithMaster() { ... syncReadLine(fd,buf,1024,5) dumpsize = atoi(buf+1); ... } - replica读取文件内容,写本地临时文件,最终存储到rdb文件
syncWithMaster() { ... //写入到临时文件中 snprintf(tmpfile,256,"temp-%d.%ld.rdb",(int)time(NULL),(long int)random()); dfd = open(tmpfile,O_CREAT|O_WRONLY,0644); ... while(dumpsize) { int nread, nwritten; //从master中读取 nread = read(fd,buf,(dumpsize < 1024)?dumpsize:1024); ... //写入本地文件 nwritten = write(dfd,buf,nread); ... dumpsize -= nread; } close(dfd); //用临时文件替换rdb文件 rename(tmpfile,server.dbfilename) ... } - replica清空本地数据库数据
syncWithMaster() { ... emptyDb(); ... } - replica加载rdb文件到内存,恢复数据库
syncWithMaster() { ... rdbLoad(server.dbfilename) ... } - master将出发数据更新的命令同步到replica
processCommand() { ... /* Exec the command */ dirty = server.dirty; cmd->proc(c); //如果此命令修改了数据,server.dirty将增加1 if (server.dirty-dirty != 0 && listLength(server.slaves)) replicationFeedSlaves(server.slaves,cmd,c->db->id,c->argv,c->argc); //将此命令发送给replicas ... } replicationFeedSlaves(list *slaves, struct redisCommand *cmd, int dictid, robj **argv, int argc) { listNode *ln = slaves->head; robj *outv[REDIS_MAX_ARGS*4]; /* enough room for args, spaces, newlines */ int outc = 0, j; //构建命令 for (j = 0; j < argc; j++) { if (j != 0) outv[outc++] = shared.space; ... outv[outc++] = argv[j]; } outv[outc++] = shared.crlf; //发送数据 while(ln) { redisClient *slave = ln->value; ... for (j = 0; j < outc; j++) addReply(slave,outv[j]); ln = ln->next; } }
因为是同步复制,所以会有很多性能问题。
- replica每次重启(或者和master链接断开后重连),都需要全量同步
每次replica连接上master同步时,master将阻塞在将内存数据全量写到磁盘文件上,然后再读取文件,最后从磁盘上读取文件,将文件内存发送给replica,在整个处理阶段master是不能响应任何请求的。
replica每次重启时,如果replica开启了持久化,是否可以断点续传?
- 部署多个replica时,master需要为每个replica生成rdb文件
部署多个replica时,master会为每个replica生成rdb文件,即使master没有客户端请求或者内存的数据没有修改。
多个replica在一定时间范围内同时连接上master请求同步,master是否共享一份rdb文件给这些replica
- 每次生成rdb文件都是同步的
每个replica连接到master请求同步时,master都是同步的生成rdb文件,replica大部分都是用来备份数据,不一定需要强一致性,一般都是最终一致性。
master在响应replica同步请求生成rdb文件的过程是否可以异步?
- 每次同步都需要读写文件
每次replica连接上master请求同步时,master都需要将内存数据写到磁盘文件,然后又从磁盘文件读取到内存,发送给replica,经历了两次IO处理。
replica也是将master发送的数据写入本地磁盘文件,然后再读取磁盘文件加载到内存恢复数据,同样经历了两次IO处理。
在全量同步时,master是否可以不写文件,直接将内存数据发送给replica。
在全量同步时,replica接收到master发送的数据时,是否不写文件,直接在内存恢复数据。
- replica和master的状态不能动态切换
replica只能通过配置文件中slaveof进行设置,其中设置了此项的为replica,没有设置的默认为master, 不能动态的通过命令进行切换。
是否可以通过命令进行动态的切换master和replica角色。
redis复制第二版 (异步复制)
redis的0.100版本开始,引入了异步复制。
- master异步生成rdb文件进行发送
- master将在一段时间内同时请求同步的replica,应用同一份rdb文件(即只生成一次rdb)

replica的整个流程依然是同步阻塞的,而master断进行了异步操作
- replica主动连接master
#首先需要配置master的IP,port,此实例将成为replica,主动连接master slaveof <masterip> <masterport>//解析配置文件 loadServerConfig() { ... else if (!strcasecmp(argv[0],"slaveof") && argc == 3) { server.masterhost = sdsnew(argv[1]); server.masterport = atoi(argv[2]); server.replstate = REDIS_REPL_CONNECT; } ... }serverCron() { ... if (server.replstate == REDIS_REPL_CONNECT) { ... syncWithMaster() ... } ... }syncWithMaster() { ... int fd = anetTcpConnect(NULL,server.masterhost,server.masterport); ... } - replica连接上master后,replica发送SYNC命令
syncWithMaster() { ... syncWrite(fd,"SYNC \r\n",7,5) ... } - replica等待读(阻塞),可以发现超时等待时间从5秒变成了3600秒
syncWithMaster() { ... syncReadLine(fd,buf,1024,3600) ... } - master接受replica连接后,处理replica的SYNC命令
- master生成子进程,后台生成rdb文件
syncCommand() { ... if (server.bgsaveinprogress) { //正在生成rdb过程中 redisClient *slave; listNode *ln; //遍历replica对象链表,查找可以共用一份rdb数据的对象 listRewind(server.slaves); while((ln = listYield(server.slaves))) { slave = ln->value; if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_END) break; } if (ln) { ... //找到共享replica对象,将此对象在rdb生成过程中缓存的新的写命令拷贝一份 listRelease(c->reply); c->reply = listDup(slave->reply); ... c->replstate = REDIS_REPL_WAIT_BGSAVE_END; ... } else { //没有找到共用replica对象,需要等待此次rdb生成完成后,再进行rdb的生成 c->replstate = REDIS_REPL_WAIT_BGSAVE_START; ... } } else { //没有rdb在生成,可以即可进行rdb的生成 ... rdbSaveBackground(server.dbfilename) //创建子进程进行处理 ... c->replstate = REDIS_REPL_WAIT_BGSAVE_END; } ... } - 当master的子进程rdb文件生成完成后,master主进程将注册异步调用函数sendBulkToSlave进行文件的发送
serverCron() { ... if (server.bgsaveinprogress) {//有后台子进程生成 ... if (wait4(-1,&statloc,WNOHANG,NULL)) {//捕获子进程结束状态 //子进程结束 ... server.bgsaveinprogress = 0; updateSalvesWaitingBgsave(exitcode == 0 ? REDIS_OK : REDIS_ERR); } } ... } updateSalvesWaitingBgsave(int bgsaveerr) { listNode *ln; int startbgsave = 0; //遍历所有的replica对象 listRewind(server.slaves); while((ln = listYield(server.slaves))) { redisClient *slave = ln->value; if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START) { //如果是等待rdb开始 startbgsave = 1; //后续将开始rdb的生成 slave->replstate = REDIS_REPL_WAIT_BGSAVE_END; } else if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_END) {//等待rdb生成结束 struct stat buf; ... //打开rdb文件 slave->repldbfd = open(server.dbfilename,O_RDONLY) ... //获得文件大小 fstat(slave->repldbfd,&buf) ... slave->repldboff = 0; slave->repldbsize = buf.st_size; slave->replstate = REDIS_REPL_SEND_BULK; //注册异步回调函数sendBulkToSlave,并且设置fd可写,使epoll进行调用 aeDeleteFileEvent(server.el,slave->fd,AE_WRITABLE); aeCreateFileEvent(server.el, slave->fd, AE_WRITABLE, sendBulkToSlave, slave, NULL) ... } //需要生成rdb,开始异步生成rdb if (startbgsave) { rdbSaveBackground(server.dbfilename) ... } } - 当master异步调用注册的发送函数,将文件发送完成后,注册异步回调函数sendReplyToClient,将发送文件期间新的修改数据的命令发送给replica
sendBulkToSlave(aeEventLoop *el, int fd, void *privdata, int mask) { redisClient *slave = privdata; ... char buf[REDIS_IOBUF_LEN]; ssize_t nwritten, buflen; if (slave->repldboff == 0) {//首次发送,先发送文件大小 ... bulkcount = sdscatprintf(sdsempty(),"$%lld\r\n",(unsigned long long) slave->repldbsize); write(fd,bulkcount,sdslen(bulkcount) ... } lseek(slave->repldbfd,slave->repldboff,SEEK_SET); //偏移到已发送位置 buflen = read(slave->repldbfd,buf,REDIS_IOBUF_LEN); //从文件中读取数据 ... //将读取的文件内容发送给replica nwritten = write(fd,buf,buflen) ... //累加发送字节 slave->repldboff += nwritten; if (slave->repldboff == slave->repldbsize) { //文件发送完成 close(slave->repldbfd); //关闭文件 ... //注册异步回调函数sendReplyToClient,将缓存的命令发送给replica aeDeleteFileEvent(server.el,slave->fd,AE_WRITABLE); slave->replstate = REDIS_REPL_ONLINE; aeCreateFileEvent(server.el, slave->fd, AE_WRITABLE, sendReplyToClient, slave, NULL) ... } } - mater将从生成rdb到发送rdb结束期间产生的修改数据的命令发送给replica
sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask) { redisClient *c = privdata; int nwritten = 0, totwritten = 0, objlen; robj *o; ... while(listLength(c->reply)) { o = listNodeValue(listFirst(c->reply)); objlen = sdslen(o->ptr); if (objlen == 0) { listDelNode(c->reply,listFirst(c->reply)); continue; } if (c->flags & REDIS_MASTER) { //如果自己是replica,则不发送响应给master nwritten = objlen - c->sentlen; } else { //master 发送给 replica nwritten = write(fd, ((char*)o->ptr)+c->sentlen, objlen - c->sentlen); ... } c->sentlen += nwritten; totwritten += nwritten; /* If we fully sent the object on head go to the next one */ if (c->sentlen == objlen) { listDelNode(c->reply,listFirst(c->reply)); c->sentlen = 0; } } ... //如果已经全部发送完,则删除对应句柄的写事件,epoll将不在调用此函数 if (listLength(c->reply) == 0) { c->sentlen = 0; aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE); } }
- 虽然master已经异步了,但是replica依然是同步的,这样replica作用不大
- master在对replica缓存命令的时候,每个replica对象都维护了一个链表,如果replica很多,并且同步期间修改数据的命令很多,将非常耗内存
本文详细解读了Redis早期同步复制与0.100版本后引入的异步复制的工作流程,对比两者性能瓶颈,重点探讨了异步复制如何提高效率及存在的挑战,如内存消耗和命令缓存问题。
2478

被折叠的 条评论
为什么被折叠?



