Skip to content

Latest commit

 

History

History
1133 lines (1016 loc) · 51.2 KB

2.Redis 持久化集群原理.md

File metadata and controls

1133 lines (1016 loc) · 51.2 KB

目录

Redis 单线程和高性能

通常讲 redis 单线程指的是 Redis 的网络 I/O 和指令读写是由一个线程完成的,至于其他的任务,比如持久化、数据同步等就是异步完成的;

虽然网络 I/O 和指令读写是单线程,但是 Redis 是在内存中工作,因此即使是单线程,执行效率也是很高的;其次,Redis I/O 模型采用 epoll 来实现 多路复用,这也是 Redis 能处理那么多并发客户连接的原因;(I/O 模型详见 NIO 章节NIO)

Redis 持久化

刚才也提到过,Redis 是工作在内存中的,所有的数据也在内存中。所以一旦服务器宕机,Redis 缓存的数据就会全部丢失。因此 Redis 针对于这种情况实现 了两种持久化机制:RDB(快照:全量备份)、AOF(增量备份)。

RDB 快照

在默认情况下,Redis 将内存数据快照保存在名为 dump.rdb 文件中。可以通过修改 redis.conf 文件参数 save 来设置多长时间内,修改多少次进行一次 保存。如save 60 1000,60秒内至少有1000个键被修改,就会自动保存一次。

  1. 数据丢失问题:这个保存很明显会暴露出一个问题,就是在达到这个触发节点的时候,Redis 宕机,数据就会丢失。
  2. 时点性问题:写入数据需要时间,比如 8点开始写入数据,需要4s写入,那么最终存入磁盘的数据是哪一刻的呢?
  3. 效率问题:当客户端向服务器发送save命令请求进行持久化时,服务器会阻塞save命令之后的其他客户端的请求,直到数据同步完成。

先来谈谈 2、3 问题。Redis 为了解决这样的问题,采用操作系统的写时复制技术(Copy-On-Write,COW),就是从父进程中 fork 一个子进程,子进程完全可以共享 父进程的数据,如果父进程是对数据进行读操作,对于子进程没有影响,直接向磁盘写入数据。如果是写操作,那么父进程会将那一刻的数据复制出一份出来,然后在新的 数据上进行修改,而子进程写入的数据还是操作数据之前那一刻的数据。因此问题 2 中写入的数据是 fork 子进程那一刻的。

AOF(增量备份)

AOF 就是为了解决 RDB 数据丢失的问题。AOF 会将修改的每一条指令记录进文件appendonly.aof中(先写入os cache,每隔一段时间 fsync到磁盘)。可以 通过参数# appendonly yes 进行控制。打开这个机制后,每当 redis 修改一个数据集,这个命令就会被追加到这个文件的末尾。这样即使 redis 宕机, 也可以通过 aof 文件进行数据恢复。同样 redis 也提供了配置策略:

appendfsync always # 每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。
appendfsync everysec # 每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据。
appendfsync no # 从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。

fsync

Linux 的 glibc 提供了 fsync(int fd)函数可以将指定文件的内容强制从内核缓存刷到磁 盘。只要 Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失。但是 fsync 是一个 磁盘 IO 操作,它很慢!如果 Redis 执行一条指令就要 fsync 一次,那么 Redis 高性能的 地位就不保了。 所以在 生产环境的服务器中,Redis 通常是每隔 1s 左右执行一次 fsync 操作,周期 1s 是可以配置的。这是在数据安全性和性能之间做了一个折中,在保持 高性能的同时,尽可能 使得数据少丢失。

redis 在业务长期运行期间,AOF的日志肯定会变得很长很长🤮。如果实例宕机,那么使用 AOF 恢复的时候,会导致 Redis 长时间无法对外提供服务,这显然 是不能容忍的。所以需要对 AOF 进行瘦身。

AOF 日志瘦身

Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令, 序列化到一个新的 AOF 日志文件中。做法就是去掉相同键值无用的指令,只保持对该 key 操作的最新指令。
在子进程进行AOF重启期间,Redis主进程执行的命令会被保存在AOF重写缓冲区里面,这个缓冲区在服务器创建子进程之后开始使用,当Redis执行完一个写命令之后,它会同时将这个写命令发送给 AOF缓冲区和AOF重写缓冲区。
这个操作过程中也会导致主线程阻塞。下面分析一下阻塞原因:
当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接收到该信号之后,会调用一个信号处理函数,并执行以下工作:

  1. 将AOF重写缓冲区中的所有内容写入到新的AOF文件中,保证新 AOF文件保存的数据库状态和服务器当前状态一致。
  2. 对新的AOF文件进行改名,原子地覆盖现有AOF文件,完成新旧文件的替换
  3. 继续处理客户端请求命令。

在整个AOF后台重写过程中,只有信号处理函数执行时会对 Redis主进程造成阻塞,在其他时候,AOF后台重写都不会阻塞主进程,如下图所示:
AOF 重写阻塞图:

小结

  1. 快照:快照是通过开启子进程的方式进行的,它是一个比较耗资源的操作,遍历整个内存,大块写磁盘会加重系统负载;
  2. AOF:fsync 是一个耗时的 IO 操作,它会降低 Redis 性能,同时也会增加系统 IO 负担;

所以通常 Redis 的主节点是不会进行持久化操作,持久化操作主要在从节点进行。从节点是备份节点,没有来自客户端请求的压力,它的操作系统资源 往往比较充沛。

Redis 4.0 混合持久化

上面提到过,RDB 容易丢失大量数据,一般使用 aof 恢复,但是 aof 恢复数据相比于 rdb 来说要慢很多,在 redis 实例较大的情况下,启动很慢。为了 解决这样的问题,Redis 4.0 带来了一个新的持久化选项——混合持久化。将 RDB 文件和增量的 aof 日志文件放到一起。注意这里的 aof 日志不再是全量的 日志了,而是自持久化开始到持久化结束这一段时间日志的增量。

Redis 数据备份策略

  1. 写crontab定时调度脚本,每小时都copy一份rdb或aof的备份到一个目录中去,仅仅保留最近48 小时的备份
  2. 每天都保留一份当日的数据备份到一个目录中去,可以保留最近1个月的备份
  3. 每次copy备份的时候,都把太旧的备份给删了
  4. 每天晚上将当前机器上的备份复制一份到其他机器上,以防机器损坏

Redis 主从架构

主从配置

主节点配置信息如下:

# 指定本机通过本机的哪一个 ip 地址来接收外部请求,而不是允许那个 ip 访问
bind 0.0.0.0
# 关闭保护模式
protected-mode no
# 设置 port 端口
port 6379
# 设置后台运行
daemonize yes
# 保存 redis 运行的 pid, 进程结束后会自动删除
pidfile /var/run/redis_6379.pid
# 生成日志文件名称
logfile "redis_6379.log"
# 指定生成备份数据的文件目录
dir /root/Dev_Azh/redis/data/6379

从节点配置如下:

# 指定本机通过本机的哪一个 ip 地址来接收外部请求,而不是允许那个 ip 访问
bind 0.0.0.0
# 关闭保护模式
protected-mode no
# 设置 port 端口
port 6380
# 设置后台运行
daemonize yes
# 保存 redis 运行的 pid, 进程结束后会自动删除
pidfile /var/run/redis_6380.pid
# 生成日志文件名称
logfile "redis_6380.log"
# 指定生成备份数据的文件目录
dir /root/Dev_Azh/redis/data/6380
# 指定主节点连接 ip
replicaof 10.211.55.3 6379

第二个从节点配置如下:

# 指定本机通过本机的哪一个 ip 地址来接收外部请求,而不是允许那个 ip 访问
bind 0.0.0.0
# 关闭保护模式
protected-mode no
# 设置 port 端口
port 6381
# 设置后台运行
daemonize yes
# 保存 redis 运行的 pid, 进程结束后会自动删除
pidfile /var/run/redis_6381.pid
# 生成日志文件名称
logfile "redis_6381.log"
# 指定生成备份数据的文件目录
dir /root/Dev_Azh/redis/data/6381
# 指定主节点连接 ip
replicaof 10.211.55.3 6379

关闭防火墙

# 查看防火墙状态
firewall-cmd --state
# 停止防火墙
systemctl stop firewalld.service
# 禁止防火墙开机启动
systemctl disable firewalld.service 

开始启动实例

# 启动主节点实例
redis-server redis_6379.conf 
# 查看服务是否启动成功
ps ‐ef | grep redis
# 结果:
root      1599     1  0 20:27 ?        00:00:00 redis-server 0.0.0.0:6379

# 启动从节点实例
redis-server redis_6380.conf 
redis-server redis_6381.conf

# 在主节点上使用客户端连接 redis,并设置数据
redis-cli -p 6379
# 设置数据
127.0.0.1:6379> set master:10.211.55.3 "master"
# 此时连接两个从节点客户端,查看数据是否同步
redis-cli -p 6380
127.0.0.1:6380> keys *
# 输出结果:
1) "master:10.211.55.3"

redis-cli -p 6381
127.0.0.1:6381> keys *
1) "master:10.211.55.3"

至此主从结构搭建成功。

Reids 主从工作原理

主从复制过程大体可以分为3个阶段:连接建立阶段(即准备阶段)、数据同步阶段、命令传播阶段。

  1. 连接建立阶段:
    1. 先在缓存中保存 master 节点信息;日志信息如下:
    efore turning into a replica, using my master parameters to synthesize a cached master: I may be able to synchronize 
    with the new master with just a partial transfer
    1. 建立 socket 长连接;日志信息如下:
    Connecting to MASTER 10.211.55.3:6379
    MASTER <-> REPLICA sync started
    1. 发送 ping 命令确认主机网络畅通;
    Master replied to PING, replication can continue...
  2. 数据同步阶段:
    1. 从节点启动,总会向 master 发送一个 psync(2.8以前发送 sync 命令) 命令到 master 请求复制数据;
    2. 主节点收到 psync 命令会在后台进行数据持久化通过bgsave生成最新的rdb快照文件, 持久化期间仍然会接收客户端的请求;
    3. 在持久化期间,master会将接收到可能修改数据集的命令缓存到内存中;
    4. master 持久化完成后,会将 rdb 文件数据集发给 slave 节点,slave 会先将这些数据生成 rdb 文件,然后再加载到内存中;
    5. 接着master会将之前加载到缓存中的命令发送给 slave
  3. 命令传播阶段:
    1. 数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。

注意:当master与slave之间的连接由于某些原因而断开时,slave能够自动重连Master,如果master收到了多 个slave并发连接请求,它只会进行一 次持久化,而不是一个连接一次,然后再把这一份持久化的数据发送 给多个并发连接的slave。

主从同步原理图(全量数据复制):

数据部分复制

从节点断开与主节点的连接,再次连接的时候,一般都会对数据进行全量复制。从redis2.8开始,master和它所有的 slave都维护了复制的数据下标 offset和master的进程id,因此,当网络连接断开后,slave会请求master 继续进行未完成的复制,从所记录的数据下标开始。如果master进程id变化了, 或者从节点数据下标 offset 太旧,已经不在master的缓存队列里了,那么将会进行一次全量数据的复制。这种机制就叫断点续传

如图所示: 注意:如果有很多从节点,为了缓解主从复制风暴(多个从节点同时复制主节点导致主节点压力过大),可以做如下架构,让部分从节点与从节点(与主节点同步) 同步数据。

Jedis 连接代码实例

引入相关依赖

   <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.9.0</version>
   </dependency>

程序 demo

public class JedisDemo {
    public static void main(String[] args) throws IOException {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(20);
        jedisPoolConfig.setMaxIdle(10);
        jedisPoolConfig.setMinIdle(5);

        // timeout,这里既是连接超时又是读写超时,从Jedis 2.8开始有区分connectionTimeout和soTimeo t的构造函数
        JedisPool jedisPool = new JedisPool(jedisPoolConfig, "10.211.55.3", 6379, 3000, null);
        Jedis jedis = null;
        
        try{
            //从redis连接池里拿出一个连接执行命令
            jedis = jedisPool.getResource();
            System.out.println(jedis.set("single", "zhuge"));
            System.out.println(jedis.get("single"));
            //管道示例
            //管道的命令执行方式:cat redis.txt | redis‐cli ‐h 127.0.0.1 ‐a password ‐ p 6379 ‐‐pipe
            /*Pipeline pl = jedis.pipelined();
            for(inti=0;i<10;i++){
                pl.incr("pipelineKey");
                pl.set("zhuge" + i, "zhuge");
            }
             */
            //lua脚本模拟一个商品减库存的原子操作
            //lua脚本命令执行方式:redis‐cli ‐‐eval /tmp/test.lua , 10
            /*jedis.set("product_count_10016", "15"); //初始化商品10016的库存*/
            String script = " local count = redis.call('get', KEYS[1]) " + " local a = tonumber(count) " +
                    " local b = tonumber(ARGV[1]) " + "ifa>=bthen" + " redis.call('set', KEYS[1], a‐b) " +
                    "end" + " return 0 ";
            Object obj = jedis.eval(script, Arrays.asList("product_count_10016"), Arrays.asList("10"));
            System.out.println(obj);
        }catch (Exception e){
            //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
            if (jedis != null){
                jedis.close();
            }
        }
    }
}

管道(Pipeline)

客户端可以一次性发送多个请求而不用等待服务器的响应,待所有命令都发送完后再一次性读取服务的响应,这样可以极大的降低多条命令执行的网络传输 开销,管道执行多条命令的网络开销实际上只相当于一次命令执行的网络开销。需要注意到是用pipeline方式打包命令发送,redis必须在处理完所有命令 前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。pipeline中发送的每个command都会被 server立即执行,如果执行失败,将会在此后的响应中得到信息;也就是pipeline并不是表达“所有command都一起成功”的语义,管道中前面命令失败, 后面命令不会有影响,继续执行。

package com.anzhi.masterslavejedis;

public class JedisMasterSlaveDemo {
    public static void main(String[] args) throws IOException {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(20);
        jedisPoolConfig.setMaxIdle(10);
        jedisPoolConfig.setMinIdle(5);

        // timeout,这里既是连接超时又是读写超时,从Jedis 2.8开始有区分connectionTimeout和soTimeo t的构造函数
        JedisPool jedisPool = new JedisPool(jedisPoolConfig, "10.211.55.3", 6379, 3000, null);
        Jedis jedis = null;
        
        try{
            //从redis连接池里拿出一个连接执行命令
            jedis = jedisPool.getResource();
            System.out.println(jedis.set("single", "zhuge"));
            System.out.println(jedis.get("single"));
            //管道示例
            //管道的命令执行方式:cat redis.txt | redis‐cli ‐h 127.0.0.1 ‐a password ‐ p 6379 ‐‐pipe
            Pipeline pl = jedis.pipelined();
            for(int i=0;i<10;i++){
                pl.incr("pipelineKey");
                pl.set("zhangsan" + i, "zhuge");
                // 模拟管道报错
                pl.setbit("zhangsan", -1, true);
            }
            List<Object> results=pl.syncAndReturnAll();
            System.out.println(results);
        }catch (Exception e){
            //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
            if (jedis != null){
                jedis.close();
            }
        }
    }
}

Redis 2.6 推出了脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行。使用脚本的好处如下:

  1. 减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器 上完成。使用脚本,减少了网络往返时延。这点跟管道类似。
  2. 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。管道不是原子的,不过 redis的批量操作命令(类似mset)是原子的。
  3. 替代redis的事务功能:redis自带的事务功能很鸡肋,而redis的lua脚本几乎实现了常规的事务功能, 官方推荐如果要使用redis的事务功能可以用redis lua替代。

Redis 哨兵高可用架构

主从架构一旦主节点挂了,就需要人工去维护,这肯定是我们不能接受的。Redis 有专门的 Sentinel 监控机制。下面来搭建一下哨兵集群。 配置 sentinel.conf 文件

主节点的 sentinel 配置

# port 端口修改
port 26379
# 设置后台运行
daemonize yes
# 设置保存 redis-sentinel 进程id的文件名称
pidfile /var/run/redis-sentinel-26379.pid
# 设置保存日志文件的名称
logfile "redis-sentinel-26379.log"
# 设置保存数据的文件目录
dir /root/Dev_Azh/redis/data/26379
# 设置要监控的主节点
sentinel monitor mymaster 10.211.55.3 6379 2

从节点 sentinel 的配置

# port 端口修改
port 26380
# 设置后台运行
daemonize yes
# 设置保存 redis-sentinel 进程id的文件名称
pidfile /var/run/redis-sentinel-26380.pid
# 设置保存日志文件的名称
logfile "redis-sentinel-26380.log"
# 设置保存数据的文件目录
dir /root/Dev_Azh/redis/data/26380
# 设置要监控的主节点
sentinel monitor mymaster 10.211.55.3 6379 2

# port 端口修改
port 26381
# 设置后台运行
daemonize yes
# 设置保存 redis-sentinel 进程id的文件名称
pidfile /var/run/redis-sentinel-26381.pid
# 设置保存日志文件的名称
logfile "redis-sentinel-26381.log"
# 设置保存数据的文件目录
dir /root/Dev_Azh/redis/data/26381
# 设置要监控的主节点
sentinel monitor mymaster 10.211.55.3 6379 2

上面我们已经配置好了主从节点的集群,那个配置都不需要改。在启动主从集群之后,再启动 sentinel 监控

# 启动主节点监控
redis-sentinel sentinel_26379.conf
redis-sentinel sentinel_26380.conf 
redis-sentinel sentinel_26381.conf

# 查看是否启动成功:
ps -ef | grep redis
root      1772     1  0 21:09 ?        00:00:04 redis-sentinel *:26379 [sentinel]
root      1880     1  0 21:27 ?        00:00:00 redis-server 0.0.0.0:6379

# 客户端连接 sentinel 服务
redis-cli -p 26379
# 用 info 查看信息
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=10.211.55.4:6380,slaves=2,sentinels=3

可以看到当前主节点是 10.211.55.4, 那么将主节点挂掉,验证故障自动恢复:

# 在主节点对应的虚机上将redis服务 kill
ps -ef | grep redis
root      1610     1  0 20:28 ?        00:00:06 redis-server 0.0.0.0:6380
root      1768     1  0 21:04 ?        00:00:05 redis-sentinel *:26380 [sentinel]
root      1891  1585  0 21:33 pts/0    00:00:00 grep --color=auto redis

# 杀死进程
kill -9 1610

# 在该主机上连接 sentinel 服务
redis-cli -p 26380
# 查看主节点信息
127.0.0.1:26379> info

# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=10.211.55.3:6379,slaves=2,sentinels=3

可以看到主节点从 10.211.55.4 变成了 10.211.55.3 这个节点。

代码验证

pom.xml 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>redis</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

yaml 文件配置

server:
  port: 8080
  
spring:
  redis:
    database: 0
    timeout: 3000
    sentinel:
      master: mymaster
      nodes: 10.211.55.3:26379,10.211.55.4:26380,10.211.55.5:26381
    lettuce:
      pool:
        max-idle: 50
        min-idle: 10
        max-active: 100
        max-wait: 1000

连接redis demo

package com.anzhi.sentineljedis.service;

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelPool;

import java.util.HashSet;
import java.util.Set;

public class JedisSentinelDemo {
    public static void main(String[] args) {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(20);
        config.setMaxIdle(10);
        config.setMinIdle(5);

        String masterName = "mymaster";
        Set<String> sentinels = new HashSet<String>();
        sentinels.add(new HostAndPort("10.211.55.3",26379).toString());
        sentinels.add(new HostAndPort("10.211.55.4",26380).toString());
        sentinels.add(new HostAndPort("10.211.55.5",26381).toString());

        //JedisSentinelPool其实本质跟JedisPool类似,都是与redis主节点建立的连接池
        //JedisSentinelPool并不是说与sentinel建立的连接池,而是通过sentinel发现redis主节点并与其建立连接
        JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(masterName, sentinels, config, 3000, null);
        Jedis jedis = null;
        try{
            jedis = jedisSentinelPool.getResource();
            System.out.println(jedis.set("sentinel", "masterA"));
            System.out.println(jedis.get("sentinel"));
            System.out.println(jedis.del("sentinel"));
        }catch (Exception e){
            // doNothing
        }finally {
            //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
            if (jedis != null){
                jedis.close();
            }
        }
    }
}

Controller 层接口

package com.anzhi.sentineljedis.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SentinelIndexCOntroller {
    private static final Logger logger = LoggerFactory.getLogger(SentinelIndexCOntroller.class);
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 测试节点挂了哨兵重新选举新的master节点,客户端是否能动态感知到
     *
     * @throws InterruptedException
     */
    @RequestMapping("/test_sentinel")
    public void testSentinel() throws InterruptedException {
        int i = 1;
        while (true){
            try{
                stringRedisTemplate.opsForValue().set("master"+i, i+""); //jedis.set(key,value);
                System.out.println("设置key:"+ "master" + i);
                Thread.sleep(10000);
                stringRedisTemplate.delete("master" + i);
                i++;
            }catch (Exception e){
                logger.error("错误:", e);
            }
        }
    }
}

这段程序主要干的事儿是:先设置键值,然后休眠10,在删除 key。一直循环

启动类

package com.anzhi.sentineljedis;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SentinelApplication {
    public static void main(String[] args) {
        SpringApplication.run(SentinelApplication.class, args);
    }
}

程序启动后,通过链接 http://localhost:8080/test_sentinel 访问。此时控制台会输出:

设置key:master1
设置key:master2
设置key:master3
设置key:master4
设置key:master5
设置key:master6
设置key:master7
设置key:master8
设置key:master9

接下来我们关闭主节点,此时控制台输出:

2023-03-02 21:44:51.256  INFO 11153 --- [xecutorLoop-1-3] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was /10.211.55.5:6381
2023-03-02 21:44:51.376  WARN 11153 --- [ioEventLoop-4-4] i.l.core.protocol.ConnectionWatchdog     : Cannot reconnect to [10.211.55.5:6381]: Connection refused: /10.211.55.5:6381
2023-03-02 21:44:57.259  INFO 11153 --- [xecutorLoop-1-9] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was 10.211.55.5:6381
2023-03-02 21:44:57.365  WARN 11153 --- [oEventLoop-4-10] i.l.core.protocol.ConnectionWatchdog     : Cannot reconnect to [10.211.55.5:6381]: Connection refused: /10.211.55.5:6381
2023-03-02 21:45:03.865 ERROR 11153 --- [nio-8080-exec-1] c.a.s.c.SentinelIndexCOntroller          : 错误:

org.springframework.dao.QueryTimeoutException: Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException: Command timed out after 3 second(s)

可以看到,主节点故障,导致程序异常。在程序运行了一段时间后,重新连接到了主节点:

2023-03-02 21:45:26.460  INFO 11153 --- [xecutorLoop-1-8] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was 10.211.55.5:6381
2023-03-02 21:45:26.466  INFO 11153 --- [oEventLoop-4-16] i.l.core.protocol.ReconnectionHandler    : Reconnected to 10.211.55.3:6379
设置key:master9
设置key:master10

但是此时主节点已经发生了变化,变成了 10.211.55.3:6379,然后程序继续运行。

Redis Cluster(Redis 集群)

根据对 Sentinel 测试其实会发现虽然故障可以自动恢复,但是在故障恢复期间,redis 服务是处于不可用的状态。另一方面,由于只有主节点提供写服务, 无法支持很高的并发。最后就是 redis 容量有限,面对那些大公司比较依赖 redis 缓存的情况下,单一集群根本就不够用。这时可能会想把 redis 容量 扩大就好了呀,但是随之而来的问题是数据备份的问题。容量大意味着 AOF 的文件将来会很大,那么数据恢复时间也就会很长,从而导致主从数据同步时间 也会增加。显然这种方案是不可取的。

在 Redis 3.0 提供了一种高可用集群模式,它具有复制、高可用和分片特性,即保证了 CAP 中的 AP 特性。Redis 集群不需要 sentinel 哨兵也能完成 节点移除和故障恢复的功能。这种集群模式没有中心节点,可水平扩展。因此也就不存在容量的问题。

Redis 集群搭建

redis高可用集群至少需要三个master节点,我们搭建三个 master 节点并给每个 master 节点配置一个 slave 节点。这里使用三台虚机搭建,每台虚机 搭建一主一从。

修改第一个主节点 8001 文件

bind 0.0.0.0
protected-mode no
port 8001
daemonize yes
pidfile /var/run/redis_cluster_8001.pid
logfile "redis_cluster_8001.log"
dir /root/Dev_Azh/redis/data/8001/
# 开启 redis-cluster 集群模式
cluster-enabled yes
# 集群信息节点文件
cluster-config-file nodes-8001.conf
# 连接超时时间
cluster-node-timeout 10000
appendonly yes

修改 8002 文件

bind 0.0.0.0
protected-mode no
port 8002
daemonize yes
pidfile /var/run/redis_cluster_8002.pid
logfile "redis_cluster_8002.log"
dir /root/Dev_Azh/redis/data/8002/
# 开启 redis-cluster 集群模式
cluster-enabled yes
# 集群信息节点文件
cluster-config-file nodes-8002.conf
# 连接超时时间
cluster-node-timeout 10000
appendonly yes

修改 8003 文件

bind 0.0.0.0
protected-mode no
port 8003
daemonize yes
pidfile /var/run/redis_cluster_8003.pid
logfile "redis_cluster_8003.log"
dir /root/Dev_Azh/redis/data/8003/
# 开启 redis-cluster 集群模式
cluster-enabled yes
# 集群信息节点文件
cluster-config-file nodes-8003.conf
# 连接超时时间
cluster-node-timeout 10000
appendonly yes

修改 8004 文件

bind 0.0.0.0
protected-mode no
port 8004
daemonize yes
pidfile /var/run/redis_cluster_8004.pid
logfile "redis_cluster_8004.log"
dir /root/Dev_Azh/redis/data/8004/
# 开启 redis-cluster 集群模式
cluster-enabled yes
# 集群信息节点文件
cluster-config-file nodes-8004.conf
# 连接超时时间
cluster-node-timeout 10000
appendonly yes

修改 8005 文件

bind 0.0.0.0
protected-mode no
port 8005
daemonize yes
pidfile /var/run/redis_cluster_8005.pid
logfile "redis_cluster_8005.log"
dir /root/Dev_Azh/redis/data/8005/
# 开启 redis-cluster 集群模式
cluster-enabled yes
# 集群信息节点文件
cluster-config-file nodes-8005.conf
# 连接超时时间
cluster-node-timeout 10000
appendonly yes

修改 8006 文件

bind 0.0.0.0
protected-mode no
port 8006
daemonize yes
pidfile /var/run/redis_cluster_8006.pid
logfile "redis_cluster_8006.log"
dir /root/Dev_Azh/redis/data/8006/
# 开启 redis-cluster 集群模式
cluster-enabled yes
# 集群信息节点文件
cluster-config-file nodes-8006.conf
# 连接超时时间
cluster-node-timeout 10000
appendonly yes

配置完成后,启动这 6 个 redis 实例

redis-server redis_cluster_8001.conf 
redis-server redis_cluster_8002.conf
redis-server redis_cluster_8003.conf
redis-server redis_cluster_8004.conf 
redis-server redis_cluster_8005.conf 
redis-server redis_cluster_8006.conf 

启动成功后,使用 redis-cli 命令创建集群。首先先确定各个虚机之间可以通过客户端互联

# 在 10.211.55.3 上连接 10.211.55.4 的 redis 服务
redis-cli -h 10.211.55.4 -p 8003
redis-cli -h 10.211.55.4 -p 800

在每台虚机上确认连接其他虚机的 redis 服务正常后,开始建立集群

redis-cli --cluster create --cluster-replicas 1 10.211.55.3:8001 10.211.55.5:8006 10.211.55.4:8003 10.211.55.3:8002 10.211.55.5:8005 10.211.55.4:8004
# 输出信息
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 10.211.55.5:8005 to 10.211.55.3:8001
Adding replica 10.211.55.4:8004 to 10.211.55.5:8006
Adding replica 10.211.55.3:8002 to 10.211.55.4:8003
M: 996869f6edf22608d407b99726b3b0e80c5ac194 10.211.55.3:8001
   slots:[0-5460] (5461 slots) master
M: 42ae80c2a513b5484088f88611d3aca8988d7528 10.211.55.5:8006
   slots:[5461-10922] (5462 slots) master
M: 7c87165c1753858ee51ca350253e17b09fab4917 10.211.55.4:8003
   slots:[10923-16383] (5461 slots) master
S: fde4695b37f216d6fec57acfc3b136c102fc3579 10.211.55.3:8002
   replicates 7c87165c1753858ee51ca350253e17b09fab4917
S: bace4b2a0bd2088202a2f09a384af632e5dcd230 10.211.55.5:8005
   replicates 996869f6edf22608d407b99726b3b0e80c5ac194
S: dfccd40cfbeabc36f564de2bd0ccf6bc49191af7 10.211.55.4:8004
   replicates 42ae80c2a513b5484088f88611d3aca8988d7528
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
......
>>> Performing Cluster Check (using node 10.211.55.3:8001)
M: 996869f6edf22608d407b99726b3b0e80c5ac194 10.211.55.3:8001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: bace4b2a0bd2088202a2f09a384af632e5dcd230 10.211.55.5:8005
   slots: (0 slots) slave
   replicates 996869f6edf22608d407b99726b3b0e80c5ac194
M: 7c87165c1753858ee51ca350253e17b09fab4917 10.211.55.4:8003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
M: 42ae80c2a513b5484088f88611d3aca8988d7528 10.211.55.5:8006
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: dfccd40cfbeabc36f564de2bd0ccf6bc49191af7 10.211.55.4:8004
   slots: (0 slots) slave
   replicates 42ae80c2a513b5484088f88611d3aca8988d7528
S: fde4695b37f216d6fec57acfc3b136c102fc3579 10.211.55.3:8002
   slots: (0 slots) slave
   replicates 7c87165c1753858ee51ca350253e17b09fab4917
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

输出上述结果基本是成功了。使用下面命令验证是否搭建成功:

10.211.55.3:8001> cluster nodes

bace4b2a0bd2088202a2f09a384af632e5dcd230 10.211.55.5:8005@18005 slave 996869f6edf22608d407b99726b3b0e80c5ac194 0 1677886398000 5 connected
7c87165c1753858ee51ca350253e17b09fab4917 10.211.55.4:8003@18003 master - 0 1677886397453 3 connected 10923-16383
996869f6edf22608d407b99726b3b0e80c5ac194 10.211.55.3:8001@18001 myself,master - 0 1677886388000 1 connected 0-5460
42ae80c2a513b5484088f88611d3aca8988d7528 10.211.55.5:8006@18006 master - 0 1677886398089 2 connected 5461-10922
dfccd40cfbeabc36f564de2bd0ccf6bc49191af7 10.211.55.4:8004@18004 slave 42ae80c2a513b5484088f88611d3aca8988d7528 0 1677886397000 6 connected
fde4695b37f216d6fec57acfc3b136c102fc3579 10.211.55.3:8002@18002 slave 7c87165c1753858ee51ca350253e17b09fab4917 0 1677886398510 4 connected

可以发现 redis 帮我们建立集群的时候是错位建立主从的,这样建立主从更安全。如果主从节点都在一台机器上,当主节点挂掉,此时这个小集群就挂掉了。 redis-cluster 集群默认当一个小集群挂掉,整个redis集群就不会对外提供服务了。

通过上面搭建 redis-cluster 集群,发现还是需要手动连接客户端,仍然处于半自动化状态。那么如何手动地向集群中添加节点呢?

# 使用 redis-cli 查看集群命令
redis-cli --cluster help
# 输出结果
Cluster Manager Commands:
  create         host1:port1 ... hostN:portN
                 --cluster-replicas <arg>
  check          host:port
                 --cluster-search-multiple-owners
  info           host:port
  fix            host:port
                 --cluster-search-multiple-owners
  reshard        host:port
                 --cluster-from <arg>
                 --cluster-to <arg>
                 --cluster-slots <arg>
                 --cluster-yes
                 --cluster-timeout <arg>
                 --cluster-pipeline <arg>
                 --cluster-replace
  rebalance      host:port
                 --cluster-weight <node1=w1...nodeN=wN>
                 --cluster-use-empty-masters
                 --cluster-timeout <arg>
                 --cluster-simulate
                 --cluster-pipeline <arg>
                 --cluster-threshold <arg>
                 --cluster-replace
  add-node       new_host:new_port existing_host:existing_port
                 --cluster-slave
                 --cluster-master-id <arg>
  del-node       host:port node_id
  call           host:port command arg arg .. arg
  set-timeout    host:port milliseconds
  import         host:port
                 --cluster-from <arg>
                 --cluster-copy
                 --cluster-replace
  1. create:创建一个集群环境host1:port1 ... hostN:portN
  2. call:可以执行redis命令
  3. add-node:将一个节点添加到集群里,第一个参数为新节点的ip:port,第二个参数为集群中任意一个已经存在的节点的ip:port
  4. del-node:移除一个节点
  5. reshard:重新分片
  6. check:检查集群状态

添加节点需要两部操作:

# 第一步添加节点(只介绍下命令,不操作了)
redis‐cli ‐‐cluster add‐node 10.211.55.3:8007 10.211.55.3:8001
# 将 8007 添加完节点后,还需要让主节点重新分配槽位
redis‐cli ‐‐cluster reshard 10.211.55.3:8001

代码验证

Jedis 操作

package com.anzhi.clusterjedis.service;

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

public class JedisClusterDemo {
    public static void main(String[] args) throws IOException {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(20);
        config.setMaxTotal(20);
        config.setMinIdle(5);

        Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();
        jedisClusterNode.add(new HostAndPort("10.211.55.3", 8001));
        jedisClusterNode.add(new HostAndPort("10.211.55.3", 8002));
        jedisClusterNode.add(new HostAndPort("10.211.55.4", 8003));
        jedisClusterNode.add(new HostAndPort("10.211.55.4", 8004));
        jedisClusterNode.add(new HostAndPort("10.211.55.5", 8005));
        jedisClusterNode.add(new HostAndPort("10.211.55.5", 8006));

        JedisCluster jedisCluster = null;
        try{
            //connectionTimeout:指的是连接一个url的连接等待时间
            //soTimeout:指的是连接上一个url,获取response的返回等待时间
            jedisCluster = new JedisCluster(jedisClusterNode, 6000, 5000, 10, config);
            System.out.println(jedisCluster.set("clusterA", "clusterA"));
            System.out.println(jedisCluster.get("clusterA"));
        }catch (Exception e){
            // doNothing
        }finally {
            if (jedisCluster != null) {
                jedisCluster.close();
            }
        }
    }
}

Spring Boot 操作

基于之前哨兵的 Spring Boot 工程改造 pom.xml 依赖参考哨兵。

yml 配置

server:
  port: 8080
  
spring:
  redis:
    database: 0
    timeout: 3000
    # 哨兵模式配置
#    sentinel:
#      master: mymaster
#      nodes: 10.211.55.3:26379,10.211.55.4:26380,10.211.55.5:26381
#    lettuce:
#      pool:
#        max-idle: 50
#        min-idle: 10
#        max-active: 100
#        max-wait: 1000
    # redis-cluster 集群模式配置
    cluster:
      nodes: 10.211.55.3:8001 10.211.55.5:8006 10.211.55.4:8003 10.211.55.3:8002 10.211.55.5:8005 10.211.55.4:8004
    lettuce:
      pool:
        max-idle: 50
        min-idle: 10
        max-active: 100
        max-wait: 1000

controller 层访问代码

@RestController
public class RedisClusterController {
    private static final Logger logger = LoggerFactory.getLogger(RedisClusterController.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/rediscluster")
    public String testCluster() throws InterruptedException {
        stringRedisTemplate.opsForValue().set("clusterA", "clusterA");
        System.out.println(stringRedisTemplate.opsForValue().get("clusterA"));
        return stringRedisTemplate.opsForValue().get("clusterA");
    }
}

创建启动类,

@SpringBootApplication
public class RedisClusterApplication {
    public static void main(String[] args) {
        SpringApplication.run(RedisClusterApplication.class, args);
    }
}

通过连接访问:http://localhost:8080/rediscluster

Redis Cluster 集群原理

上面我们在配置 Redis 集群的时候。最后输出了这样一个信息:[OK] All 16384 slots covered.。 redis-cluster 将所有数据划分为 16384 个 slots(槽位),每个节点负责其中一部分槽位。槽位的信息存储于每个节点中。

槽指派机制

当新的节点加入集群的时候,同样会得到一份集群的槽位配置信息并将其缓存在客户端本地。这个信息就是我们在 conf 配置文件中指定的cluster-config-file nodes-8006.conf

当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存在客户端本地。这样当客户端要查找某个 key 时,可以直接 定位到目标节点。同时因为槽位的信息可能会存在客户端与服务器不一致的情况,还需要纠正机制来实现槽位信息的校验调整。

槽定位算法

Cluster 默认会对 key 值使用 crc16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模 来得到具体槽位。

HASH_SLOT = CRC16(key) mod 16384

跳转重定位

当客户端向一个错误的节点发出了指令,该节点会发现指令的 key 所在的槽位并不归自己管理,这时它会向客 户端发送一个特殊的跳转指令携带目标操作 的节点地址,告诉客户端去连这个节点去获取数据。客户端收到指 令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存,后续所 有 key 将使用新的槽位映射表。

Redis 集群节点间的通信机制

redis cluster 节点间采取 gossip 协议进行通信。

维护集群的元数据(集群节点信息,主从角色,节点数量,各节点共享的数据等)有两种方式:集中 式和gossip

  1. 集中式:优点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以立即感 知到;不足在于所有的元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力。很多中间件都会借助zookeeper集中式存储元数据。
  2. gossip:协议包含多种消息,包括ping,pong,meet,fail等等。
    1. meet:某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信;
    2. ping:每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping交换元数据(类似自己感知到的集 群节点增加和移除,hash slot信息等);
    3. pong: 对ping和meet消息的返回,包含自己的状态和其他信息,也可以用于信息广播和更新;
    4. fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了。

gossip协议的优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;缺点在 于元数据更新有延时可能导致集群的一些操作会有一些滞后。

gossip通信的10000端口

每个节点都有一个专门用于节点间gossip通信的端口,就是自己提供服务的端口号+10000,比如7001,那么 用于节点间通信的就是17001端口。 每个节 点每隔一段时间都会往另外几个节点发送ping消息,同时其他几点接收到ping消息之后返回pong消息。

网络抖动

真实世界的机房网络往往并不是风平浪静的,它们经常会发生各种各样的小问题。比如网络抖动就是非常常见 的一种现象,突然之间部分连接变得不可访问, 然后很快又恢复正常。 为解决这种问题,Redis Cluster 提供了一种选项cluster-node-timeout,表示当某个节点持续 timeout 的时间失联时,才 可以认定该节点出现故障,需要进行主从切换。如果没有这个选项,网络抖动会导致主从频繁切换 (数据的重新复制)。

Redis集群选举原理分析

当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的 master 可能会有多个slave,从而存在多个 slave竞争成为master节点的过程, 其过程如下:

  1. slave发现自己的master变为FAIL
  2. 将自己记录的集群currentEpoch加1,并广播 FAILOVER_AUTH_REQUEST 信息
  3. 其他节点收到该信息,只有master响应,判断请求者的合法性,并发送 FAILOVER_AUTH_ACK,对每一个 epoch 只发送一次 ack
  4. 尝试 failover的 slave 收集 master 返回的 FAILOVER_AUTH_ACK
  5. slave 收到超过半数 master 的 ack 后变成新 Master(这里解释了集群为什么至少需要三个主节点,如果只有两 个,当其中一个挂了,只剩一个主节点是不能选举成功的)
  6. slave广播Pong消息通知其他集群节点。

从节点并不是在主节点一进入 FAIL 状态就马上尝试发起选举,而是有一定延迟,一定的延迟确保我们等待 FAIL 状态在集群中传播,slave 如果立即尝试选举, 其它 masters 或许尚未意识到 FAIL 状态,可能会拒绝投票

延迟计算公式:

DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms

SLAVE_RANK 表示此 slave 已经从 master 复制数据的总量的 rank。Rank越小代表已复制的数据越新。这种方式下,持有最新数据的 slave 将会首先 发起选举(理论上)。

集群脑裂数据丢失问题

redis集群没有过半机制会有脑裂问题,网络分区导致脑裂后多个主节点对外提供写服务,一旦网络分区恢复,会将其中一个主节点变为从节点,这时会有 大量数据丢失。

规避方法可以在redis配置里加上参数(这种方法不可能百分百避免数据丢失,参考集群leader选举机制):

min‐replicas‐to‐write 1 //写数据成功最少同步的slave数量,这个数量可以模仿大于半数机制配置,比如集群总共三个节点可以配置1,加上leader 就是2,超过了半数。

注意:这个配置在一定程度上会影响集群的可用性,比如slave要是少于1个,这个集群就算leader正常也不能 提供服务了,需要具体场景权衡选择。

集群是否完整才能对外提供服务

当redis.conf的配置cluster-require-full-coverage 为 no 时,表示当负责一个插槽的主库下线且没有相应的从 库进行故障恢复时,集群仍然可用, 如果为yes则集群不可用。

Redis集群为什么至少需要三个master节点,并且推荐节点数为奇数?

因为新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中 一个挂了,是达不到选举新master的条件的。 奇数个master节点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点的 集群相比,大家如果都挂了一个master节点 都能选举新master节点,如果都挂了两个master节点都没法选举 新master节点了,所以奇数的master节点更多的是从节省机器资源角度出发说的。

Redis集群对批量操作命令的支持

对于类似mset,mget这样的多个key的原生批量操作命令,redis集群只支持所有key落在同一slot的情况,如 果有多个key一定要用mset命令在redis集 群上操作,则可以在key的前面加上{XX},这样参数数据分片hash计 算的只会是大括号里的值,这样能确保不同的key能落到同一slot里去,示例如下:

假设name和age计算的hash slot值不一样,但是这条命令在集群下执行,redis只会用大括号里的 user1 做 hash slot计算,所以算出来的slot值肯 定相同,最后都能落在同一slot。

哨兵leader选举流程

当一个master服务器被某sentinel视为下线状态后,该sentinel会与其他sentinel协商选出sentinel的leader进 行故障转移工作。每个发现master 服务器进入下线的sentinel都可以要求其他sentinel选自己为sentinel的 leader,选举是先到先得。同时每个sentinel每次选举都会自增配置纪元 (选举周期),每个纪元中只会选择一 个sentinel的leader。如果所有超过一半的sentinel选举某sentinel作为leader。之后该sentinel进行故障转移 操作,从存活的slave中选举出新的master,这个选举过程跟集群的master选举很类似。 哨兵集群只有一个哨兵节点,redis的主从也能正常运行以及选 举master,如果master挂了,那唯一的那个哨 兵节点就是哨兵leader了,可以正常选举新master。 不过为了高可用一般都推荐至少部署三个哨兵节点。 为什么推荐奇数个哨兵节点原理跟集群奇数个master节点类似。