Redis 分片集群的如何实现

1 搭建分片集群 主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决: 海量数据存储问题,单个Redis节点对于数据的存储量是有上限的高并发写

1 搭建分片集群

主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:

  • 海量数据存储问题,单个Redis节点对于数据的存储量是有上限的
  • 高并发写的问题,高并发读的问题我们可以用主从集群来解决,那高并发写的问题又该怎样解决呢

针对上述问题,我们可以搭建Redis的分片集群,如图所示:

Redis的分片集群具有以下特征:

  • 集群中有多个master,每个master保存不同数据
  • 每个master都可以有多个slave节点
  • master之间通过ping监测彼此健康状态(可以取代哨兵机制)
  • 客户端请求可以访问集群任意节点,最终都会被转发到正确节点

接下来我们可以动手来搭建一个Redis分片集群

1.1 集群结构

分片集群需要的节点数量较多,这里我们搭建一个最小的分片集群,包含3个master节点,每个master包含一个slave节点,结构如下:

这里我们会在同一台虚拟机中开启6个redis实例,模拟分片集群,信息如下:

IPPORT角色
192.168.211.1007001master
192.168.211.1007002master
192.168.211.1007003master
192.168.211.1008001slave
192.168.211.1008002slave
192.168.211.1008003slave

1.2 准备实例和配置

这里我的redis安装目录为/usr/local/redis-6.2.6,以下操作将以此目录进行参考,额外需要注意的是,以下操作都是在redis没有设置密码的情况下进行的,如果你的redis设置了密码,那么按照以下步骤进行就会出问题。

1)创建出7001、7002、7003、8001、8002、8003目录

# 进入/local目录
cd /usr/local
# 创建目录
mkdir 7001 7002 7003 8001 8002 8003

2)在/usr/local下准备一个新的redis.conf文件,内容如下:

port 6379
# 开启集群功能
cluster-enabled yes
# 集群的配置文件名称,不需要我们创建,由redis自己维护
cluster-config-file /usr/local/6379/nodes.conf
# 节点心跳失败的超时时间
cluster-node-timeout 5000
# 持久化文件存放目录
dir /usr/local/6379
# 绑定地址
bind 0.0.0.0
# 让redis后台运行
daemonize yes
# 注册的实例ip
replica-announce-ip 192.168.211.100
# 保护模式
protected-mode no
# 数据库数量
databases 1
# 日志
logfile /usr/local/6379/run.log

3)将这个文件拷贝到每个目录下:

# 进入/local目录
cd /usr/local
# 执行拷贝
echo 7001 7002 7003 8001 8002 8003 | xargs -t -n 1 cp redis.conf

4)修改每个目录下的redis.conf,将其中的6379修改为与所在目录一致:

# 进入/local目录
cd /usr/local
# 修改配置文件
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t sed -i 's/6379/{}/g' {}/redis.conf

1.3 启动

因为已经配置了后台启动模式,所以可以直接启动服务:

# 进入/usr/local目录
cd /usr/local
# 一键启动所有服务
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-server {}/redis.conf

通过ps查看状态:

ps -ef | grep redis

发现服务都已经正常启动:

如果要关闭所有进程,可以执行命令:

ps -ef | grep redis | awk '{print $2}' | xargs kill

或者(推荐这种方式):

printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->} -t redis-cli -p {<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->} shutdown

1.4 创建集群

虽然服务启动了,但是目前每个服务之间都是独立的,没有任何关联。我们需要执行以下命令来创建集群,注意,以下命令需要你的redis版本在5.0之后:

redis-cli --cluster create --cluster-replicas 1 192.168.211.100:7001 192.168.211.100:7002 192.168.211.100:7003 192.168.211.100:8001 192.168.211.100:8002 192.168.211.100:8003

命令说明:

  • redis-cli --cluster:代表集群操作命令
  • create:代表创建集群
  • --cluster-replicas 1 :指定集群中每个master的副本个数为1,也就是说一个master只有一个slave,此时节点总数 ÷ (replicas + 1) 得到的就是master的数量。因此节点列表中的前n个就是master,其它节点都是slave节点,在创建集群时这些slave会被随机分配给不同master

执行上述命令之后,控制台会列出当前主从节点分配的结果,即将那些slave分别分配给哪些master,并询问你是否同意,这里我们输入'yes'即可

确定之后,集群开始创建

通过命令可以查看集群状态:

redis-cli -p 7001 cluster nodes

1.5 测试

集群操作时,需要在redis-cli连接时带上-c参数才可以

redis-cli -p 7001

通过观察上述从节点的状态,我们发现7001的slave是8001,我们可以尝试在7001里插入一个数字,再从8001里获取

事实上,我们不仅可以从8001里获取到num,也可以从其他slave甚至其他master里获取到num:

而且我们发现,当我们试图从其他节点获取num时,最后都会跳转到7001,为什么会这样呢?这就涉及到我们即将讲解的插槽原理

2 散列插槽

一个Redis分片集群有0~16383共16384个插槽(hash slot),这些插槽会被平均分给每一个master节点,一个master节点映射着一部分插槽,这一点在集群创建时的信息中可以看到

在分片集群中,数据key并不是与某个节点绑定,而是与插槽绑定。数据key与插槽是多对一的关系,redis会根据key的有效部分计算插槽值,然后将key放入对应插槽,key的有效部分分两种情况:

  • 当key中包含"{}“时,且”{}“中至少包含1个字符,”{}"中的部分是有效部分
  • key中不包含"{}",整个key都是有效部分

举个例子,假如key是num,那么插槽值就会根据num来计算,如果key是{itheima}num,那么插槽值就会根据itheima来计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。

如果当前操作的key所在的插槽不属于本节点,则会发生重定向,重定向的目标是该插槽所属的节点,这个节点一定是master,如果我们连接的节点为slave,则会直接进行重定向,因为slave是没有插槽的。

针对上述几点,演示如下:

如上图所示,我们连接了7001,并试图插入数据k1,这时redis需要去判断k1所属的插槽位置,由于key中不包含’{}',因此整个key都是有效部分,redis会对k1做hash运算然后对16384取余,得到的结果是12706,这也就是k1所在的插槽的位置,在当前集群中,映射该插槽的节点是7003,此时就会发生重定向,我们也可以观察到当我们执行完set k1 1命令之后,操作的端口已经变成了7003。此时我们继续在7003端口中进行操作,比如修改数据num的值,而num所在的插槽是2765,在当前集群中,映射该插槽的节点是7001,因此当我们执行完set num 2命令之后,操作的端口又重新变成了7001

那么接下来让我们思考两个问题:

为什么数据key要与插槽绑定,而不是与节点绑定呢?

这是因为Redis的主节点有可能会出现宕机情况,也有可能由于集群伸缩而被删除,当节点删除或者发生宕机时,节点上保存的数据也就丢失了,但如果数据绑定的是插槽而不是节点,那么当出现上述情况时,就可以将故障节点的插槽转移至存活节点上。这样,数据跟插槽绑定,就永远都能够找到数据所在位置。

如何将同一类数据固定的保存在一个插槽中

在业务开发中,同一类型的数据key最好是保存在一个插槽中,因为如果分散保存,在我们调用的时候就很可能出现重定向的情况,重定向是会消耗一部分性能的。如果我们希望将同一类型的数据key最好是保存在一个插槽中,可以为这些key带上一个用’{}'包裹的固定前缀,比如{user}zs、{user}ls等等,因为我们之前说过,当key中包含"{}“,且”{}“中至少包含1个字符时,”{}"中的部分是有效部分,redis会根据这一部分来计算插槽值,如果我们将同一类型的key都加上这类前缀,就能保证这些key在同一个插槽中了

3 集群伸缩

集群已经创建了,那么如果我们想在集群中添加节点或删除节点,又应该怎么做呢?

redis-cli --cluster提供了很多操作集群的命令,可以通过下面方式查看:

其中就包括添加节点的命令:

假设现在有以下需求:向集群中添加一个新的master节点7004,并在这个节点中存储 num = 10,执行步骤如下:

3.1 创建节点并添加到集群

1)在/usr/local目录下创建一个文件夹:

cd /usr/local
mkdir 7004

2)拷贝配置文件:

cp redis.conf 7004

3)修改配置文件:

printf '%s\n' 7004 | xargs -I{<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->} -t sed -i 's/6379/{}/g' {<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->}/redis.conf

4)启动

redis-server 7004/redis.conf

接下来就需要将7004添加到集群中了,在执行添加操作之前,我们先来了解以下添加节点的命令

添加节点首先需要以下几个参数:

  • new_host:new_port :指定新添加的节点的ip地址与端口号,这个没什么好说的
  • existing_host:existing_port:任意指定一个集群中已经存在的节点的ip地址与端口号。因为集群中加入新节点是需要通知其他旧节点的,新节点只需要将自己的信息提供给集群中任意一个节点,那么整个集群就都能知道关于新节点的信息了
  • cluster-slave:可选项,不指定则表示该节点是master,如果指定了则表示该节点是一个slave
  • cluster-master-id <arg>:如果我们指定了cluster-slave,那么就需要通过该参数指定该节点的master的id

了解了该命令之后,接下来我们来执行添加节点操作:

执行命令:

redis-cli --cluster add-node 192.168.211.100:7004 192.168.211.100:7001

通过命令查看集群状态:

redis-cli -p 7001 cluster nodes

如图,7004加入了集群,并且默认是一个master节点:

但是,我们也可以看到7004是没有插槽的,因为插槽已经被其他master瓜分完毕了,因此没有任何数据可以存储到7004上,这时候我们就需要进行插槽的转移,即将其他matser的插槽分出一部分给7004

3.2 转移插槽

首先回归需求本身,我们的需求是将num=10存储在7004节点上,那么我们的目的就很明显了,首先需要知道num存储在哪个插槽上,然后将这个插槽转移到7004上即可

如上图所示,num的插槽为2765,该插槽目前是保存在7001上的,因此我们可以将0~3000的插槽从7001转移到7004,转移插槽的命令格式如下:

具体步骤如下:

1)输入转移插槽命令,这里我们需要转移的插槽在7001上,因此需要指定7001的地址

redis-cli --cluster reshard 192.168.211.100:7001

2)系统会询问我们要移动多少个插槽,这里我们输入3000即可

3)系统接着询问我们需要让哪个节点来接收插槽,这里我们需要输入7004的ID

4)接着系统会询问我们要从哪些节点中移动这些插槽到7004

这里我们有三个选择:

  • all:代表全部,也就是三个节点各转移一部分
  • 具体的id:目标节点的id
  • done:表示结束

这里我们需要从7001中获取插槽,因此填写7001的id,然后输入done表示结束

5)接下来会冒出一大串东西,并询问我们是否确定要移动这些插槽,这里我们直接输入yes即可

输入yes之后,等待控制台打印结束,插槽也就移动完毕了

6) 输入以下命令查看插槽是否已经移动到7004

redis-cli -p 7001 cluster nodes

很显然,我们的目的已经达成了

那么如果我们要删除7004节点,又应该怎样做呢?这里笔者只给去具体思路,大家可以自行尝试一下:

  • 先将 7004 分配的插槽转移至其他节点
  • 执行删除节点命令redis-cli --cluster del-node host:port node_id
  • 通过命令查询结果redis-cli -p 7001 cluster nodes

4 故障转移

之前我们提到过,redis分片集群可以通过master之间的心跳监测来监测彼此之间的健康状态,从而取代哨兵。而我们也知道,哨兵的作用就是监测master和slave的状态,当认为一个master客观下线后,会从slave中选举出一个新的master,现在让我们来验证一下redis分片集群是否具备这个功能。

首先集群的初始状态是这样的,如果状态为connected则表示节点正常连接

其中7001、7002、7003、7004都是master,我们计划让7002宕机。

4.1.自动故障转移

当集群中有一个master宕机会发生什么呢?我们可以直接停止一个redis实例,例如7002:

redis-cli -p 7002 shutdown

1)首先是该实例与其它实例失去连接

2)然后是疑似宕机,7002的状态变成了disconnected

3)最后是确定下线,将7002的一个slave提升为新的master,这里由于7002只有一个slave,即8002,因此8002被选为了新的master

4)接下来我们通过以下命令再次启动7002节点

redis-server /usr/local/7002/redis.conf

当7002再次启动之后,它就已经变为一个slave节点了

上面这种叫自动故障转移,但有的时候我们可能需要做手动故障转移,比如当某台master机器比较老旧,需要升级时,我们就可以在这个集群中新增一个节点,让这个节点成为取代原来的master成为新的master,这样原来的master就会变成新master的一个slave,从而实现机器的无感知升级

4.2 手动故障转移

我们可以在slave节点中使用cluster failover命令,这个命令会让当前slave节点的master暂时宕机,宕机期间会将自身的数据转移给执行命令的slave节点,宕机结束后,之前的master会变成slave,而执行命令的slave会变成新的master。

其详细流程如下:

当slave执行了cluster failover命令之后,就会向master节点发送节点替换通知,为了避免数据的丢失,master接收到slave节点发送过来的通知后,就会暂时拒绝来自客户端的任何数据读写请求。然后,master会将自己当前的offset返回给slave,slave接收到后会判断自身数据中的offset与master的offset是否一致,如果不一致,则需要进行数据同步。由于 master 已经拒绝了客户端的所有请求,那么一旦 slave完成数据同步,也就表示slave与master之间数据是完全一致的。

数据同步结束之后,就会开始进行故障转移,让slave与master 进行角色互换,该slave成为新的master,而旧的master则转变为一个新的slave。转移结束后,slave便会标记自己为master,并向集群中每一个节点广播故障转移的结果。当集群中节点收到广播后,后续的所有交互便转移至新的master。

这种failover命令可以指定三种模式:

缺省:默认的流程,如图1~6歩,一般我们会选择这个force:省略了对offset的一致性校验,直接开始故障转移takeover:直接执行第5歩,忽略数据一致性、忽略master状态和其它master的意见

接下来我们可以尝试一下,在7002这个slave节点上执行手动故障转移,让它重新夺回master地位,步骤如下:

1)利用redis-cli连接7002,并执行cluster failover命令

2)通过redis-cli -p 7001 cluster nodes命令查看节点状态

5 RedisTemplate访问分片集群

我们只需要通过以下简单的配置,就可以通过Java代码访问我们之前部署好的分片集群

1)在boot项目的pom文件中导入依赖

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

2)在application.yml中指定sentinel相关信息:

spring:
  redis:
    cluster:
      nodes: #指定分片集群中每一个节点信息
        - 192.168.150.101:7001
        - 192.168.150.101:7002
        - 192.168.150.101:7003
        - 192.168.150.101:8001
        - 192.168.150.101:8002
        - 192.168.150.101:8003

3)在项目的启动类中,添加一个新的bean,这个bean是用来做Redis集群的读写分离的

@Bean
public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer(){
    return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}

bean中配置的信息是读写策略,包括四种可选项:

  • MASTER:从master读取
  • MASTER_PREFERRED:优先从master节点读取,master不可用才读取slave
  • REPLICA:从slave节点读取
  • REPLICA _PREFERRED:优先从slave节点读取,所有的slave都不可用才读取master

上述配置完毕之后,我们就可以正常使用RedisTemplate来对redis集群进行操作,如果集群中某个的master宕机了,集群就会自动选举新的master,并将新master的信息发送给该Java程序,这样Java程序就可以对新master进行写操作而对其他节点进行读操作了。而这一过程都是自动完成的,无需我们过多关注

到此这篇关于Redis 分片集群的实现的文章就介绍到这了,更多相关Redis 分片集群内容请搜索好代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持好代码网!

标签: Redis 分片集群