[Redis] Redis (4) Redis Cluster 多分片集群架构
目录
- 序
- 概述:Redis 架构
- 数据分片算法
- 搭建redis集群(三分片三主六从架构)
- 故障处理
- 集群扩容
- Jedis 客戶端的支持: JedisCluster
- X 参考文献
序
- 近来几个月,一直被多套生产环境的各类性能问题所困,持续优化、持续改进。
- 这不,有一套环境的 flink 作业(
hmset
操作:近千个 hash feild,且高并发(每秒100+,未来还会爆炸式增长数倍))的性能瓶颈阻塞在了redis
上(CPU飚满、带宽飚满)。
- redis 5.0
- 单分片1主1从架构 (即:单核单线程)
- 内存:4gb
-
这种【数据密集型写入 + 高并发】场景下,单纯地纵向增加 redis 规格(内存)是不可行了,扩增
CPU
规格和带宽规格也更不可能(云厂商内部所有Redis的CPU介质和规格是固定不变的) -
那么,Redis 的优化只能从如下几方面入手:
- 方面0:适当降低 hmset 的操作频次
- 方面1:调优redis自身配置
- appendonly :
yes
=>replia-only
/no
/ ...
- 方面2:变更REDIS架构为多核多线程架构 【暂不考虑】
- 对应某为云厂商的REDIS企业版
- 为了解决单线程架构的瓶颈问题,Redis 在
6.0
版本中引入了多线程网络 I/O 处理机制。这标志着 Redis 开始部分支持多线程,旨在提升 Redis 在高并发场景下的性能表现。- 尽管 Redis 在 6.0 版本中引入了多线程,但它并没有完全转换为多线程架构,而是采用了“部分多线程”的策略。具体来说,Redis 将网络 I/O 操作从主线程中分离出来,交由多个工作线程来处理,而主线程仍然负责执行具体的命令。
- 详情参见:[Redis] Redis 多核多线程模型 - 博客园/千千寰宇
- 弊端:不适用于数据密集型写入场景;且涉及到升级 redis 大版本
- 方面3:变更REDIS架构为Redis Cluster多主机多分片集群架构
- 为此,需要考察考察 Redis Cluster 多主机模式下的多分片集群架构。
概述:Redis 架构
Redis 架构
主从模式
哨兵模式(Redis Sentinel)
- REDIS 哨兵模式的产生原因:Redis是一种流行的内存数据库,具有快速、灵活和可扩展的特性。然而,在应用程序对数据可用性和可靠性要求更高时,Redis主从复制会遇到一些限制和弊端,为了解决这些问题,Redis引入了哨兵模式:
- 假如主机宕机之后,整个redis服务只能读、不能写。
- REDIS 哨兵模式的定义
- Redis 哨兵模式是一种用于构建高可用性 Redis 集群的解决方案。
Redis Sentinel
(哨兵)是一个分布式系统,用于管理多个Redis服务器实例。- 它的主要目的是提供高可用性和故障转移(failover)功能。当主服务器(master)出现故障时,
Sentinel
能够自动将一个从服务器(slave)提升为新的主服务器,从而确保数据不丢失并保持服务的可用性。- 它通过监控 Redis 实例的状态并自动进行故障转移,提供了客户端重定向机制以确保应用程序可以正常访问 Redis。
- 三个哨兵(sentinel, 英译: 哨兵、守卫):自动监控和维护集群,不存放数据,只做吹哨人
- 1主2从:用于读取和存放数据
- 哨兵模式能干嘛:
- 主从监控:监控reids主从是否正常运行
- 消息通知:哨兵可以将故障转移的结果发送给客户端
- 故障转移:如果Master异常,则会进行主从切换,将其中一个Slave作为新的Master
- 配置中心:客户端通过连接哨兵来获得当前Redis服务的主节点地址
- 哨兵模式的主要组件和功能:
- 监控(Monitoring):Sentinel不断地检查主服务器和从服务器的状态,确保它们运行正常。
- 通知(Notification):当主服务器出现问题时,Sentinel可以通过API通知管理员或其他应用。
- 自动故障转移(Automatic failover):如果主服务器失效,Sentinel会将一个从服务器提升为新的主服务器,并通知其他Sentinel以及客户端关于这一变化。
- 配置提供者(Configuration provider):客户端在初始化时会连接到Sentinel,通过Sentinel获取当前主服务器的地址。
- Redis Sentinel的优点:
- 它可以自动监控Redis实例,当发生故障时会进行自动故障转移和选举。Redis Sentinel是Redis官方提供的高可用方案,不需要修改Redis的源代码,同时可以支持多个Redis实例,可以在集群中起到负载均衡的作用
- 适合读写分离的场景
- Redis Sentinel的缺点:
- 只能做Master/Slave模式的同步复制,Master节点的数据一旦出现问题就无法被恢复。
- 无法支持跨节点事务和故障恢复
- 配置 Sentinel
- 详情参见: Redis基础系列-哨兵模式 - CSDN
- 要配置Redis Sentinel,你需要创建一个或多个sentinel配置文件(通常是.conf文件),每个文件包含以下基本配置:
# 示例sentinel.conf文件
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1
sentinel monitor
指令用于添加一个新的监控目标,这里mymaster是监控组的名称,127.0.0.1 6379是主服务器的地址和端口,2是该组中至少有多少个Sentinel同意主服务器宕机后才开始进行故障转移。sentinel down-after-milliseconds
指定在多长时间内,如果主服务器没有响应,则认为它已经宕机。sentinel failover-timeout
定义了故障转移的超时时间。sentinel parallel-syncs
定义了在执行故障转移时,可以并行同步的从服务器数量。
- 启动 Sentinel
redis-sentinel /path/to/sentinel.conf
假如启动3个哨兵:
redis-sentinel sentinel26379.conf --sentinel
redis-sentinel sentinel26380.conf --sentinel
redis-sentinel sentinel26381.conf --sentinel
- 客户端配置
客户端连接到Sentinel来获取当前主服务器的地址,而不是直接连接到主服务器。例如,使用Redis的Python客户端时,可以这样做:
from redis.sentinel import Sentinel
sentinel = Sentinel([('localhost', 26379)], socket_timeout=0.1)
master = sentinel.master_for('mymaster', socket_timeout=0.1)
print(master.get("some_key"))
- 注意事项
- 确保Sentinel实例的数量至少为监控组中定义的数量加一,以避免脑裂(split-brain)问题。
- 监控组中的每个
Sentinel
都应该能够访问所有主从服务器的信息。- 在生产环境中,通常会有多个Sentinel实例分布在不同的物理机上,以提高系统的可靠性和可用性。
通过以上步骤,你可以设置和使用Redis的Sentinel模式来保证Redis服务的高可用性。
代理模式(代理分片/Twemproxy)
- Redis代理分片用得最多的就是
Twemproxy
,由Twitter开源的Redis代理,其基本原理是:
通过中间件的形式,Redis客户端把请求发送到Twemproxy,Twemproxy根据路由规则发送到正确的Redis实例,最后Twemproxy把结果汇集返回给客户端。
- Twemproxy作为一种代理层,它可以提供数据分片和负载均衡功能,可以支持多个Redis节点。
- Twemproxy可以将访问请求路由到对应的节点,从而实现高效的数据读写和负载均衡。
Twemproxy通过引入一个代理层,将多个Redis实例进行统一管理,使Redis客户端只需要在Twemproxy上进行操作,而不需要关心后面有多少个Redis实例,从而实现了Redis集群。
- Twemproxy的优点:
- 简单易用,不需要改变现有的应用程序,可以快速地部署和管理。
客户端像连接Redis实例一样连接Twemproxy,不需要改任何的代码逻辑。
- 支持无效Redis实例的自动删除。
- Twemproxy与Redis实例保持连接,减少了客户端与Redis实例的连接数。
- Twemproxy的缺点:
- 但Twemproxy不支持数据的自动分片和故障转移,需要人工介入管理。 (适合小规模集群、非核心/非敏感业务)
- 性能损失:由于Redis客户端的每个请求都经过Twemproxy代理才能到达Redis服务器,这个过程中会产生性能损失
- 没有友好的监控管理后台界面,不利于运维监控。
- 无法平滑地扩容/缩容
集群模式(多主机多分片集群模式)
- 广义的集群:只要是多台机器,构成一个分布式系统,就可以称为一个“集群”。像前面的主从结构,【哨兵模式】都是“广义的集群”
- 狭义的集群:redis提供的集群模式(Cluster Arch),这个集群模式主要解决"单线程/单核架构下CPU资源不足"(主因)、****内存空间不足**(主因)、"单实例带宽资源不足"的问题
例如,整个数据全局是1TB,引入三组Master/Slave来存储(三分片三主三从架构的Cluster架构),那么:每一组Master/Slave存储数据全集的一部分,从而构成一个更大的整体,称为redis集群(Cluster)
在上图中,这三组机器存储的数据都不一样,每个slave都是对应master的备份(当master挂了,就会选举一个slave成为新的master)。
每个红框部分称为一个分片,如果全量数据进一步增加,只要再增加更多,即可解决。
Redis 多分片集群架构的特点
- redis cluster主要是针对海量数据+高并发+高可用的场景,海量数据。
如果你的数据量很大,那么建议就用 redis cluster;
数据量不是很大时,使用sentinel就够了。
redis cluster的性能和高可用性均优于哨兵模式。
【优点】:数据分片(水平扩展)+负载均衡 => 分担CPU负载 和 内存数据的负载,提高总体处理效率
- 用于在多个Redis节点之间共享数据和负载。它通过将数据分片存储在多个节点上,实现数据的横向扩展和高可用性。
- 数据分片(水平扩展): 通过将数据分片存储在多个节点上,Redis集群可以水平扩展,处理更大的数据量和更高的并发请求。
- 负载均衡:Redis集群会自动将请求路由到正确的节点,实现负载均衡,提高系统的整体性能。
【优点】:高可用性
- Redis集群采用主从复制的方式,当主节点发生故障时,可以自动切换到从节点,保证服务的可用性。
【缺点】:仅支持单个DB
Redis Cluster
是一个由多个 Redis 节点组成的集群,每个节点都可以存储数据。
根据 Redis 官方文档的描述,
Redis Cluster
目前仅支持一个数据库库,即默认的0
号库。这意味着每个节点上只能存储一个数据库,无法创建多个数据库库。
虽然 Redis Cluster 不支持多个数据库库,但可以通过使用key
前缀的方式来模拟多个数据库的功能。通过为不同业务或模块的数据添加不同的前缀,可以实现逻辑上的数据库划分。但需要注意的是,这种方式并不是真正意义上的多个数据库,只是一种简单的模拟。
【缺点】:配置复杂
- 在搭建和配置Redis集群时,需要关注节点的部署、槽的分配和数据迁移等细节,相对比较复杂。
【缺点】:不支持跨节点事务
- Redis集群模式不支持跨节点的事务操作,因为事务操作需要在同一个节点上执行。
【缺点】:增加节点时,涉及数据迁移
【缺点】额外的内存消耗
- 额外的内存消耗(可以暂时忽略这个缺点):为了实现高可用性和数据分片,Redis集群需要维护额外的节点和槽的信息,会占用一定的内存资源。
数据分片算法
哈希求余
基本思路
借鉴哈希表的基本思想
- 针对要插入的数据的
key
(redis都是键值对结构)计算hash
值(比如:使用MD5计算hash值)。
md5
是一个非常广泛使用的hash算法
- md5计算的结果是定长的,无论输入的原字符串多长,最终算出的结果都是固定长度
- md5计算的结果是分散的,两个原字符串,哪怕大部分都相同,只有一小部分不同,算出来的结果也会差别很大。因此使用md5作为hash函数,可以有效避免hash冲突
- md5计算的结果是不可逆的,给你原字符串,很容易算出md5值;给你md5值,很难还原出原始字符串。因此常使用md5加密
- 再把这个hash值余上分片个数,就得到一个下标。
- 此时,就可以把这个数据放到该下标对应的分片中。
即
hash(key) % N
优缺点
- 优点
- 简单高效,数据分配均匀
- 缺点
- 随着业务增长,数据变多,现有分片不够使用。
- 需要进行“扩容”,需要重新进行
hash
,计算新的下标。
上图中一共20个数据,只有3个数据不需要搬运,如果是20亿的数据,就需要搬运17亿!!!
并且每个分片中不止有主节点还有从节点,需要进行主从同步,开销特别大。
一致性哈希
- 使用
hash
求余中,当前key属于哪个分片是交替的。
像上图中,102属于0号分片,103属于1号分片,104属于2号分片,105又数据0号分片,交替出现,导致搬运成本非常高。
-
而一致性哈希把交替出现,改进成连续出现。
-
把
0->2^32-1
的数据空间,映射到一个圆环上,数据按照顺时针方向增长
- 假设当前存在三个分片,就把分片放到圆环的某个位置上
- 假定有一个key,计算得到的hash值为H,那么这个key映射到哪个分片规则是,从H所在位置,顺时针往下找,找到第一个分片,就是该key所从属的分片
- 假定有一个key,计算得到的hash值为H,那么这个key映射到哪个分片规则是,从H所在位置,顺时针往下找,找到第一个分片,就是该key所从属的分片
N个分片把整个圆环分成了N个管辖区间,key的hash值落在某个区间内,就归对应区间管理
扩容
从3个分片扩容到4个分片时
- 此时,只需要将0号分片上的部分数据给搬运到3号分片上即可,1,2号分片管理的区间不变。
- 优点:大大降低了扩容是数据搬运的规模,提高了扩容操作的效率
- 缺点:数据分配不均匀,会数据倾斜
哈希槽分区算法
-
这是
redis
采用的分片算法,可以有效解决搬运成本高和数据分配不均的问题,redis cluster引入哈希槽(hash slots
)算法。
:::tips
hash_slot = crc16(key) % 16384
::: -
hash_slot 哈希槽
其中crc16也是一种hash算法
16384 = 16 * 1024 即 16k
- 相当于把整个哈希值,映射到16384个槽位上,即[0,16383].
然后再把这些槽位比较均匀的分配给每个分片,每个分片的节点都需要记录自己持有的那些槽位。
例如当前有3个分片,可能的分配方式:
0号分片:[0,5461],共5462个槽位
1号分片:[5462,10923],共5463个槽位
2号分片:[10924,16383],共5460个槽位
每个分片会使用“位图”这样的数据结构表示出当前有多少槽位。
16384个bit位,用每一位的0/1来区分自己这个分片当前是否持有该槽位号。16384%8=2048,即2kb
扩容
例如,新增一个分片,就需要针对原有的槽位进行重新分配
0号分片: 【0,4095】共4096个槽位
1号分片:【5462,9557】共4096个槽位
2号分片:【10924,15019】共4096个槽位
3号分片:【4096,5461】+【9558,10923】+【15020,16383】共4096个槽位
在上述过程中,只有被移动的槽位,对应的数据才需要被搬运。并且分片上的槽位号,不一定是连续的区间。
- 哈希槽分区算法的实质:结合了哈希算法和一致性哈希的思想
FAQ
问题1:redis集群最多有16384个分片吗?
- 虚拟槽分区是Redis Cluster采用的分区方式
- 预设虚拟槽,每个槽就相当于一个数字,有一定范围。每个槽映射一个数据子集,一般比节点数大
Redis Cluster中预设虚拟槽的范围为0到16383
1 把16384槽按照节点数量进行平均分配,由节点进行管理
2 对每个key按照CRC16规则进行hash运算
3 把hash结果对16383进行取余
4 把余数发送给Redis节点
5 节点接收到数据,验证是否在自己管理的槽编号的范围,如果在自己管理的槽编号范围内,则把数据保存到数据槽中,然后返回执行结果,如果在自己管理的槽编号范围外,则会把数据发送给正确的节点,由正确的节点来把数据保存在对应的槽中。
Redis Cluster的节点之间会共享消息,每个节点都会知道是哪个节点负责哪个范围内的数据槽
- 一共有16384个槽位,如果是16384个分片,意味着一个分片一个槽位,此时很难保证数据在各个分片的均衡性。
key要先映射到槽位,再映射到分片中。
如果每个分片的包含的槽位比较多,槽位个数相当,就可以认为包含的key的数量也是相当的。
如果每个分片包含的槽位非常少,槽位个数不一定能直观反应到key的数目,因为有的槽位,有多个key,有的槽位,可能没有key.并且redis作者建议集群分片数不超过1000
问题2:为什么是16384个槽位
- redis作者答案:https://github.com/antirez/redis/issues/2576
- 节点之间通过心跳包通信,心跳包中包含该节点持有拿下slots,这个是使用位图表示,表示16384(16k)个slots,需要位图(bitmap)大小是2kb。
如果给定的slots数更多,比如65536,需要8kb位图表示,8kb对于内存不算什么,但是在频繁的网络心跳包中,是一个不小的开销。
另一方面,redis集群不建议超过1000个分片,所以16k对于最多1000个分片来说是足够用的,同时也会使对应的槽位配置位图体积不至于很大。
搭建redis集群(三分片三主六从架构)
- 三分片三主六从架构
即 每个分片,对应:1个主节点、2个从节点
- 基于docker,搭建一个集群,每个节点都是一个容器
拓扑结构:
注意:我们一共会创建11个redis节点,其中前9个用来演示集群搭建,后两个用来演示集群扩容
创建目录和配置
- step1 创建
redis-cluster
目录,内部创建2个文件:docker-compose.yml
和generate.sh
docker-compose.yml
version: '3.3'
networks:
mynet:
ipam:
config:
- subnet: 172.30.0.0/24
services:
redis1:
image: 'redis:5.0.9'
container_name: redis1
restart: always
volumes:
- ./redis1/:/etc/redis/
ports:
- 6371:6379
- 16371:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.101
redis2:
image: 'redis:5.0.9'
container_name: redis2
restart: always
volumes:
- ./redis2/:/etc/redis/
ports:
- 6372:6379
- 16372:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.102
redis3:
image: 'redis:5.0.9'
container_name: redis3
restart: always
volumes:
- ./redis3/:/etc/redis/
ports:
- 6373:6379
- 16373:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.103
redis4:
image: 'redis:5.0.9'
container_name: redis4
restart: always
volumes:
- ./redis4/:/etc/redis/
ports:
- 6374:6379
- 16374:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.104
redis5:
image: 'redis:5.0.9'
container_name: redis5
restart: always
volumes:
- ./redis5/:/etc/redis/
ports:
- 6375:6379
- 16375:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.105
redis6:
image: 'redis:5.0.9'
container_name: redis6
restart: always
volumes:
- ./redis6/:/etc/redis/
ports:
- 6376:6379
- 16376:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.106
redis7:
image: 'redis:5.0.9'
container_name: redis7
restart: always
volumes:
- ./redis7/:/etc/redis/
ports:
- 6377:6379
- 16377:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.107
redis8:
image: 'redis:5.0.9'
container_name: redis8
restart: always
volumes:
- ./redis8/:/etc/redis/
ports:
- 6378:6379
- 16378:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.108
redis9:
image: 'redis:5.0.9'
container_name: redis9
restart: always
volumes:
- ./redis9/:/etc/redis/
ports:
- 6379:6379
- 16379:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.109
redis10:
image: 'redis:5.0.9'
container_name: redis10
restart: always
volumes:
- ./redis10/:/etc/redis/
ports:
- 6380:6379
- 16380:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.110
redis11:
image: 'redis:5.0.9'
container_name: redis11
restart: always
volumes:
- ./redis11/:/etc/redis/
ports:
- 6381:6379
- 16381:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.111
generate.sh
//此脚本还需细化:
for port in ...
## redis{number}/redis.conf
port 6379
bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.30.0.10
cluster-announce-port 6379
cluster-announce-bus-port 16379
done
- step2 关掉所有启动的redis容器,防止后续发生端口冲突
可以通过
docker ps -a
来查看是否全部关闭
- step3 执行shell脚本,
generate.sh
内容:
bash generate.sh
生成的目录:
其中每个
redis.conf
都不相同,以redis1为例:
区别在于每个配置中
cluster-announce-ip
是不同的,其他部分都相同,因为后续会给每个节点分配不同的ip地址
- 配置说明:
- cluster-enabled yes` 开启集群
cluster-config-file nodes.conf
集群节点生成的配置cluster-node-timeout 5000
节点失联的超时时间cluster-announce-ip 172.30.0.101
节点自身的ipcluster-announce-port 6379
节点自身的业务端口cluster-announce-bus-port 16379
管理端口,用来给一些管理上的任务通信的。例如主节点挂了,需要让从节点成为主节点,就需要通过刚才管理端口来完成对应的操作
启动容器
- 启动之前检查redis服务是否全部关闭
ps aux | grep redis
启动容器
docker-compose up -d
- 验证是否启动
docker ps -a
构建集群
此处,把前9个主机构成集群,3主6从。后两个主机用来演示后续扩容
redis-cli \
--cluster create 172.30.0.101:6379 172.30.0.102:6379 172.30.0.103:6379 172.30.0.104:6379 172.30.0.105:6379 172.30.0.106:6379 172.30.0.107:6379 172.30.0.108:6379 172.30.0.109:6379 \
--cluster-replicas 2
使用集群
- 此时,使用客户端连上集群中的任一节点,都相当于连上整个集群。
- 使用命令
redis-cli -h 172.30.0.101 -p 6379 -c
- 客户端后面要加上
-c
选项
- 因为通过“哈希槽分区算法”计算出对应的哈希槽,如果该哈希槽不在该分片,而是在其他分片就访问不到。
- 加上
-c
就会自动把请求重定向到对应的节点
- 使用
cluster nodes
可以查看整个集群的情况
- 如果在从节点进行写操作,会重定向到master节点
故障处理
主节点宕机
- 在上述的拓扑结构中,
redis1,redis2,redis3
是主节点,挑选一个停掉
docker stop redis1
- 重新启动redis1
docker start redis1
故障处理流程
1. 故障判定,识别出某个节点是否挂了
- 节点A给节点B发送ping包,B就给A返回pong包,包含集群的配置信息
其提供的集群信息包括:该节点的id,该节点从属于哪个分片,是主节点还是从节点,属于哪个主节点,持有哪些
slots
的位图
- 每个节点,每秒钟都会随机给一些节点发送ping包,而不是全发一遍。
这样的设定是为了避免如果节点很多,心跳包也会很多(例如9个节点,如果全发,就是9*8=72组心跳包,而且是按照N^2级别增长)
- 当节点A给节点B发送ping包,B不能如期回应的时候,此时A就会尝试重置和B的tcp连接,看是否能连接成功。
如果仍然连接失败,A就会把B设为
PFALL
状态(主观下线)
- A判定B为
PFALL
之后,会通过redis内置的Gossip
协议,和其他节点进行沟通,向其他节点确认B的状态
每个节点都会维护一个自己的”
下线列表
“,由于视角不同,每个节点的下线列表也不一定相同
- 此时,A发现很多其他节点也认为B为
PFALL
,并且数目超过集群个数的一半,那么:A就会把B标记FALL
(客观下线),并把消息同步给其他节点,其他节点收到后,也会把B标记为FALL
2. 故障迁移
-
如果B是从节点,那么:【不需要】进行故障迁移
-
如果B是主节点,那么:就会从B的从节点中挑选一个(比如C和D)触发故障迁移
-
从节点会判定自己是否具有参选资格,如果从节点和主节点已很久没有通信,即很久没有同步过数据,主从节点之间差异较大,时间超过阈值,就失去竞选资格
-
具有资格的节点,比如C和D,就会先休眠一段时间,休眠时间=500ms基础时间+[0,500ms]随机时间 + 排名 * 1000ms。offset值越大,排名越靠前(越小)
-
假定C的休眠时间到了,C就会给集群中其他节点,进行拉票操作,但是只有主节点才有投票资格
-
主节点就会把自己的票投给C(每个主节点只有一票),当C收到的票数超过主节点数目的一半,C就会晋升为主节点。
C自己负责执行
slaveof no one
,并且让D执行slaveof C
- 同时,C还会将自己成为主节点的信息,同步给集群的其他节点,大家也会更新自己保存的集群结构信息。
REDIS哨兵模式,是先投票竞选出一个
leader
,让leader
负责找一个从节点升级为主节点。而集群模式里是直接投票选出新的主节点
出现集群宕机
- 情况1:某个分片,所有的主节点和从节点都挂了
- 情况2:某个分片,主节点挂了,但是没有从节点
- 情况3:超过半数的master节点都挂了
集群扩容
- 现在搭建的集群的主机号101-109,9个主机,构成了3主6从结构的集群。现在将主机号为110和111也加入集群中,以110为master,111为slave,数据分片从3->4。
添加主节点
- 将新的主节点110加入到集群中
redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379
172.30.0.110:6379
:新增节点的地址172.30.0.101:6379
:集群上任意一个节点的地址都可以,表示要把这个新节点添加到这个集群上
重新分配slots
redis-cli --cluster reshard 172.30.0.101:6379
172.30.0.101:6379
:集群中任意节点的地址都可以,表示这个集群
此处是询问用户要移动多少的slots给新增的主节点,我们这里分成4片,即4096
这是问你要把这些哈希槽分配给谁,输入他的id ↑
- 有两种分配方式让你选择
- all : 表示从其他每个持有slots的master节点都拿一些过来
- 手动指定 : 从某一个/几个节点来移动slots,输入完他们的id,以done结尾
当输入yes才是真正开始搬运
从节点添加到集群中
- 将主机号111添加到集群中,作为主机号110的从节点
命令格式:
redis-cli --cluster add-node new_host:new_port existing_host:existing_port --cluster-slave --cluster-master-id <arg>
# redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster-slave --cluster-master-id [172.30.1.110节点的nodeid]
FAQ
问题:在搬运slots/key的过程中,此时客户端端能否访问到redis集群?
- 搬运key, 大部分key是不用搬运的,针对这些未搬运的key,是可以正常访问的;但针对这些正在搬运的key,是有可能出现访问出错的情况.
例如,客户端访问key1,集群通过分片算法,得到key1是第一个分片的数据,就会重定向到第一个分片的节点,就可能在重定向过去之后,正好key1被搬走,自然就无法访问.
Jedis 客戶端的支持: JedisCluster
FAQ
Q: Jedis客户端是非线程安全的,为什么?需要注意什么?
- 原理解析:
Jedis的请求流和响应流都是一个全局变量
如果同一个jedis
连接同时被多个线程使用的话,比如A线程执行了jedis.get(“a”)
, B线程执行了jedis.get(“b”)
,那么完全有可能出现get(“a”)
的指令拿到b结果的情况,会出现数据错乱。
其实知道了有个全局变量之后,相信线程不安全的原因就很好理解了。
- 示例
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost");
int inti = 0;
new Thread(()->{
for (int j = 0;j<10;j++){
jedis.set("a" + inti,String.valueOf(inti));
System.out.println("a" + inti +" is:" + jedis.get("a" + inti));
}
}).start();
int intij = 1;
new Thread(()->{
for (int j = 0;j<10;j++){
jedis.set("a" + intij,String.valueOf(intij));
System.out.println("a" + intij +" is:" + jedis.get("a" + intij));
}
}).start();
}
输出结果:
预期结果:
应该是“a0 is 0”或“a1 is 1” , 但是出现了“a0 is OK” / “a0 is 1” 的情况
这显然就是一个set指令的内容也被作为了get的指令了,a0 is 1也同理,B线程的结果反而被A线程收到了
- 怎么避免这个问题?
当然是一个线程用一个jedis,同一个jedis实例同一时刻只会被一个客户端线程使用即可。
考虑到反复创建jedis是一个耗时操作,所以建议是:使用池化技术,比如jedispool
,这也是在哨兵模式下最常用的一个池化技术。
但是,如果是
jedis cluster
的话,单一的一个jedis pool
也就不够用了。
Q: JedisCluster的初始化过程,和执行JedisCluster.get
等指令经过了哪些流程?
- 上一部分讲解了,在哨兵模式下,使用
jedis pool
来解决jedis多线程下线程不安全的问题
我们知道在哨兵模式下,其实只有一个主节点的,如果没有额外程序控制读写分离的话,其实从节点只是作为备份(会有主从复制和故障转移),而不会被真正业务使用到的。
这也说明一个问题:其实客户端只是跟一个实例节点在交互而已,这时使用一个jedispool
,然后jedispool中的所有jedis对象都指向同一个主节点实例的ip和port,当然没有问题。
但是在cluster集群模式下,情况就不一样了,因为此时是有多个主节点了,每个主节点还占据了一部分的槽位,那么:也就意味着客户端在和redis
交互的时候,是需要和多个主节点交互的
比如get(“a”)这个指令,可能是到了ip1:port1
这个主节点,get(“b”)这个指令,是需要到ip2:port2
这个主节点上执行的(要知道,最终都会归于jedis这个客户端)
此时也就带来一个问题: 客户端需要通过key值,来确认到底是要用哪个ip:port的jedis客户端
也就是说:jediscluster
的客户端至少需要维护一个主节点和jedispool的一个map:
- 我们初始化一个
JedisCLuster
往往通过如下步骤:
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(30);// 最大连接数
config.setMaxIdle(2);// 最大连接空闲数
Set<HostAndPort> jedisClusterNodes = new HashSet<HostAndPort>();
jedisClusterNodes.add(new HostAndPort("192.168.101.101", 6379));
jedisClusterNodes.add(new HostAndPort("192.168.101.102", 6379));
jedisClusterNodes.add(new HostAndPort("192.168.101.103", 6379));
jedisClusterNodes.add(new HostAndPort("192.168.101.104", 6379));
jedisClusterNodes.add(new HostAndPort("192.168.101.105", 6379));
jedisClusterNodes.add(new HostAndPort("192.168.101.106", 6379));
//JedisCluster jc = new JedisCluster(jedisClusterNodes);
//JedisCluster jc = new JedisCluster(jedisClusterNodes, config);
JedisCluster jc = new JedisCluster(jedisClusterNode, DEFAULT_TIMEOUT, DEFAULT_TIMEOUT, DEFAULT_REDIRECTIONS, "cluster", DEFAULT_CONFIG);
jc.set("foo", "bar");
assertEquals("bar", jc.get("foo"));
翻看源码,逐步跟进去构造方法,你可以发现这么一个时序图。这个就是jediscluster的真正的初始化过程:
这里面,关键点在于:在初始化时,虽然只传递了一个主节点的信息(我们知道:redis cluster是去中心化的,传递一个节点就足够了)
但客户端通过initializeSlotCache
方法会和redis集群做交互(具体可以看initializeSlotCache方法的执行步骤),通过一个command,拿到所有的主节点相关信息,及每个主节点分别含有哪些槽位的信息
从而可构造出上述我们说的:Map<String,JedisPool> nodes
、Map<Integer,Jedispool> slots
这两个map,从而为后续客户端的指令执行打下基础
关键点在于:
- redis客户端通过
crc16(key)%16384
找到对应的槽位后,通过getConnectionFromSlot
方法,可以拿到对应的jedispool- 然后执行
execute
方法(其实就是jedis get set方法)- 如果执行失败,大概率可能是出现了槽位的重新分配,那么:此时需要更新替换操作,renewSlotCache之后再执行客户端指令。
Q: 为什么cluster模式下,客户端无法支持pipline和mget等指令?但是某些场景下mget又是可以执行成功?
- 问题1:为什么redis集群模式不支持
pipline
?
我们知道,
pipline
主要是为了解决多次网络IO的问题,将一系列指令发送到一个服务节点进行执行:
Jedis jedis = new Jedis(String, int);
Pipeline p = jedis.pipelined(); //pipline本质上是单个jedis的行为,所以只会有一个目标ip:port
p.set(key,value); //每个操作 都发送请求给redis-server
p.get(key,value);
p.sync(); // 这段代码获取所有的response
但通过上面讲解,我们也知道:
- 在
redis cluster
模式下,会有很多个实例节点,- 而
pipline
的一系列指令中,必然包含了一系列的key值- 这些
key
通过crc16(key)%16384
算的的槽位完全可能不在同一个节点上所以,
pipline
指令在redis cluster
模式下,天然不支持
当然,可以通过一些改造的方式实现,比如Lettuce
框架,但是至少从原理上来说的确pipline
就是不是特别适合在redis
集群模式下使用的
- 问题2:为什么redis集群模式,有时候又可以执行
mget
,有时候不行?
- 不行的原因,其实和pipline是类似的,无非就是多个key,对应的槽位是不在同一个实例节点上的
为什么有时候又可以执行mget这种批量指令?????
- 原因就是
hash_tag
,只要你的key中包含了{ }
这个标识符,那么在计算crc16的时候,就只会拿{}
里面的内容进行计算,那么只要你保持{}
中的字符串是一样的,那么这些key就一定会落在同一个实例节点上,那么执行mget指令理所当然就没有问题了, 实践如下:
这里
{%s}
其实就是代表的一个用户id(如:openid
)。
那么通过这种hash_tag
,你就可以将一个用户的各类特征都存储在同一个redis实例中,在一些业务场景中,可能每个请求都需要拿到当前用户对应的各类特征,而这些特征存在于不同的key中,如果不用hash tag
,那么必然意味着需要多次IO,分别去获取数据.
但是使用了hash tag
之后,不仅key变得比较有规则,而且还可以使用mget
批量操作指令,高效获取批量特征,从而降低系统的整体延迟,提高cpu利用率
X 参考文献
![QQ沟通交流群](https://blog-static.cnblogs.com/files/johnnyzen/cnblogs-qq-group-qrcode.gif?t=1679679148)
本文链接: https://www.cnblogs.com/johnnyzen/p/18691569
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 本地部署DeepSeek后,没有好看的交互界面怎么行!
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 趁着过年的时候手搓了一个低代码框架
· 推荐一个DeepSeek 大模型的免费 API 项目!兼容OpenAI接口!
· 用 C# 插值字符串处理器写一个 sscanf