Redis高可用(三)- 主从复制

主从复制:将数据从一台机复制到另一台机。前者为主节点(master),后者为从节点(slave);数据的复制是单向的,只能从主节点到从节点。

默认情况下,每台Redis服务器都是主节点,一个主节点可以有多台从节点,一个从节点只能有一个主节点。

一、Redis主从复制作用

复制是单向的,复制就会带来数据一致性问题,无可避免,尽量缩短

  1. 数据冗余:实现了数据的热备,是持久化之外的一种数据冗余方式。
  2. 故障恢复:当主节点出现问题时,可以切换到从节点提供服务,实现故障恢复;实际上是一种服务的冗余。
  3. 负载均衡:读写分离,主写从读,分担服务器负载;在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。但读写分离是对于有磁盘IO阻塞,redis作为缓存可以不用考虑。Redis集群的redis-proxy按权重将读写请求转发到master或者某个read-only replica上。
  4. 高可用基石:主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

二、Redis复制连接建立/断开

1、建立复制连接

主从复制需要在从节点发起,不需要在主节点做任何事情,有三种方式:

  1. 配置文件:在从服务器的配置文件中加入slaveof  masterip  masterport
  2. 启动命令:redis-server启动命令(redis-server ./redis.conf)后加入 --slaveof
  3. 客户端命令:redis-cli客户端执行命令slaveof masterip  masterport,则该Redis实例成为从节点;slaveof是异步命令,所以保存主节点ip和port后直接返回OK,然后再执行复制操作

Redis高可用(三)- 主从复制_第1张图片

 

2、断开复制连接

通过slaveof no one断开,从节点断开链接,不会删除已有的数据,只是不再接受主节点新的数据变化。清除保存的主库的runid,当下一次执行slaveof [IP] [PORT]时就是“是第一次执行复制”。Redis从节点默认是readonly(只读)模式,当断开连接salveof no one就会解除节点只读模式,从节点变回为主节点

Redis高可用(三)- 主从复制_第2张图片

 

3、服务器运行ID(runid)

每个Redis节点(无论主从),启动时都会自动生成一个随机ID(每次启动都不一样),由40个随机的十六进制字符组成;runid用来唯一识别一个Redis节点。通过info Server命令,可以查看节点的runid。

  • 主从节点初次复制时,主节点将自己的runid发送给从节点,从节点将这个runid保存起来;当断线重连时,从节点会将这个runid发送给主节点;主节点根据runid判断能否进行部分复制:
  • 如果从节点保存的runid与主节点的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);
  • 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。

主从复制的三个阶段

连接建立阶段(即准备阶段)、数据同步阶段命令传播阶段

三、连接建立阶段

主从节点之间建立连接。

1、保存主节点信息

从节点执行slaveof 异步命令,存储主节点的ip和port信息,由于slaveof命令是异步命令,客户端直接返回OK。

2、建立socket连接

从节点的复制定时函数replicationCron()每秒调用1次,发现主节点可以连接,便会根据主节点的ip和port,创建socket连接,连接成功后:

从节点:为该socket建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收RDB文件、接收命令传播等。

主节点:接收到从节点的socket连接后(即accept之后),为该socket创建相应的客户端状态,并将从节点看做是连接到主节点的一个客户端,后面的步骤会以从节点向主节点发送命令请求的形式来进行。

3、发送ping命令

从节点成为主节点的客户端之后,发送ping命令进行首次请求:检查socket连接是否可用,以及主节点当前是否能够处理请求,返回结果说明:

  1. 返回pong:socket连接正常,主节点可以处理请求,复制过程继续。
  2. 超时:一定时间后从节点仍未收到主节点的回复,说明socket连接不可用,则从节点断开socket连接,并重连。
  3. 返回pong以外的结果:如果主节点返回其他结果,如正在处理超时运行的脚本,说明主节点当前无法处理命令,则从节点断开socket连接,并重连。

 

 

4、身份验证

requirepass对登录权限做限制,redis每个节点的requirepass可以是独立、不同的。使用密码来保护 Redis 服务器,连接Redis服务器就要使用 AUTH 命令解锁,解锁之后才能使用Redis命令。

masterauth配置在从节点;当master设置requirepass后,对应的slave节点设置mesterauth用于和master的权限校验,在slave节点数据同步的时候用到。

源码分析:redis集群的密码信息存在哪?是如何更新的?关于requirepass和masterauth的问题_damanchen的博客-CSDN博客_masterauth

// redis.conf文件配置
requirepass 1234

// 可以通过命令动态设置配置,但是通过该方式设置的配置在redis服务重启后就会失效,可以通过rewrite命令写入文件
config set requirepass 1234
config set masterauth 1234
// redis>= 2.8.0版本执行,将修改的配置写入到redis.conf文件中
config rewrite

// 设置登录密码后的客户端登录需要进行密码验证
redis-cli -c -p 8000 -a 1234
或者
auth 1234

当从节点设置了masterauth配置,才会向主节点进行身份验证;从节点通过向主节点发送auth命令进行的,auth命令的参数即为配置的masterauth的值。

如果主节点设置密码与从节点masterauth一致(一致是指都存在且密码相同,或者都不存在),则身份验证通过,复制过程继续;如果不一致,则从节点断开socket连接,并重连。

5、发送从节点端口信息

身份验证成功后,从节点会向主节点发送其监听的端口号(前述例子中为6380),主节点保存到该从节点对应的客户端的slave_listening_port字段中;该端口信息除了在主节点中执行info Replication时显示以外,没有其他作用。

四、数据同步阶段

数据同步阶段之前,从节点是主节点的客户端,主节点不是从节点的客户端;而这一阶段及以后,主从节点互为客户端。原因在于:在此之前,主节点只需要响应从节点的请求即可,不需要主动发请求,而在数据同步阶段和后面的命令传播阶段,主节点需要主动向从节点发送请求(如推送缓冲区中的写命令),才能完成复制。

连接建立成功后,从节点向主节点发送psync命令(Redis2.8以前是sync命令),开始同步。根据主从节点当前状态的不同,可以分为全量复制和部分复制

全量复制:用于初次复制或无法进行部分复制的情况,将主节点所有数据发生给从节点,是一个非常重型操作。

部分复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,比全量复制更高效。

1、全量复制

Redis2.8以前,从节点发送sync命令请求同步数据,同步方式是全量复制;

Redis2.8及以后,从节点发送psync命令请求同步数据,根据主从节点当前状态采用全量复制或部分复制。

Redis高可用(三)- 主从复制_第3张图片

PSYNC命令进行全量复制过程:

  1. 根据状态判断是否全部复制。
  2. 主节点收到全量复制命令,fork子进程,执行bgsave,在后台生成RDB文件,并使用复制缓冲区记录从现在开始执行的所有写命令。
  3. 主节点的bgsave执行完成后,将RDB文件发送给从节点;从节点先清除旧数据,然后接收的RDB文件,将数据库状态更新至主节点执行bgsave时的数据库状态。
  4. 主节点发送复制缓冲区数据给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态。从节点清空老数据、载入新RDB文件的过程是阻塞的,无法响应客户端的命令。主节点上会为每个从节点都维护一个复制缓冲区,来保证主从节点间的数据同步。
  5. 如果从节点开启了AOF,则会触发bgrewriteaof的执行,从而保证AOF文件更新至主节点的最新状态。 

 Redis高可用(三)- 主从复制_第4张图片

 psync命令的执行过程:

1、从节点根据当前状态,决定如何调用psync命令,从节点判断是否有主库的runid(之前未执行过slaveof或最近执行了slaveof no one),没有主库runid则为第一次同步,执行psync ? -1;有则发送命令为psync ,其中runid为上次复制的主节点的runid,offset为上次复制截止时从节点保存的复制偏移量。

2、主节点根据收到的psync命令,及当前服务器状态,决定执行全量复制还是部分复制

Redis高可用(三)- 主从复制_第5张图片

 

全量复制为什么是非常重型的操作?

  • 主节点通过bgsave命令fork子进程进行RDB持久化,该过程是非常消耗CPU、内存(页表复制)、硬盘IO的;
  • 主节点将RDB文件发送给从节点,消耗主从节点大量带宽;
  • 从节点清空老数据、载入新RDB文件的过程是阻塞的,无法响应客户端的命令;如果从节点只想bgrewriteaof,也会带来额外消耗。

2、部分复制

Redis高可用(三)- 主从复制_第6张图片

 Redis2.8开始提供部分复制,用于处理网络中断时的数据同步。

复制偏移量:主从节点各维护一个复制偏移量(offset),代表的是主节点向从节点传递的字节数;主节点每次向从节点传播N个字节数据时,主节点的offset增加N;从节点每次收到主节点传来的N个字节数据时,从节点的offset增加N。

复制积压缓冲区由主节点维护的、固定长度的、先进先出(FIFO)队列,默认大小1MB;当主节点开始有从节点时创建,在命令传播阶段,主节点将写命令会发送一份给复制积压缓冲区,作为写命令的备份;除了存储写命令,复制积压缓冲区中还存储了其中的每个字节对应的复制偏移量(offset)。无论主节点有一个还是多个从节点,都只有一个复制积压缓冲区

从节点将offset发送给主节点后,主节点根据offset和缓冲区大小决定能否执行部分复制:

如果offset偏移量之后的数据,仍然都在复制积压缓冲区里,则执行部分复制;

如果offset偏移量之后的数据已不在复制积压缓冲区中(数据已被挤出),则执行全量复制。

Redis高可用(三)- 主从复制_第7张图片

 

五、命令传播阶段

数据同步完成,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。主从节点还维持着心跳机制:PING和REPLCONF ACK。

即Redis主从超时检测机制

  • 主监测从:slave定期发送replconf ack offset命令到master来报告自己的存活状况
  • 从监测主:master定期发送ping命令或者\n命令到slave来报告自己的存活状况

1、主->从 PING

每隔指定的时间,主节点会向从节点发送PING命令,主要是为了让从节点进行超时判断。发送频率由配置repl-ping-slave-period 10,默认为10s,如果超时则断开连接

源码实现:定期执行函数serverCron会执行replicationCron方法,该方法内部执行的动作包括重连接主服务器向主服务器发送 ACK(主检测从的存活性) 、判断数据发送失败情况、断开本服务器超时的从服务器,向从服务器发送PING或者\n命令(从检测主的存活性)。

// redis源码:https://github.com/redis/redis/blob/3.2/src/replication.c#L2245
// 复制 cron 函数,每秒调用一次
void replicationCron(void) {
    // 尝试连接主机,但是超时
    /* Bulk transfer I/O timeout? */
    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();
    }
    
    // 向从服务器发送PING
    /* First, send PING according to ping_slave_period. */
    if ((replication_cron_loops % server.repl_ping_slave_period) == 0) {
        ping_argv[0] = createStringObject("PING",4);
        replicationFeedSlaves(server.slaves, server.slaveseldb,
            ping_argv, 1);
        decrRefCount(ping_argv[0]);
    }
}

Redis高可用(三)- 主从复制_第8张图片

2、从->主:REPLCONF ACK

命令传播阶段,从向主发送REPLCONF ACK命令,频率是每秒1次;命令格式为:REPLCONF ACK {offset},其中offset指从节点保存的复制偏移量,作用:

1、实时监测从节点网络状态:主节点用于复制超时的判断。在主节点中使用info Replication,可以看到其从节点的状态中的lag值,代表主节点上次收到该REPLCONF ACK命令的时间间隔,在正常情况下,该值应该是0或1,如下图所示:

 2、检测命令丢失:从节点发送自身的offset,主节点与自己的offset对比,如果从节点数据缺失(如网络丢包),主节点会推送缺失的数据(这里也会利用复制积压缓冲区)。注意,offset和复制积压缓冲区,不仅可以用于部分复制,也可以用于处理命令丢失等情形;区别在于前者是在断线重连后进行的,而后者是在主从节点没有断线的情况下进行的。

3、辅助保证从节点的数量和延迟:主节点使用min-slaves-to-write和min-slaves-max-lag参数,来保证主节点在不安全的情况下不会执行写命令;所谓不安全,是指从节点数量太少,或延迟过高。例如min-slaves-to-write和min-slaves-max-lag分别是3和10,含义是如果从节点数量小于3个,或所有从节点的延迟值都大于10s,则主节点拒绝执行写命令。而这里从节点延迟值的获取,就是通过主节点接收到REPLCONF ACK命令的时间来判断的,即前面所说的info Replication中的lag值。

// redis源码:https://github.com/redis/redis/blob/3.2/src/server.c#L1282
// serverCron中执行replicationCron方法
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
     
     /* Replication cron function -- used to reconnect to master,
     * detect transfer failures, start background RDB transfers and so forth. */
    run_with_period(1000) replicationCron();
}

// redis源码:https://github.com/redis/redis/blob/3.2/src/replication.c#L2245
// replicationCron方法
void replicationCron(void) {
    
    // 向主服务器发送 ACK
    /* Send ACK to master from time to time.
     * Note that we do not send periodic acks to masters that don't
     * support PSYNC and replication offsets. */
    if (server.masterhost && server.master &&
        !(server.master->flags & CLIENT_PRE_PSYNC))
        replicationSendAck();
    
    

    /* Second, send a newline to all the slaves in pre-synchronization
     * stage, that is, slaves waiting for the master to create the RDB file.
     * The newline will be ignored by the slave but will refresh the
     * last-io timer preventing a timeout. In this case we ignore the
     * ping period and refresh the connection once per second since certain
     * timeouts are set at a few seconds (example: PSYNC response). */
    listRewind(server.slaves,&li);
    while((ln = listNext(&li))) {
        client *slave = ln->value;

        if (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_START ||
            (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_END &&
             server.rdb_child_type != RDB_CHILD_TYPE_SOCKET))
        {
            if (write(slave->fd, "\n", 1) == -1) {
                /* Don't worry, it's just a ping. */
            }
        }
    }
    
    /* Disconnect timedout slaves. */
    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);
            }
        }
    }
    
}

六、各阶段复制的优化

1、第一次建立复制

会进行全量复制:如主节点数据量大,应避开流量高峰期,避免阻塞;多个从节点时,错开连接,避免主节点带宽占用过大。从节点过多,可从一主多从变为树状结构,但是多层从节点延迟变大,数据一致性变差。

2、主节点重启

主节点重启,runid会发生变化,会导致全量复制,复制风暴

debug reload:重启后,主节点的runid和offset都不受影响,避免了全量复制。在一些场景,可能希望对主节点进行重启,例如主节点内存碎片率过高,或者希望调整一些只能在启动时调整的参数。如果使用普通的手段重启主节点,会使得runid发生变化,可能导致不必要的全量复制。

Redis高可用(三)- 主从复制_第9张图片

存碎片率可通过 info memory命令查看,mem_fragmentation_ratio项为碎片率。另外可以通过memory purge命令(4.0及以上版本)手动清理。 

3、从节点重启

从节点宕机重启后,其保存的主节点的runid会丢失,因此即使再次执行slaveof,也无法进行部分复制。

4、网络中断

  • 网络问题时间极短,只造成了短暂的丢包,主从节点没判定超时(未触发repl-timeout);此时只需要通过REPLCONF ACK来补充丢失的数据即可。
  • 网络问题时间很长,主从节点判断超时(触发了repl-timeout),且丢失的数据超过了复制积压缓冲区所能存储的范围;此时主从节点无法进行部分复制,只能进行全量复制。为了尽可能避免这种情况的发生,应该根据实际情况适当调整复制积压缓冲区的大小;此外及时发现并修复网络中断,也可以减少全量复制。
  • 介于前述两种情况之间,主从节点判断超时,但丢失的数据仍然在复制积压缓冲区;此时主从节点可以进行部分复制。

七、复制相关的配置

1、主节点相关配置

  • repl-diskless-sync no:作用于全量复制阶段,控制主节点是否使用diskless复制(无盘复制)。所谓diskless复制,是指在全量复制时,主节点不再先把数据写入RDB文件,而是直接写入slave的socket中,整个过程中不涉及硬盘;diskless复制在磁盘IO很慢而网速很快时更有优势。需要注意的是,截至Redis3.0,diskless复制处于实验阶段,默认是关闭的。
  • repl-diskless-sync-delay 5:该配置作用于全量复制阶段,当主节点使用diskless复制时,该配置决定主节点向从节点发送之前停顿的时间,单位是秒;只有当diskless复制打开时有效,默认5s。之所以设置停顿时间,是基于以下两个考虑:(1)向slave的socket的传输一旦开始,新连接的slave只能等待当前数据传输结束,才能开始新的数据传输 (2)多个从节点有较大的概率在短时间内建立主从复制。
  • client-output-buffer-limit slave 256MB 64MB 60:与全量复制阶段主节点的缓冲区大小有关,见前面的介绍。
  • repl-disable-tcp-nodelay no:与命令传播阶段的延迟有关,见前面的介绍。
  • masterauth :与连接建立阶段的身份验证有关,见前面的介绍。
  • repl-ping-slave-period 10:与命令传播阶段主从节点的超时判断有关,见前面的介绍。
  • repl-backlog-size 1mb:复制积压缓冲区的大小,见前面的介绍。
  • repl-backlog-ttl 3600:当主节点没有从节点时,复制积压缓冲区保留的时间,这样当断开的从节点重新连进来时,可以进行部分复制;默认3600s。如果设置为0,则永远不会释放复制积压缓冲区。
  • min-slaves-to-write 3与min-slaves-max-lag 10:规定了主节点的最小从节点数目,及对应的最大延迟,见前面的介绍。

2、从节点相关配置

  • slave-serve-stale-data yes:与从节点数据陈旧时是否响应客户端命令有关,见前面的介绍。
  • slave-read-only yes:从节点是否只读;默认是只读的。由于从节点开启写操作容易导致主从节点的数据不一致,因此该配置尽量不要修改。

3、主从都相关配置

  • slaveof :Redis启动时起作用;作用是建立复制关系,开启了该配置的Redis服务器在启动后成为从节点。该注释默认注释掉,即Redis服务器默认都是主节点。
  • repl-timeout 60:与各个阶段主从节点连接超时判断有关,见前面的介绍。

4、info Replication

info Replication可以查看与复制相关的状态,对于了解主从节点的当前状态,以及解决出现的问题都会有帮助。

Redis高可用(三)- 主从复制_第10张图片

 

八、相关问题

1、延迟与不一致

命令传播是异步的,主节点发送写命令不会等待从节点的回复;因此主从节点之间很难保持实时的一致性。数据不一致的程度,与主从节点之间的网络状况、主节点写命令的执行频率、以及主节点中的repl-disable-tcp-nodelay配置等有关。

repl-disable-tcp-nodelay no:该配置作用于命令传播阶段,控制主节点是否禁止与从节点的TCP_NODELAY;默认no,即不禁止TCP_NODELAY。当设置为yes时,TCP会对包进行合并从而减少带宽,但是发送的频率会降低,从节点数据延迟增加,一致性变差;具体发送频率与Linux内核的配置有关,默认配置为40ms。当设置为no时,TCP会立马将主节点的数据发送给从节点,带宽增加但延迟变小。

从节点与主节点断开链接后,从节点的slave-serve-stale-data参数:它控制这种情况下从节点的表现;如果为yes(默认值),则从节点仍能够响应客户端的命令,如果为no,则从节点只能响应info、slaveof等少数命令。该参数的设置与应用对数据一致性的要求有关;如果对数据一致性要求很高,则应设置为no。

2、数据过期问题

单机redis两种删除策略,在主从架构中,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除,当客户端通过Redis从节点读取数据时,很容易读取到已经过期的数据。Redis 3.2中,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端;将Redis升级到3.2可以解决数据过期问题。

惰性删除:服务器不会主动删除数据,只有当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除。

定期删除:服务器执行定时任务删除过期数据,但是考虑到内存和CPU的折中(删除会释放内存,但是频繁的删除操作对CPU不友好),该删除的频率和执行时间都受到了限制。

3、复制超时问题

主从复制超时判断的核心,在于repl-timeout参数,该参数规定了超时时间的阈值(默认60s),对于主节点和从节点同时有效,超时后会断开链接,避免无效从节点(min-slaves-to-write)判断,已经减少各种资源浪费(缓冲区、带宽、连接等);

  • 主节点:每秒1次调用复制定时函数replicationCron(),在其中判断当前时间距离上次收到各个从节点REPLCONF ACK的时间,是否超过了repl-timeout值,如果超过了则释放相应从节点的连接。
  • 从节点:从节点对超时的判断同样是在复制定时函数中判断,基本逻辑是:

当前处于连接建立阶段,且距离上次收到主节点的信息的时间已超过repl-timeout,则释放与主节点的连接;

当前处于数据同步阶段,且收到主节点的RDB文件的时间超时,则停止数据同步,释放连接;

当前处于命令传播阶段,且距离上次收到主节点的PING命令或数据的时间已超过repl-timeout值,则释放与主节点的连接。

避免超时优化:

  • 数据同步阶段:在主从节点进行全量复制bgsave时,主节点需要首先fork子进程将当前数据保存到RDB文件中,然后再将RDB文件通过网络传输到从节点。如果RDB文件过大,主节点在fork子进程+保存RDB文件时耗时过多,可能会导致从节点长时间收不到数据而触发超时;此时从节点会重连主节点,然后再次全量复制,再次超时,再次重连……这是个悲伤的循环。为了避免这种情况的发生,除了注意Redis单机数据量不要过大,另一方面就是适当增大repl-timeout值,具体的大小可以根据bgsave耗时来调整。
  • 命令传播阶段:如前所述,在该阶段主节点会向从节点发送PING命令,频率由repl-ping-slave-period控制;该参数应明显小于repl-timeout值(后者至少是前者的几倍)。否则,如果两个参数相等或接近,网络抖动导致个别PING命令丢失,此时恰巧主节点也没有向从节点发送数据,则从节点很容易判断超时。
  • 慢查询导致的阻塞:如果主节点或从节点执行了一些慢查询(如keys *或者对大数据的hgetall等),导致服务器阻塞;阻塞期间无法响应复制连接中对方节点的请求,可能导致复制超时。

4、复制风暴

大量从节点对同一主节点或同一机器的多个主节点短时间内发起全量复制的过程。复制风暴会导致主节点造成CPU、内存、宽带的大量消耗。因此我们应避免复制风暴的发生。

单节点复制风暴:主节点master挂掉重启,导致runid变化,所有slave从节点都要做一次全量复制。

  • 采用树状结构降低多个从节点对主节点的消耗;
  • debug reload重启,主节点的runid和offset都不受影响,避免了全量复制。

单机器复制风暴:多个主节点master部署在同一台机器上时,机器宕机之后,会产生大量复制。

  • 主节点尽量分散在多台机器上
  • 主节点所在机器故障后提供故障转移机制

你可能感兴趣的:(Redis系列,redis,缓存,数据库,java)