[Redis] Redis (4) Redis Cluster 多分片集群架构

目录

  • 近来几个月,一直被多套生产环境的各类性能问题所困,持续优化、持续改进。
  • 这不,有一套环境的 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.ymlgenerate.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 节点自身的ip
  • cluster-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> nodesMap<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 参考文献

posted @   千千寰宇  阅读(64)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 本地部署DeepSeek后,没有好看的交互界面怎么行!
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 趁着过年的时候手搓了一个低代码框架
· 推荐一个DeepSeek 大模型的免费 API 项目!兼容OpenAI接口!
· 用 C# 插值字符串处理器写一个 sscanf
点击右上角即可分享
微信分享提示