1 搭建分片集群
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
- 海量数据存储问题,单个Redis节点对于数据的存储量是有上限的
- 高并发写的问题,高并发读的问题我们可以用主从集群来解决,那高并发写的问题又该怎样解决呢
针对上述问题,我们可以搭建Redis的分片集群,如图所示:
Redis的分片集群具有以下特征:
- 集群中有多个master,每个master保存不同数据
- 每个master都可以有多个slave节点
- master之间通过ping监测彼此健康状态(可以取代哨兵机制)
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点
接下来我们可以动手来搭建一个Redis分片集群
1.1 集群结构
分片集群需要的节点数量较多,这里我们搭建一个最小的分片集群,包含3个master节点,每个master包含一个slave节点,结构如下:
这里我们会在同一台虚拟机中开启6个redis实例,模拟分片集群,信息如下:
IP | PORT | 角色 |
---|---|---|
192.168.211.100 | 7001 | master |
192.168.211.100 | 7002 | master |
192.168.211.100 | 7003 | master |
192.168.211.100 | 8001 | slave |
192.168.211.100 | 8002 | slave |
192.168.211.100 | 8003 | slave |
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,如果指定了则表示该节点是一个slavecluster-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进行写操作而对其他节点进行读操作了。而这一过程都是自动完成的,无需我们过多关注