redis主从同步

     redis支持简单易用的主从复制(master-slave replication)功能,该功能也是redis高可用性实现的基础。 

  • redis复制原理

     redis的节点都会有一个backlog内存缓冲区用于数据同步,其中slave的backlog缓冲区会一直存在,master的backlog缓冲区当master与最后一个slave断开连接一段时间后就会被free掉。 

     redis的backlog是一个环形缓冲区,feedReplicationBacklog函数由master调用,负责将数据写入到backlog缓冲区中。

///********* redis/src/server.h  ****************
struct redisServer {
.....
    char *repl_backlog;             /* Replication backlog for partial syncs */
    long long repl_backlog_size;    /* Backlog circular buffer size */
    long long repl_backlog_histlen; /* Backlog actual data length */
    long long repl_backlog_idx;     /* Backlog circular buffer current offset,
                                       that is the next byte will'll write to.*/
    long long repl_backlog_off;     /* Replication "master offset" of first
                                       byte in the replication backlog buffer.*/
.....
}
 
///********* redis/src/replication.c  ****************
void feedReplicationBacklog(void *ptr, size_t len) {
    unsigned char *p = ptr;
 
    server.master_repl_offset += len;
 
    /* This is a circular buffer, so write as much data we can at every
     * iteration and rewind the "idx" index if we reach the limit. */
    while(len) {
        size_t thislen = server.repl_backlog_size - server.repl_backlog_idx;
        if (thislen > len) thislen = len;
        memcpy(server.repl_backlog+server.repl_backlog_idx,p,thislen);
        server.repl_backlog_idx += thislen;
        if (server.repl_backlog_idx == server.repl_backlog_size)
            server.repl_backlog_idx = 0;
        len -= thislen;
        p += thislen;
        server.repl_backlog_histlen += thislen;
    }
    if (server.repl_backlog_histlen > server.repl_backlog_size)
        server.repl_backlog_histlen = server.repl_backlog_size;
    /* Set the offset of the first byte we have in the backlog. */
    server.repl_backlog_off = server.master_repl_offset -
                              server.repl_backlog_histlen + 1;
}

           master当收到写操作时,就会调用replicationFeedSlaves函数将这类操作写入到backlog中同时推送给各个slave。

///********* redis/src/replication.c  ****************
 
void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc) {
     .....
 /* Write the command to the replication backlog if any. */
    if (server.repl_backlog) {
        char aux[LONG_STR_SIZE+3];
 
        /* Add the multi bulk reply length. */
        aux[0] = '*';
        len = ll2string(aux+1,sizeof(aux)-1,argc);
        aux[len+1] = '\r';
        aux[len+2] = '\n';
        feedReplicationBacklog(aux,len+3);
 
        for (j = 0; j < argc; j++) {
            long objlen = stringObjectLen(argv[j]);
 
            /* We need to feed the buffer with the object as a bulk reply
             * not just as a plain string, so create the $..CRLF payload len
             * and add the final CRLF */
            aux[0] = '$';
            len = ll2string(aux+1,sizeof(aux)-1,objlen);
            aux[len+1] = '\r';
            aux[len+2] = '\n';
            feedReplicationBacklog(aux,len+3);
            feedReplicationBacklogWithObject(argv[j]);
            feedReplicationBacklog(aux+len+1,2);
        }
    }
     /* Write the command to every slave. */
    listRewind(slaves,&li);
    while((ln = listNext(&li))) {
        client *slave = ln->value;
 
        /* Don't feed slaves that are still waiting for BGSAVE to start */
        if (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_START) continue;
 
        /* Feed slaves that are waiting for the initial SYNC (so these commands
         * are queued in the output buffer until the initial SYNC completes),
         * or are already in sync with the master. */
 
        /* Add the multi bulk length. */
        addReplyMultiBulkLen(slave,argc);
 
        /* Finally any additional argument that was not stored inside the
         * static buffer if any (from j to argc). */
        for (j = 0; j < argc; j++)
            addReplyBulk(slave,argv[j]);
    }
}

      slave节点向master节点申请数据同步时,会附带一个master_replid(避免master重启,master重启后master_replid 会改变,保证数据源唯一)以及 已接收数据的offset。

char master_replid[CONFIG_RUN_ID_SIZE+1];  /* Master PSYNC runid. */
long long master_initial_offset;           /* Master PSYNC offset. */
  • 如果slave节点是新添加或者重启后的,那么就会将offset设置为-1,发送“PSYNC ? -1”给master,master会返回master_replid 、全局的复制offset。然后slave和master就会进行全量重同步。
  • 如果slave与master短时间(在master的backlog没有free之前)断开连接又重新连接,slave会将已获取数据的offset和master_replid通过PSYNC命令发送过去,master收到后会比较接收到master_replid与自身的server.replid是否相同以及请求的psync_offset是否在master保存的backlog的缓冲区范围内;
///********* redis/src/replication.c  ****************  
 
int masterTryPartialResynchronization(client *c) {
......
if (strcasecmp(master_replid, server.replid) &&
        (strcasecmp(master_replid, server.replid2) ||
         psync_offset > server.second_replid_offset))
    {
        /* Run id "?" is used by slaves that want to force a full resync. */
        if (master_replid[0] != '?') {
            if (strcasecmp(master_replid, server.replid) &&
                strcasecmp(master_replid, server.replid2))
            {
                serverLog(LL_NOTICE,"Partial resynchronization not accepted: "
                    "Replication ID mismatch (Slave asked for '%s', my "
                    "replication IDs are '%s' and '%s')",
                    master_replid, server.replid, server.replid2);
            } else {
                serverLog(LL_NOTICE,"Partial resynchronization not accepted: "
                    "Requested offset for second ID was %lld, but I can reply "
                    "up to %lld", psync_offset, server.second_replid_offset);
            }
        } else {
            serverLog(LL_NOTICE,"Full resync requested by slave %s",
                replicationGetSlaveName(c));
        }
        goto need_full_resync;
    }
 
    /* We still have the data our slave is asking for? */
    if (!server.repl_backlog ||
        psync_offset < server.repl_backlog_off ||
        psync_offset > (server.repl_backlog_off + server.repl_backlog_histlen))
    {
        serverLog(LL_NOTICE,
            "Unable to partial resync with slave %s for lack of backlog (Slave request was: %lld).", replicationGetSlaveName(c), psync_offset);
        if (psync_offset > server.master_repl_offset) {
            serverLog(LL_WARNING,
                "Warning: slave %s tried to PSYNC with an offset that is greater than the master replication offset.", replicationGetSlaveName(c));
        }
        goto need_full_resync;
    }
    ........
     //进行部分同步
}

  

  • 全量重同步(SYNC)

          这种同步方式一般是在PSYNC执行失败后触发的。SYNC有两种方式:Disk-backed和Diskless。

  • disk-backed:在接到slave的SYNC请求后,会fork一个子进程用来将内存中的数据写入RDB文件,同时会将新来的请求保存在一个临时的内存缓冲区中。待RDB文件完成后,将临时缓冲区的数据与原有的内存数据进行合并并释放临时缓冲区。在写RDB文件的过程中,新来的SYNC请求都会被放到一个队列中,当RDB文件完成后将RDB文件内容发送给队列中的所有slave。
  • diskless:在接收到slave的SYNC的请求后,会等待一段时间(也可以配置不等待),等待过程中新来的SYNC请求也都会被放到等待队列中。master会与等待队列中的slave建立连接,将数据直接发送给这些slave。(这种方式的优点在于不写RDB文件,避免了磁盘I/O开销,提升了效率)

 

  • 部分同步(PSYNC)

master在收到slave发来的PSYNC请求(异常情况上面已经讨论过了,这里不再考虑),master会比较slave发来offset与master当前backlog中的offset,将backlog中比slave多出的数据传输给slave。(注:目前redis 4.0 提出的PSYNC 2.0本人还没有深入研究,回头有时间将相关的知识分享出来)

 

     为了保证数据的安全、一致性,可以通过配置当slave满足一定条件时才进行set操作。因为redis使用异步写的方式复制,master发送的写数据不一定能够被slave接收到。redis有以下特性:

  • slave每秒都会ping master一次,并report 复制的情况
  • master记录各个slave最后一次ping的timestamp
  • 用户配置允许的网络延迟最大值min-slaves-max-lag,以及执行写操作的slave的数量 min-slaves-to-write

     如果min-slaves-to-write个slave的网络延迟低于min-slaves-max-lag,master就会进行写操作,否则就返回客户端写失败。  

redis 事务(transaction)

     事务可以一次执行多个命令,有两个重要的特征:1、事务是一个单独的隔离操作:事务中的所有命令都会序列化、顺序的执行;事务在执行的过程中不会被其他客户端发来的命令中断。2、事务是一个原子操作,即要么执行完毕要么全部都不执行。

     redis的事务涉及到的命令有:MULTI, EXEC, DISCARD, WATCH。

  • EXEC: 负责触发并执行事务中的所有命令。EXEC命令的回复是一个数组,数组中的每一个元素都是执行事务中的命令所产生的回复,其中回复的顺序和命令发送、执行的先后顺序是一致的。当客户端处于事务状态时,所有传入的命令都会返回一个内容为QUEUED的状态回复。
  • MULTI:用于开启一个事务,总是返回OK;MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令被放到一个队列中,不会被立刻执行,当EXEC命令被调用时,队列中的命令依次被执行。
  • DISCARD:清空任务队列,并放弃执行事务,并且客户端从事务状态中退出。
  • WATCH:为redis事务提供check-and-set(CAS)行为;如果一些key被WATCH,那么对这些key的操作会被监视;如果一个被监视的key在EXEC执行前被修改了,那么整个事务都会被取消,EXEC批量返回空回复(null multi-bulk reply)来表示事务已经失败。单个WATCH命令可以监控多个key。

 

  • 事务错误

               使用事务的过程中可能会遇到错误:

  1. 事务在执行EXEC前,入队的命令可能出错。例如,命令出现语法错误或者其他更为严重的错误,诸如内存超过最大限制之类的错误。
  2. 命令可能在EXEC调用后失败。例如,事务中的命令可能处理了错误类型的键导致事务失败。

       对于第一种事务失败,可以检查命令入队列时的返回值,如果发现有命令在入队时失败,那么大部分客户端就会停止并取消这个事务的。服务器会对命令入队失败的情况进行记录,并在客户端调用EXEC命令时,拒绝执行并自动放弃这个事务。对于EXEC命令执行后产生的错误,并没有对对其进行特殊的处理:即使某个/些命令在执行时产生错误,事务中的其他命令依然会继续执行。

     

  • 回滚(roll back)

           redis并不支持操作回滚。1、redis命令的错误只会因为错误的命令(语法)才失败,这些问题是因为编程错误造成的,应该有开发人员来解决。2、不对回滚进行支持,可以是redis内部保持简单、快速。

 

  • 用CAS(check-and-set)实现乐观锁

          用WATCH来监控要修改的key,然后通过EXEC来执行事务。如果WATCH执行后、事务EXEC前,key被修改,则当前客户端的事务就会失败。程序接下来就会不断重复这个过程,知道事务成功执行为止。这种形式的锁被成为乐观锁。对key的监视从WATCH命令执行开始,到EXEC被调用时(不考虑EXEC的执行结果)结束。UNWATCH可以手动取消对key的监控。

 

  • 脚本和事务

          redis中的脚本也是一种事务,而且比事务来的更简单,并且速度更快。当需要用事务的,推荐用脚本的方式。