【Redis系列9】手把手带你搭建单机版高可用分布式Redis集群(Cluster)

前言

Redis作为一款优秀的Nosql数据库,使用非常广泛,但是在有些场景下,一台Redis服务器是不能满足要求的,我们可能需要多台Redis服务器来一起工作,而且如果仅仅使用一台Redis服务器,那么假如这一台服务器挂了,也会给业务带来很大的影响,严重的可能会导致整个系统不可用,所以一个高可用的分布式Redis集群是非常必要的。

Redis集群服务

Redis当中的集群方案实现方式可以分为三大类:主从复制集群,基于哨兵机制实现的高可用集群和Redis分布式Cluster方案,下面就让我们分别来进行介绍

主从复制

主动复制,即:master-slave方案,是一个非常常见的设计模型。其中主库用来读写,并且将数据同步给从库,一旦主库挂了,那么从库就可以升级为主库。

配置一主两从master-slave集群

配置文件中配置replicaof no one则表示当前Redis服务器为主服务器,然后从服务器使用配置replicaof host port这样就成为了master的从库。配置成功之后,连接上主从库,可以执行命令replication info命令进行查看信息。

  • 主库执行info replication命令显示信息如下(role表示当前是一个master库,下面的slaveX展示的就是从库的信息):
    在这里插入图片描述
  • 从库执行info replication命令显示信息如下(role表示当前是一个slave库,下面的master_XX展示的就是主库的相关信息):
    在这里插入图片描述
    配置主从的命令除了可以在配置文件中配置,还可以直接在redis服务器上执行,或者也可以在启动的时候执行./redis-server --slaveof ip port来指定主从服务器。

搭建好主从之后,主服务器上数据就会被同步到从服务器,注意,从服务器默认是只读的,可以通过配置文件replica-read-only no来修改。

题外话:上面的replicaof命令可以替换为slaveof命令,但是建议还是使用replicaof命令,因为slaveof命令在国外被理解奴隶制度,所以当时因为这个命令Redis作者发起过一个投票,半数以上的人支持改名,所以后面Redis就采用了replicaof命令来替换slaveof命令。

主从复制原理分析

master-slave集群的关键在于数据的同步,而在数据同步之前必须先建立连接。

建立连接

建立连接主要分为以下几步:

  • 1、执行slaveof命令时候,从服务器会在本地将主服务器的一些信息(如:IP和端口等信息)保存在redisServer内。
  • 2、创建和主服务器的连接,创建连接之后,从服务器就相当于主服务器的一个客户端。
  • 3、从服务器向主服务发送ping命令,确认连接是否可用,如果Redis服务器需要授权,这一步还会进行授权认证。
  • 4、如果从服务器收到主服务器的pong回复之后,表示当前连接可用,此时从服务器会将自己的服务器的端口号发送给主服务器,主服务器收到之后将其记录在redisClient内。
  • 5、如果从服务器没收到主服务器返回pong,则会发起重连。

PS:建立完连接之后,主从服务器会定时(间隔1s)向对方发送replconf of命令来检测对方是否正常。上图中在主服务器查看从服务器信息中有一个lag属性记录的就是上一次发送心跳包时间。

同步

master和slave服务之间连接建立之后,就会开始进行数据同步,而首次同步一般用于首次建立主从连接的时候,这时候因为是初次建立master-slave关系,所以需要进行数据的全量同步。

首次数据全量同步主要分为以下步骤:

  • 1、从服务器向主服务器发送同步数据命令。
  • 2、主服务器接收到同步数据命令之后,则会执行bgsave命令,在后台生成一个RDB文件,并将其发送给从服务器,此时如果有新的命令过来,主服务器会将其记录在缓冲区内。
  • 3、从服务器收到主服务器发送过来的RDB文件之后,会首先清除自己的数据,然后载入RDB文件来生成数据。
  • 4、当从服务器执行完RDB文件之后,主服务器会将缓冲区的命令发送给从服务器,从服务器再依次执行。

命令传播

执行完首次全量同步之后,这时候主服务器会将自己接收到的改变了数据库状态的命令发送给从服务器,从而使得主从服务器数据始终保持一致。

既然是命令传播,那么就不可避免的会造成数据延迟,Redis当中提供了一个参数来进行优化。

repl-disable-tcp-nodelay no //默认是no

当参数设置为yes时,此时会将数据包进行合并发送,也就是降低了发送频率(发送频率与Linux内核配置有关);当参数设置为no时,则主服务器每执行一个能改变数据库状态的命令就会立刻实时同步给从服务器。

部分重同步

上面就是在服务器正常情况下的同步措施,同步+命令传播可以使得正常情况下主从服务器数据保持一致。然而如果说从服务器因为停电等其他因素导致其和主服务器之间的连接中途断开,那么当连接再次恢复正常之后,如果还是重新全量同步则效率会非常低,也显得没有必要,所以Redis就需要支持部分重同步。

实现部分重同步最关键的地方就是需要记录原先同步的偏移量,只有这样才能在连接恢复正常之后继续实现命令传播,而无需传输整个RDB文件

同步偏移量

主从服务器各自都会维护一个数据复制的偏移量,这个偏移量表示的是发送命令的字节数。
下图就是一个刚建立连接的主从服务器,默认偏移量offset都是0:
在这里插入图片描述
当主服务器向从服务器发送100字节之后,主从服务器的偏移量就会变成100。
在这里插入图片描述
那么如果master再次向slave1和slave2传输了一个200字节的命令,slave1接收到了,而slave2没有接收到,那么就会出现以下情况:
在这里插入图片描述
这时候当slave2再次和master恢复连接之后,此时slave2服务器会想主服务器发送同步命令,同步命令会带上偏移量,这时候主服务器收到了,发现slave2发送过来额偏移量是100,而自己已经到300了,那么主服务器就会把101到300之间的命令再次进行发送给slave2,从而达到了部分重同步的目的。

复制积压缓冲区

上面的部分重同步貌似看起来能解决问题,但是这又会带来另一个问题,那就是当主服务器将命令发送出去之后,为了实现部分重同步还需要将命令保存起来,否则当从服务器的偏移量低于主服务器时,主服务器也无法将命令重传播。

那么问题就来了,这个命令要保存多久呢?如果一直保存下去就会占据大量的空间,为了解决这个问题,master服务器维护了一个固定长度的FIFO队列,即复制积压缓冲区

当进行命令传播的过程中,master服务器不仅会将命令传播给所有的slave服务器,同时还会将命令写入复制积压缓冲区。复制积压缓冲区默认大小为1MB。

下面就是一个完整的部分重同步流程图:
在这里插入图片描述
也就是说,当master服务器记录的偏移量+1已经不存在与复制积压缓冲区了,就会执行一次全量同步,即发送RDB文件给从服务器。

主从服务的不足之处

主从服务器通过读写分离实现了数据的可靠性,但是其并未实现系统的高可用性。其主要存在以下两个问题:

  • 1、首次同步或者部分重同步时需要执行全量同步时发送的RDB文件如果过大,则会非常耗时。
  • 2、假如master服务器挂了,那么系统并不能手动切换master服务器,仍然需要人为进行切换。

哨兵Sentinel机制

Redis的Sentinel机制主要是为了实现Redis服务器的高可用性而设计的一种解决方案。这种方案也是为了弥补主从复制模式的不足,Sentinel机制实现了主从服务的自动切换。

Sentinel原理分析

Sentinel其本身也是一个特殊的Redis服务,在Redis的安装包内,除了redis.conf文件,还有一个sentinel.conf文件,这个就是启动sentinel服务的配置文件,启动命令则通过redis-sentinel来执行(如:./redis-sentinel ../sentinel.conf)或者也可以通过redis-server命令指定参数sentinel来启动(如:./redis-server ../sentinel.conf --sentinel)。

Sentinel主要用来监控Redis集群中的所有节点,当Sentinel服务发现master不可用时,可以实现master服务的自动切换。但是如果Sentinel服务自己挂了怎么办?所以为了实现高可用,Sentinel服务本身也是一个集群,和Redis的master-slave模式不同的是,Sentinel集群之间在正常情况下没有主从关系,相互之间是平等的,只有在需要执行故障转移时才需要进行Leader选举。
下图就是一个3个Sentinel服务集群和1主2从的Redis集群示意图:
在这里插入图片描述
Sentinel集群之间的服务会互相监控,然后每个Sentinel服务都会监控所有的master-slave节点,一旦发现master节点不可用,则Sentinel中通过选举产生的Leader节点会执行故障转移,切换master节点。

主观下线和客观下线

Sentinel服务默认以每秒1次的频率向Redis服务节点发送ping命令(Sentinel服务之间也会发送ping命令进行检测)。如果在指定时间内(可以由参数down-after-milliseconds进行控制,默认30s)没有收到有效回复,Sentinel会将该服务器标记为下线,即主观下线

down-after-milliseconds master-name milliseconds

当某一个Sentinel把master服务标记为主观下线之后,会去询问其他Sentinel节点,确认这个master是否真的下线,当达到指定数量的Sentinel服务都认为master服务器已经主观下线,这时候Sentinel就会将master服务标记为客观下线,并执行故障转移操作。
多少个Sentinel服务认定master节点主观下线才会正式将master服务标记为客观下线,由以下参数控制:

sentinel monitor <master-name> <ip> <redis-port> <quorum>

其中的quorum选项就是决定了这个数量。

需要注意的是,每个Sentinel服务的判断主观下线和客观下线的配置可能不一样,所以当Sentinel1判定master已经主观下线或者客观下线时,其他Sentinel服务并不一定会这么认为,但是只要有一个Sentinel判定master已经客观下线,其就会执行故障转移,但是故障转移并不一定是由判断为客观下线的Sentinel服务来执行,在执行故障转移的之前,Sentinel服务之间必须进行Leader选举。

Leader选举

当某一个或者多个Sentinel服务判定master服务已经下线,其会发起Leader选举,选举出Leader之后,由Leader节点执行故障转移。

Raft选举算法

Sentinel服务的Leader选举是通过Raft算法来实现的。Raft是一个共识算法(consensus algorithm),其核心思想主要有两点:

  • 1、先到先得
  • 2、少数服从多数

在Raft算法中,每个节点都维护了一个属性election timeout,这是一个随机的时间,范围在150ms~300ms之间,哪个节点先到达这个时间,哪个节点就可以发起选举投票。

选举步骤总要可以总结为以下步骤:

  • 1、发起选举的服务首先会给自己投上一票。
  • 2、然后会向其他节点发送投票请求到其他节点,其他节点在收到请求后如果在election timeout范围内还没有投过票,那么就会给发起选举的节点投上一票,然后将election timeout重置。
  • 3、如果发起选举的节点获得的票数超过一半,那么当前服务就会成为Leader节点,成为Leader节点之后就会维护一个heartbeat timeout时间属性,在每一次到达heartbeat timeout时间时,Leader节点就会向其他Follow节点发起一个心跳检测。
  • 4、Follow节点收到Leader节点的心跳包之后就会将election timeout清空,这样可以防止Follow节点因为到达election timeout而发起选举。
  • 5、假如Leader节点挂了,那么Follow节点的election timeout将不会被清空,谁先到达,谁就会再次发起选举。

PS:因为election timeout是一个随机值,虽然概率小,但也可能出现两个节点同时发起投票选举,这种情况就可能出现一次选举并不能选出Leader(比如总共4个节点,每个节点都得了2票),此时就会等待下一次首先到达election timeout的节点再次发起投票选举。

如果对Raft算法感兴趣的,可以点击这里观看演示。

Sentinel选举Leader

Sentinel中的选举虽然是源于Raft算法,但是也做了以下改进:

  • 1、触发选举并不是由election timeout时间决定,而是由谁先判定master下线来决定的。
  • 2、Sentinel节点并没有维护election timeout属性,而是维护了一个配置纪元configuration epoch属性,配置纪元是一个计数器(默认0),每一个Sentinel节点的同一个配置纪元只能投票1次(先到先得),每次投票前会将配置纪元自增+1。
  • 3、选举出Leader之后,Leader并不会向Follow节点发送心跳包告诉其他Follow节点自己成为了Leader。

故障转移

当Sentinel选举出Leader之后,Leader就会开始执行故障转移,执行故障转移主要分为一下三步:

  • 1、在已判定客观下线的master服务器的slave服务器中找到一个合格的slave服务器,向其发送replicaof no one命令,使其转换为master服务。
  • 2、向其他从服务器发送replicaof ip port命令,使其成为新master服务的slave节点。
  • 3、将已下线的master服务也设置为新的master服务的slave节点。
如何选举新的master节点

新的master选举条件主要需要参考4个因素:

  • 1、断开连接时长:首先将所有于已下线master节点断开连接时间超过down-after-milliseconds * 10的slave节点删除掉,确保salve节点的数据都是比较新的。
  • 2、slave节点的优先级排序:将所有的salve节点按照优先级进行排序,选出优先级最高的slave节点作为新的master节点(优先级由配置文件参数replica-priority决定,默认100)。
  • 3、复制偏移量:如果有多个优先级相同的slave节点,则选出复制偏移量最大的的slave节点作为新的master节点。
  • 4、进程id:如果还是没选出新的master节点,那么会再次选择进程id最小的slave节点作为新的master节点。

配置Sentinel集群

配置sentinel需要修改sentinel.conf配置文件,主要涉及到以下的一些配置属性:

protected-mode no
port 26380
pidfile /xxx/redis-sentinel-26380.pid
logfile "/xxx/sentinel.log"
dir  "/xxx"
sentinel monitor mymaster xx.xxx.xxx.xxx 6370 2  //ip不要设置成127.0.0.1或者localhost
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
参数 说明
protected-mode 是否允许外部网络访问
port sentinel端口
pidfile pid文件
logfile 日志文件
dir sentinel工作主目录
sentinel monitor 监控的master服务名称,ip和端口,其中ip建议使用真实ip取代本机ip
sentinel down-after-milliseconds master下线多少毫秒才会被Sentinel 判定为主观下线
sentinel parallel-syncs 切换新的master-slave时,slave需要从新的master同步数据,这个数字表示允许多少个slave同时复制数据
sentinel failover-timeout 故障转移超时时间,这个时间有4个含义

注意上面的master服务名称可以取值,但是在同一个Sentinel集群中需要保持一致,否则会无法正确监控。

故障转移超时时间用在了以下4个地方:

  • 1、同一个sentinel对同一个master两次failover之间的间隔时间。
  • 2、从检测到master服务器故障开始,到被强制切换到新的master服务器并开始复制数据为止的时间。
  • 3、取消已经在进行故障转移(没有产生任何配置更改的故障转移)所需的时间。
  • 4、将所有salve配置新的master节点所需要的时间。超过这个时间如果仍然没有完成还是会继续进行,但是不一定会按照配置parallel-syncs所指定的并行数来进行。

Sentinel机制日志解析

下图就是一个Sentine故障转移的日志,最开始6371为被Sentinel监控的master服务器:
在这里插入图片描述

  • 1-7行表示Sentinel启动完毕,正在监控6371端口的master服务器。
  • 8-9行表示Sentinel发现了master服务有两个slave节点,端口为6370和6372。
  • 10行表示Sentinel判定6371的master服务器已经主观下线。
  • 11行的1/1表示已经满足客观下线条件,所以Sentinel将master判定为客观下线。
  • 12行表示将配置纪元自增1。
  • 13行表示要开始准备故障转移,但是需要先进性Leader选举。
  • 14行表示自己给自己投了1票,因为这里只配置了一个Sentinel,所以他成为了Leader,由它来执行故障转移。
  • 15-17行表示经过一些列条件判定之后,6370被推选为新的master。
  • 18-21行表示Sentinel像6370服务发送slaveof no one命令,使其成为新的master节点,并等待这个过程完成之后修改其配置文件。
  • 22-25行表示6372服务开始同步新的master节点数据。
  • 26行正式将master服务器的地址切换到6370(因为这里只有1主2从,挂了1个还有2个,如果还有其他从服务器,也需要依次来复制数据)。切换master地址之后客户端就可以获取到新的master服务地址。
  • 27行表示将6372服务器添加到新的master服务器的slave列表。
  • 28-29行表示将旧的master-6371服务器设置为新的master服务器的slave节点,这样当6371再次启动之后,会成为新的master服务器的slave节点。

Sentinel机制的使用

Sentinel机制下,客户端应该怎么连接上master服务呢?因为master是可能改变的,所以在Sentinel机制下,客户端需要连接上Sentinel服务,然后从Sentinel服务获得master的地址进行连接。如下图所示:
在这里插入图片描述

Jedis使用Sentinel机制

下面就是一个使用Jedis客户端使用Sentinel机制的例子。
1、引入pom依赖:

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

2、新建一个测试类TestJedisSentinel进行测试:

package com.lonelyWolf.redis.sentinel;

import redis.clients.jedis.JedisSentinelPool;
import java.util.HashSet;
import java.util.Set;

public class TestJedisSentinel {
    
    
    private static JedisSentinelPool pool;

    private static JedisSentinelPool initJedisSentinelPool() {
    
    
        // master的名字是sentinel.conf配置文件里面的名称
        String masterName = "mymaster";
        Set<String> sentinels = new HashSet<String>();
        sentinels.add("xx.xxx.xxx.xxx:26380");
//        sentinels.add("xx.xxx.xxx.xxx:26381");
//        sentinels.add("xx.xxx.xxx.xxx:26382");
        pool = new JedisSentinelPool(masterName, sentinels);
        return pool;
    }

    public static void main(String[] args) {
    
    
        JedisSentinelPool pool = initJedisSentinelPool();
        pool.getResource().set("name", "longly_wolf");
        System.out.println(pool.getResource().get("name"));
    }
}

连接时需要把所有Sentinel的连接建立并放入池内,然后客户端会遍历其中所有服务,找到第一个可用的Sentinel服务,并获取到master服务器的地址,然后建立连接。

SpringBoot使用Sentinel机制

如果使用SpringBoot,则需加入以下两个配置:

spring.redis.sentinel.master=mymaster
spring.redis.sentinel.nodes=ip:port,ip:port,ip:port

第一个参数sentinel.conf中自定义的名字,第二个参数需要配置所有的Sentinel服务器ip和端口信息。

Sentinel机制的不足之处

哨兵机制虽然实现了高可用性,但是仍然存在以下不足:

  • 1、主从切换的过程中会丢失数据,因为只有一个 master,所以在切换过程中服务是不可用的。
  • 2、哨兵机制其本质还是master-slave集群,即:1主N从。也就是master服务器依然只有1个,并没有实现水平扩展。

Redis分布式集群方案

要实现一个Redis水平扩展,需要实现分片来进行数据共享,可以有三种思路:

  • 1、在客户端实现相关的逻辑,由客户端实现分片决定路由到哪台服务器。
  • 2、将分片处理的逻辑运行一个独立的中间服务,客户端连接到这个中间服务,然后由中间服务做请求的转发。
  • 3、基于服务端实现。

客户端实现分片

客户端实现分片的话,Jedis提供了这个分片(Sharding)功能:

package com.lonelyWolf.redis.cluster.client;

import redis.clients.jedis.*;
import java.util.Arrays;
import java.util.List;

/**
 * Jedis客户端实现集群分片功能
 */
public class TestJedisSharding {
    
    
    public static void main(String[] args) {
    
    
        JedisPoolConfig poolConfig = new JedisPoolConfig();

        //创建所有的连接服务分片
        JedisShardInfo shardInfo1 = new JedisShardInfo("xx.xxx.xxx.xxx", 6370);
        JedisShardInfo shardInfo2 = new JedisShardInfo("xx.xxx.xxx.xxx", 6371);

        //将所有连接加入连接池
        List<JedisShardInfo> shardInfoList = Arrays.asList(shardInfo1, shardInfo2);
        ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, shardInfoList);

        //创建10个key值存入服务器
        ShardedJedis jedis = jedisPool.getResource();//获取连接
        for (int i=1;i<=10;i++){
    
    
            jedis.set("name" + i,"lonely_wolf" + i);
        }
        //取出key和其所在服务器信息
        for (int i=1;i<=10;i++){
    
    
            String key = "name" + i;
            Client client = jedis.getShard("name"+i).getClient();
            System.out.println("key值:" + jedis.get(key) + ",存在于服务器的端口为:" + client.getPort());
        }
    }
}

输出结果如下:
在这里插入图片描述
然后我们分别去服务器上看一下可以看到是完全匹配的:
在这里插入图片描述
在这里插入图片描述

客户端分片的缺陷

客户端实现分片的好处就是配置简单,而且分片规则都是由客户端来实现,但是却存在以下缺点:

  • 1、客户端需要支持分片,假如代码移植到其他项目,而其他项目使用的Redis客户端不支持分片,那就会造成集群不可用。
  • 2、客户端分片模式下不能很好地实现服务器动态增减。

中间代理服务实现分片

中间代理服务实现分片其实就是将客户端的分片逻辑进行抽取,然后单独部署成一个服务,这样做的好处是客户端不需要处理分片逻辑,而且也不用关心服务器的增减。

中间代理服务分片的方案有两个使用比较广泛,那就是(这两种方案如果有兴趣的可以点击对应的链接进去github获取到对应的源码进行了解):

使用中间服务的最大缺陷就是其本身是独立的服务,而为了保证高可用,也需要对这个中间服务进行高可用的集群配置,所以会导致整个系统架构更加复杂。

Redis Cluster方案

Redis Cluster是Redis3.0版本正式推出的,实现了高可用的分布式集群部署。

一个Cluster集群由多个节点组成,Redis当中通过配置文件cluster-enabled是否为yes来决定当前Redis服务是否开启集群模式,只有开启了集群模式,才可以使用集群相关的命令,与之相反的,如果没有开启集群的Redis我们称之为单机Redis。

数据分片

水平集群的最关键一个问题就是数据应该如何分配,主流的有哈希后取模一致性哈希两种数据分片方式。

哈希后取模

哈希后再取模这种方式比较简单,就是将key值进行哈希运算之后再来除以节点数,即:hash(key)%N,然后根据余数来决定落在哪个节点。这种方式数据分布会比较均匀,但是这种方式同时也是一种静态的数据分片方式,一旦节点数发生变化,需要重新计算然后将数据进行重新分布。

一致性哈希

一致性哈希的原理就是把所有的哈希值组织成一个虚拟的哈希圆环,其中起点0和终点232-1位置重叠,如下图所示:

在这里插入图片描述
上图中绿色表示当前存在的节点,黄色表示数据落点处,如果数据没有落在节点上,则会落到顺时针找到第一个节点,如上图中黄色的数据最终会落到Node1节点上。

这种方式的好处就是如果新增或者删除了节点,那么最多也只会影响相关的1个节点的数据,而不需要将所有的数据全部进行重新分片。
下图就是新增了一个Node5节点,那么其影响的就是Node2到Node5之间的这部分数据需要由原来落在Node3节点的数据改到Node5节点,对其他节点数据没有任何影响。
在这里插入图片描述

一致性哈希有一个问题就是当节点数比较少的情况下会导致数据分布不均匀。如下图所示:
在这里插入图片描述
这里面只有2个节点,但是有3条数据落在了Node2而Node1只有1条数据,分布不均匀,为了解决这种问题,一致性哈希又引入了虚拟节点。当节点过少的时候,在哈西环上创建一些虚拟节点,然后按照范围指派给实际节点。
如下图,蓝色的就是虚拟节点,其中Node1-1的数据会分到Node1上,而Node2-1会分到Nde2上,这样就可以使得数据较为平均:
在这里插入图片描述

槽(slot)

Redis当中的数据分布并没有采用以上两种数据分片方式,而是另外引入了一个槽(slot)的概念。

Redis将整个数据库划分为16384个槽(slot),然后根据当前数据库的节点数来划分,每个节点负责一部分槽。如下图所示:
在这里插入图片描述
槽数组也是一个位图数组(bit array),每个对象分配到哪个槽是根据CRC16算法得到的,需要注意的是:一个key落在哪一个槽是不会改变的,但是每个Redis Group(Redis主从服务)负责的槽可能会发生变化

如何让相关业务数据强制落在同一个槽

有时候我们同一个业务需要缓存一些数据,假如这些数据落在不同的槽归属于不同的服务器负责,那么在有些时候是会带来不便的,比如multi开启事务就不能跨节点,所以我们应该如何让相关业务数据强制落在同一个槽呢?

Redis提供了一种{}机制,当我们的key里面带有{}的时候,那么Redis只会通过计算{}里面的字符来进行哈希,所以我们可以通过这种方式来强制同一种业务数据落在同一个槽。

如下所示,我们可以将所有用户相关信息的key都带上{user_info},当然get的时候是需要带上{}这个完整的key,带上{}只是影响slot的计算,其他并不影响:
在这里插入图片描述

客户端的重定向

Redis集群中服务器的数据分布客户端是不可知的,所以假如在一个客户端获取key,然后这个key不存在当前服务器那么服务器会根据后台自己存储的信息判断出当前key所在槽归属于哪台服务器负责,会返回一个MOVED指令,带上服务器的ip和端口来告诉客户端当前key所在的服务器,客户端接收到MOVED指令之后会进行重定向,然后获取key值。

这种方式客户端获取一个key可能会需要连接2次服务器。Jedis等客户端会在本地维护一份 slot和node的映射关系,所以大部分时候不需要重定向,这种客户端也称之为smart jedis。但是这种特性并不是所有的客户端都支持的。

重新分片

Redis集群中,可以将已经指派给某个节点的任意数量的槽重新指派给另一个节点,且重新指派的槽所属的键值对也会被移动到新的目标节点,利用这个特性就可以实现新增节点或者删除节点。

重新分片的操作是可以在线进行的,也就是说在重新分片的过程中,集群不需要下线,并且参与重新分片的节点也可以继续处理命令请求。

ASK错误

在重新分片的过程中会涉及到键值对的迁移,那么可能会出现这样一种情况:被迁移槽的一部分键值对保存在目标节点里面,而还有一部分键值对仍然在原节点还没来得及前迁移。

而假如这时候客户端来访问原节点时发现本来应该在这个节点的键值对已经被迁移到目标节点了,那么这时候就会返回一个ASK错误来引导客户端到目标节点去访问,这时候客户端并不能直接访问去访问目标节点,因为目标节点在所有键值对被迁移完成前是无法直接通过正常明星访问得到的。

目标节点在接收其他节点指派过来的槽所对应键值对时,会通过一个临时数组属性importing_slots_from[16384]来存储,所以客户端在接收到返回的ASK错误之后,客户端会先向目标节点发送一个ASKING命令,之后再发送原本想要执行的命令,这样目标节点就知道当前客户端访问的key是正在迁移过来的,知道去哪里取这个数据。

需要注意的时,ASKING命令的作用是客户端会打上一个标记,而这个标记是临时性的,当服务器执行过一个带有ASKING标记的命令之后就会将该标记清除。

下图就是一个特殊情况下的执行流程图:
在这里插入图片描述

ASK错误和MOVED错误
  • ASK错误可以认为是一种过渡时期的特殊错误,只有在发生槽迁移的过程中,发现原本属于node1管理的槽被指派给了node2,而数据又还没有迁移完成的情况下,因为这是一种特殊场景,所以客户端收到ASK错误之后不能直接连到目标节点执行命令(这时候直接连过去目标节点会返回MOVED命令指向node1),客户端收到ASK错误之后需要先向node2节点发送一个ASKING命令给自己打上标记才能真正发送客户端想要执行的命令。
  • MOVED错误是在正常情况或者说槽已经完成重新分片的情况下返回的错误,这种情况服务器发现当前key所在槽不归自己管,那么就会直接MOVED错误和负责管理该槽的服务器信息,客户端收到MOVED错误之后就会再次连接目标节点执行命令。

Redis Group

Redis集群中的节点并不是只有一台服务器,而是一个由master-slave组成的集群,称之为Redis Group,那么既然是主从就会涉及到主从的数据复制,复制的原理和我们前面讲述的是一样的,但是故障检测以及故障转移和前面讲述的Sentinel模式下有点区别。

故障检测

集群中的各个节点都会定期向集群中的其他节点发送PING消息来检测对方是否在线,如果接收PING消息的节点没有在规定时间内返回PONG,那么发送PING消息的节点就会将其标记为疑似下线(probable fail,PFAIL)。

PFAIL类似于Sentinel机制下的主观下线,不同的是集群中并不是发现PFAIL之后才会去询问其他节点,而是定期通过消息的方式来交换状态。

在Redis集群中,各个节点会通过互相发送消息(PING)的方式来交换集群中各个节点的状态信息,一旦某一个主节点A发现半数以上的主节点都将某一个主节点B标记为PFAIL,这时候主节点A就会将主节点B标记为已下线(FAIL),然后主节点A会向集群中发一条主节点B已下线的FAIL消息的广播,所有收到这条广播消息的节点会立即将主节点B标记为已下线

注意:如果广播之后,发现这个master节点所在的Redis Group中,所有的slave节点也挂了,那么这时候就会将集群标记为下线状态,整个集群将会不可用

故障转移

当一个从节点发现自己的主节点已下线时,从节点将会开始进行故障转移,执行故障转移的从节点需要经过选举,如何选举在后面讲,故障转移的步骤主要分为以下三步:

  • 1、被选中的从节点会执行slave no one命令,使得自己成为新的主节点。
  • 2、新的主节点会将已下线的主节点负责的槽全部指派给自己。
  • 3、新的主节点会向集群发一条PONG消息的广播,收到这条消息的其他节点就会知道这个节点已经成为了新的master节点,并且将旧节点的槽进行了接管。
选举新的master节点

一个从节点发现主节点下线后,会发起选举,选举步骤如下:

  • 1、该从节点会将集群的配置纪元自增1(和Sentinel机制一样,配置纪元默认值也是0)。
  • 2、该从节点会向集群发一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST的消息广播。
  • 3、其他节点收到广播后,master节点会判断合法性,如果一个主节点具有投票权(正在负责处理槽),那么就会返回一个CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息给他投1票(同样的,一个配置纪元内,一个master节点最多只能投票1次)。
  • 4、如果同时有多个从节点发起投票,那么每个从节点都会统计自己所得票数,然后进行统计,只有得到了大于参与投票的主节点数的一半的从节点,就会成为新的master节点。
  • 5、如果一个配置纪元内没有一个从节点达到要求,那么集群会把配置纪元再次自增,并再次进行选举,直到选出新的master。

为什么槽定义为16384个

前面我们提到Redis集群中,判断一个key值落在哪个槽上是通过CRC16算法来计算的,CRC16算法产生的hash值有16bit,也就是可以产生216(即:65536)个值。

Redis中每秒都在发送PING消息,发送PING消息的节点会带上自己负责处理的槽信息,如果创建65536个槽(0-65535),那么就需要65536大小的位图(Bitmap)来存储,也就是需要:65535/8/1024=8k的位图数组空间,这对于频繁发送的心跳包来说太大了,而如果使用16384那么只需要16384/8/1024=2kb的位图数组空间,这是原因之一。

另一个原因是Redis集群中的节点数官方建议不要超过1000个,那么对于最大的1000个节点来说,16384个槽是比较合适的,因为16384/1000=16,也就是极端情况下每个节点负责16个slot,这是比较合适的,槽如果太小了(即slot/N不宜过大)会影响到位图的压缩。

下面截图就是Redis作者的回复:
在这里插入图片描述

手动配置一个Redis Cluster集群

配置一个Redis Cluster至少需要3个master节点,所以为了高可用,每个master节点又至少需要配置一个slave节点,也就是最低配版的高可用Redis Cluster服务是需要3主3从。

为什么至少需要3个maser节点

为什么至少需要3个master节点的原因是如果一个master挂了,至少3个节点才能执行后面的故障转移等操作。1个master就不用说了,挂了就没了;如果是2个master节点就可能出现这种情况A发现B挂了,但是A自己只有1个,也就是认为B挂了的主节点刚好等于所有主节点的一半,没大于一半,所以他就没办法断定节点B就是挂了,所以2个master也不行。

手把手搭建一个3主3从Redi集群

  • 1、首先需要把配置文件cluster-enabled参数改为yes,这样才能启动集群模式。
  • 2、还需要把配置文件cluster-config-file 修改一下,这个是节点文件,由每个节点自己管理,但是我们需要配置好文件名和路径,主要的配置文件如下所示:
port 6370 //端口号
daemonize yes  //是否后台运行
protected-mode no  //网络是否允许对外访问 no表示允许
dir /usr/local/redis-6370/  //redis工作主目录
cluster-enabled yes  //是否开启集群模式
cluster-config-file /usr/local/redis-6370/nodes-6370.conf //node文件
cluster-node-timeout 5000 //集群超时时间
appendonly yes  //是否开启aop持久化
pidfile /var/run/redis_6370.pid  //pid文件
  • 3、完成好配置之后,依次启动6个服务。
    在这里插入图片描述
  • 4、登录任意一个节点,执行cluster info命令会发现当前集群的状态是下线状态(fail)
    fail
  • 5、执行下面的创建集群命令,注意:ip要使用真实的ip,而不要使用127或者localhost等本机ip。
redis-cli --cluster create ip:6370 ip:6371 ip:6372 ip:6373 ip:6374 ip:6375 --cluster-replicas 1

执行之后会有如下提示,也就是系统自动帮我们分配好了槽而且将主从确定了:
在这里插入图片描述
-6 输入yes同意系统的分配方案,然后等待配置成功。
在这里插入图片描述

  • 7、配置成功之后纳入任意一个节点执行cluster info命令查看集群信息
    在这里插入图片描述

搭建集群常见错误

搭建集群过程中可能会出现以下错误:

  • 1、[ERR] Node 47.107.155.197:6370 is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0.
    在这里插入图片描述

这个错误有2个原因:一个就是redis当中有数据,这个执行flushall命令或者把rdb和aof两个持久化文件删除掉如果有的话,再重启即可;另一个原因就是可能初始化集群失败过1次,那么这时候需要把当时配置好的nodes.conf文件删掉,并重启Redis服务就可以解决。

  • 2、初始化集群输入yes之后提示Waiting for the cluster to join,但是却迟迟等不到成功。

这个原因可能是防火墙引起的,除了正常的数据连接端口,如6370等,还有另一个端口需要用数据端口固定加上10000得到16370。所以在上面的案例中,需要确保以下12个端口都是通的(10000以下的是正常Redis工作接口):

6370 16370 6371 16371 6372 16372 6373 16373 6374 16374 6375 16375

这个10000的偏移量是固定的,加上10000偏移量后得到的端口是集群内部用来执行故障检测、配置更新、故障转移授权等操作的,客户端不能连接这个端口。

  • 3、[ERR] Not all 16384 slots are covered by nodes.
    在这里插入图片描述

这个原因和上面第1种的错误原因类似,可能是创建集群失败过而没有清空数据导致第二次创建失败,这时候清空数据并删除对应的node.conf文件即可解决。

Redis Cluster集群常用命令

  • cluster info:打印当前集群的信息。
  • cluster nodes:打印出集群中node及其信息。
  • cluster meet :将指定ip和port的node添加到当前执行命令节点所在的集群中。
  • cluster forget <node_id>:从集群中移除某一个节点。
  • cluster replicate <node_id>:将当前节点设置指定的节点的从节点。
  • cluster saveconfig:将当前节点的配置文件保存到硬盘里面。
  • cluster keyslot :计算键key应该被放置在哪个槽上。
  • cluster countkeysinslot :返回cao目前包含的键值对数量。
  • cluster getkeysinslot :返回count个slot槽中的键。
  • cluster addslots [slot…]:将一个或多个槽( slot)指派给当前节点
  • cluster delslots [slot…] : 移除一个或多个槽对当前节点的指派
  • cluster flushslots : 移除当前节点的所有槽。
  • cluster setslot node <node_id>:将槽slot指派给指定的节点, 如果槽slot已经指派给另一个节点,则会先删除再指派。
  • cluster setslot migrating <node_id>:将本节点的槽slot迁移到指定的节点中。
  • cluster setslot importing <node_id> :从指定的节点中导入槽slot到当前节点。
  • cluster setslot stable:取消对槽slot的导入( import)或者迁移( migrate)。

客户端如何使用Redis Cluster集群

如果直接在shell上操作,那么当发现一个槽不在当前节点,我们必须根据返回的MOVED错误自己手动去返回的节点中操作,所以Redis Cluster集群要求客户端必须要实现自动重定向。

在Jedis中,只要连接任意一个或者多个节点,且不论是master还是slave节点,Jedis就可以自动实现重定向连接到集群中任意节点。

下面我们就以Jedis客户端为例来看看如何操作Redis Cluster:

package com.lonelyWolf.redis.cluster.server;

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

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

public class TestRedisCluster {
    
    
    public static void main(String[] args) throws IOException {
    
    
        HostAndPort hp1 = new HostAndPort("xx.xxx.xxx.xxx",6370);
        Set nodes = new HashSet<HostAndPort>();
        nodes.add(hp1);
        JedisCluster cluster = new JedisCluster(nodes);
        cluster.set("name", "lonely_wolf");
        System.out.println(cluster.get("name") + cluster.);
        cluster.close();
    }
}

可以看到我这边只添加了6370一个节点,但是实际上name这个key应该是归6371管的:
在这里插入图片描述
注意,这里如果Jedis的版本额Redis的版本不匹配,可能会报错,上面示例中因为Redis使用的是5.0.5版本,而假如Jedis使用的是低版本,如2.8。那么就会报一个类似于下面这个异常:
Exception in thread "main" java.lang.NumberFormatException: For input string: "6374@16374"

Redis Cluster的不足

  • 1、客户端需要能实现重定向或者实现smart client特性。
  • 2、数据的复制是通过异步复制的,不保证数据的强一致性。

总结

本文从最简单的master-slave架构入手,逐步分析了Redis中的master-slave架构,Sentinel架构的实现原理,紧急着介绍了目前运用最广泛的Redis Cluster高可用分布式架构的原理,最后还和大家一起手把手的分别搭建了master-slave集群,Sentinel集群和Redis Cluster集群,相信通过本文,大家对Redis中的三种集群方案会有深入的认识。

请关注我,和孤狼一起学习进步

猜你喜欢

转载自blog.csdn.net/zwx900102/article/details/110495732