Redis分布式解决方案-RedisCluster
一、Redis-cluster理论基础
1.1、为什么要实现redis-cluster
- 数据量考虑:主从复制集群、哨兵模式集群本质上还是redis单点存储所有数据,随着业务发展,数据量越来越大,单机redis存储会出现瓶颈,此时需要考虑分布式需求,把数据分布到不同的机器上.
- 网络流量:业务的流量已经超过服务器的网卡的上限值,可以考虑使用分布式来进行分流
1.2、海量数据分布式解决方案
1.2.1、数据分区
全量数据根据某种规则,分不到不同的子集.
常用的分区规则:
- 顺序分区
- 哈希分区
- 节点取余
- 一致性哈希
- 虚拟槽分区
1.2.2、顺序分区
1.2.3、哈希分区-节点取余
原理
选择哈希函数,对key进行哈希运算,用哈希值对节点数进行取余hashFunction(key)/节点数量,得到的结果就是数据要存储的节点.
存在问题:
节点取余分区方式有一个问题:即当增加或减少节点时,原来节点中的80%的数据会进行迁移操作,对所有数据重新进行分布
节点取余分区方式建议使用多倍扩容的方式,例如以前用3个节点保存数据,扩容为比以前多一倍的节点即6个节点来保存数据,这样只需要适移50%的数据。数据迁移之后,第一次无法从缓存中读取数据,必须先从数据库中读取数据,然后回写到缓存中,然后才能从缓存中读取迁移之后的数据
1.2.4、一致性哈希
一致性哈希原理:
将所有的数据当做一个token环,token环中的数据范围是0到2的32次方。然后为每一个数据节点分配一个token范围值,这个节点就负责保存这个范围内的数据。
对每一个key进行hash运算,被哈希后的结果在哪个token的范围内,则按顺时针去找最近的节点,这个key将会被保存在这个节点上。
一致性哈希插入元素:
一致性哈希扩容:
在上面的图中,有4个key被hash之后的值在在n1节点和n2节点之间,按照顺时针规则,这4个key都会被保存在n2节点上,
如果在n1节点和n2节点之间添加n5节点,当下次有key被hash之后的值在n1节点和n5节点之间,这些key就会被保存在n5节点上面了
在上面的例子里,添加n5节点之后,数据迁移会在n1节点和n2节点之间进行,n3节点和n4节点不受影响,数据迁移范围被缩小很多
同理,如果有1000个节点,此时添加一个节点,受影响的节点范围最多只有千分之2
一致性哈希一般用在节点比较多的时候
优点:
采用客户端分片方式:哈希 + 顺时针(优化取余)
节点伸缩时,只影响邻近节点,但是还是有数据迁移
缺点:
缓存热点问题:可能集中在某一个hash区间的值特别多
任何一个master宕机,都会有1/n的数据溶入数据库
虚拟节点改进:
假设共有n个节点,则可以给每个节点增加m个副本,让这m个副本均匀的分布到任意两个真实节点之间.
增加数据的均衡性,避免单节点数据过多.
1.2.5、虚拟槽分区
虚拟槽分区是Redis Cluster采用的分区方式
预设虚拟槽,每个槽就相当于一个数字,有一定范围。每个槽映射一个数据子集,一般比节点数大
Redis Cluster中预设虚拟槽的范围为0到16383(2^14)
二、Redis-cluster 原理
2.1、Redis-cluster 原理
redis-cluster简图
redis-cluster是什么
- 多个主从架构组合在一起对外提供服务(redis-cluster至少需要三个)
- 每个主从架构存储一部分数据
- 默认情况下:master处理所有的写请求和读请求,slave只做备份
redis-cluster是如何分配数据和查找数据的?
- redis-cluster分配16384个虚拟槽位slot
- redis-cluster的每个master会对应一组槽位区间
- redis-cluster的每个master会记录一份slot-Redis节点的对应关系
获取数据时:
- crc16(key) % 16384 = 槽位
- 查询当前槽位所属redis节点
- 如果是当前节点,则处理请求,否则返回MOVED 信息
- 如果当前节点正在进行槽迁移,返回ask信息
2.2、Redis-cluster集群配置
2.2.1、配置文件参数介绍
开启集群功能
cluster-enabled yes
集群配置文件
虽然是集群配置文件,但是不能人工编辑,主要用于记录集群中有哪些节点、状态、持久化参数等.
cluster-config-file nodes-6379.conf
节点超时时间
这是集群中的节点能够失联的最大时间,超过这个时间,该节点就会被认为故障。如果主节点超过这个时间还是不可达,则用它的从节点将启动故障迁移,升级成主节点。注意,任何一个节点在这个时间之内如果还是没有连上大部分的主节点,则此节点将停止接收任何请求。一般设置为15秒即可。
cluster-node-timeout 15000
从节点有效性参数
如果设置成0,则无论从节点与主节点失联多久,从节点都会尝试升级成主节点。如果设置成正数,则cluster-node-timeout乘以cluster-slave-validity-factor得到的时间,是从节点与主节点失联后,此从节点数据有效的最长时间,超过这个时间,从节点不会启动故障迁移。假设cluster-node-timeout=5,cluster-slave-validity-factor=10,则如果从节点跟主节点失联超过50秒,此从节点不能成为主节点。注意,如果此参数配置为非0,将可能出现由于某主节点失联却没有从节点能顶上的情况,从而导致集群不能正常工作,在这种情况下,只有等到原来的主节点重新回归到集群,集群才恢复运作。
cluster-replica-validity-factor 10
迁移所需最小从节点数量
主节点需要的最小从节点数,只有达到这个数,主节点失败时,它从节点才会进行迁移。
cluster-migration-barrier 1
节点故障时,cluster是否提供服务
在部分key所在的节点不可用时,如果此参数设置为"yes"(默认值), 则整个集群停止接受操作;如果此参数设置为”no”,则集群依然为可达节点上的key提供读操作。
cluster-require-full-coverage yes
master是否进行故障转移
当设置为“是”时,此选项可防止复制副本在主服务器故障期间尝试故障转移其主服务器。但是,如果强制执行手动故障转移,主服务器仍然可以执行手动故障转移。
这在不同的情况下非常有用,特别是在多个数据中心操作的情况下,如果在整个DC故障的情况下不希望一方被提升,我们希望它永远不会被提升
cluster-replica-no-failover no
节点故障时,是否可读
默认是no,当集群认为有节点出现故障时,该故障可能是因为网络分区导致该节点与集群隔离开,或者该节点无法得到集群大多数节点的认同时,此时该故障节点将会拒绝任何读写请求,这样的好处是将阻止客户端从该节点读取到不一致的数据。该参数也可以被配置为yes,即使该节点故障也允许读操作,这在应用要求优先进行读取操作但是可以阻止不一致性写入的情况下比较有用,当Redis集群仅有少量切片情况下且没有从节点执行故障转移时可以考虑使用。
cluster-allow-reads-when-down no
2.3、集群key路由原理
2.3.1、Moved重定向
Redis 客户端可以自由地向集群中的每个节点发送查询,包括从节点。节点会分析查询,如果可以接受(即查询中只提到了一个key,或者提到的多个key都在同一个hash slot),它会查找哪个节点负责hash slot钥匙或钥匙所属的地方。
如果散列槽由节点提供服务,则查询将被简单处理,否则节点将检查其内部散列槽到节点映射,并以 MOVED 错误回复客户端
MOVED信息格式
127.0.0.1:8006> set name jason
(error) MOVED 5798 127.0.0.1:8002
(error) MOVED 槽位 所在节点ip:端口
客户端在收到MOVED错误信息后,应该更新本地slot-redis节点信息.
- 只更新当前返回的slot-redis节点信息
- 通过cluster nodes 或 cluster slots 更新全部的slot-redis节点信息
cluster slots返回信息:
127.0.0.1:8006> cluster slots
1) 1) (integer) 5461 槽位开始
2) (integer) 6460 槽位结束
3) 1) "127.0.0.1" 节点IP
2) (integer) 8002 节点端口
3) "118154eb6a209f78284711f82fa07944fa30f752" 节点运行ID
4) 1) "127.0.0.1"
2) (integer) 8004
3) "0d1f8d829704301976d4fb3d14add4b3f80d27ad"
2) 1) (integer) 11922
2) (integer) 16383
3) 1) "127.0.0.1"
2) (integer) 8002
3) "118154eb6a209f78284711f82fa07944fa30f752"
4) 1) "127.0.0.1"
2) (integer) 8004
3) "0d1f8d829704301976d4fb3d14add4b3f80d27ad"
3) 1) (integer) 6462
2) (integer) 10922
3) 1) "127.0.0.1"
2) (integer) 8001
3) "ce09af98e66c6c134e4e7ad7604f8b3629eeb1a2"
4) 1) "127.0.0.1"
2) (integer) 8003
3) "e077e8263dafaf5d4b12f6a9d090223c51c80f9c"
4) 1) (integer) 6461
2) (integer) 6461
3) 1) "127.0.0.1"
2) (integer) 8006
3) "0e665b951a51302c65525e2fb7d7697de9ebd71f"
4) 1) "127.0.0.1"
2) (integer) 8007
3) "ffe530d79592905c46e72be346b35d04e49d2dd1"
5) 1) (integer) 10923
2) (integer) 11921
3) 1) "127.0.0.1"
2) (integer) 8006
3) "0e665b951a51302c65525e2fb7d7697de9ebd71f"
4) 1) "127.0.0.1"
2) (integer) 8007
3) "ffe530d79592905c46e72be346b35d04e49d2dd1"
6) 1) (integer) 0
2) (integer) 5460
3) 1) "127.0.0.1"
2) (integer) 8000
3) "0fecb5733920721da1688256fcd265fc9e60fa8a"
4) 1) "127.0.0.1"
2) (integer) 8005
3) "12d2dd1a64fb57f7dfb0338589349e6753dc74cb"
cluster nodes
0d1f8d829704301976d4fb3d14add4b3f80d27ad 127.0.0.1:8004@18004 slave 118154eb6a209f78284711f82fa07944fa30f752 0 1623947145000 8 connected
12d2dd1a64fb57f7dfb0338589349e6753dc74cb 127.0.0.1:8005@18005 slave 0fecb5733920721da1688256fcd265fc9e60fa8a 0 1623947143000 1 connected
e077e8263dafaf5d4b12f6a9d090223c51c80f9c 127.0.0.1:8003@18003 slave ce09af98e66c6c134e4e7ad7604f8b3629eeb1a2 0 1623947142000 2 connected
ffe530d79592905c46e72be346b35d04e49d2dd1 127.0.0.1:8007@18007 slave 0e665b951a51302c65525e2fb7d7697de9ebd71f 0 1623947145294 7 connected
118154eb6a209f78284711f82fa07944fa30f752 127.0.0.1:8002@18002 master - 0 1623947144262 8 connected 5461-6460 11922-16383
ce09af98e66c6c134e4e7ad7604f8b3629eeb1a2 127.0.0.1:8001@18001 master - 0 1623947145000 2 connected 6462-10922
0e665b951a51302c65525e2fb7d7697de9ebd71f 127.0.0.1:8006@18006 myself,master - 0 1623947144000 7 connected 6461 10923-11921
0fecb5733920721da1688256fcd265fc9e60fa8a 127.0.0.1:8000@18000 master - 0 1623947146311 1 connected 0-5460
2.3.2、Ask重定向
为什么我们不能简单地使用 MOVED 重定向?因为 MOVED 意味着我们认为哈希槽永久由不同的节点提供服务,并且应该针对指定节点尝试下一个查询,而 ASK 意味着只向指定节点发送下一个查询。
这是必需的,因为关于哈希槽 8 的下一个查询可能是关于仍在 A 中的键,所以我们总是希望客户端在需要时先尝试 A,然后再尝试 B。由于这种情况仅发生在 16384 个可用哈希槽中的一个哈希槽中,因此对集群的性能影响是可以接受的。
我们需要强制客户端行为,因此为了确保客户端只会在尝试 A 后尝试节点 B,如果客户端在发送查询之前发送 ASKING 命令,节点 B 将只接受设置为 IMPORTING 的插槽的查询。
基本上,ASKING 命令在客户端上设置一个一次性标志,强制节点为有关 IMPORTING 插槽的查询提供服务。
从客户端的角度来看,ASK 重定向的完整语义如下:
如果收到 ASK 重定向,则只发送被重定向到指定节点的查询,但继续向旧节点发送后续查询。
使用 ASKING 命令启动重定向查询。
不要更新本地客户端表以将哈希槽 8 映射到 B。
一旦哈希槽 8 迁移完成,A 将发送一条 MOVED 消息,客户端可以将哈希槽 8 永久映射到新的 IP 和端口对。请注意,如果有问题的客户端更早地执行映射,这不是问题,因为它不会在发出查询之前发送 ASKING 命令,因此 B 将使用 MOVED 重定向错误将客户端重定向到 A。
ASK指令只是代表一个临时过程,等迁移结束,发送MOVED指令,客户端更新slot-redis节点对应关系
2.3.3、hash Tag
键哈希标签:只关心键的部分数据,根据这部分数据进行哈希运算.这样可以使得多个包含该部分的key落到同一个槽.
为了实现散列标签,键的散列槽在某些条件下以稍微不同的方式计算。如果密钥包含一个“{...}”图案仅之间子 {和},以获得散列时隙被散列。但是,由于可能多次出现{或者}算法由以下规则很好地指定:
如果键包含一个{字符。
如果}右边有一个字符{
并且如果在第一次出现{和第一次出现之间有一个或多个字符}。
然后,而不是散列密钥,只散列第一次出现{和接下来的第一次出现之间的}内容。
例子:
这两个键{user1000}.following和{user1000}.followers由于只有子意愿散列到相同的散列时隙user1000将为了计算散列时隙被散列。
对于键foo{}{bar},整个键将像往常一样散列,因为第一次出现的{后面跟}在右边,中间没有字符。
对于键foo{{bar}}zap,子字符串{bar将被散列,因为它是第一次出现{和第一次出现}在其右侧之间的子字符串。
对于密钥foo{bar}{zap}的子串bar将被散列,由于算法停止在所述第一有效或无效(无字节内)匹配{和}。
该算法得出的结论是,如果密钥以 开头{},则保证它作为一个整体进行散列。这在使用二进制数据作为键名时很有用。
2.3.4、hash slot的信息传播
两种传播方式:
1、心跳消息.ping或pong数据包的发送方总是添加有关它服务和散列槽集的信息
2、update消息.由于心跳包中都有关于发送者configEpoch和所有服务的hash slot信息.如果心跳包的接受者发现发送者的信息都是陈旧的,它将发送一个带有新的信息的包,强制发送者节点更新信息
2.4、Jedis客户端
2.4.1、JedisCluster
每个JedisPool中缓存了slot和节点node的关系
key和slot的关系:对key进行CRC16规则进行hash后与16383取余得到的结果就是槽
JedisCluster启动时,已经知道key,slot和node之间的关系,可以找到目标节点
JedisCluster对目标节点发送命令,目标节点直接响应给JedisCluster
如果JedisCluster与目标节点连接出错,则JedisCluster会知道连接的节点是一个错误的节点
此时JedisCluster会随机节点发送命令,随机节点返回moved异常给JedisCluster
JedisCluster会重新初始化slot与node节点的缓存关系,然后向新的目标节点发送命令,目标命令执行命令并向JedisCluster响应
如果命令发送次数超过5次,则抛出异常"Too many cluster redirection!"
2.4.2、多节点命令实现
Redis Cluster不支持使用scan命令扫描所有节点
多节点命令就是在在所有节点上都执行一条命令
批量操作优化
串行mget
定义for循环,遍历所有的key,分别去所有的Redis节点中获取值并进行汇总,简单,但是效率不高,需要n次网络时间
串行IO
对串行mget进行优化,在客户端本地做内聚,对每个key进行CRC16hash,然后与16383取余,就可以知道哪个key对应的是哪个槽
本地已经缓存了槽与节点的对应关系,然后对key按节点进行分组,成立子集,然后使用pipeline把命令发送到对应的node,需要nodes次网络时间,大大减少了网络时间开销
并行IO
并行IO是对串行IO的一个优化,把key分组之后,根据节点数量启动对应的线程数,根据多线程模式并行向node节点请求数据,只需要1次网络时间
hash_tag
将key进行hash_tag的包装,然后把tag用大括号括起来,保证所有的key只向一个node请求数据,这样执行类似mget命令只需要去一个节点获取数据即可,效率更高
四种优化方案优缺点分析
2.5、Redis-cluster高可用
2.5.1、故障发现
Redis Cluster通过ping/pong消息实现故障发现:不需要sentinel
ping/pong不仅能传递节点与槽的对应消息,也能传递其他状态,比如:节点主从状态,节点故障等
故障发现就是通过这种模式来实现,分为主观下线和客观下线
主观下线
1.节点1定期发送ping消息给节点2
2.如果发送成功,代表节点2正常运行,节点2会响应PONG消息给节点1,节点1更新与节点2的最后通信时间
3.如果发送失败,则节点1与节点2之间的通信异常判断连接,在下一个定时任务周期时,仍然会与节点2发送ping消息
4.如果节点1发现与节点2最后通信时间超过node-timeout,则把节点2标识为pfail状态
客观下线
当半数以上持有槽的主节点都标记某节点主观下线时,可以保证判断的公平性
集群模式下,只有主节点(master)才有读写权限和集群槽的维护权限,从节点(slave)只有复制的权限
客观下线流程:
1.某个节点接收到其他节点发送的ping消息,如果接收到的ping消息中包含了其他pfail节点,这个节点会将主观下线的消息内容添加到自身的故障列表中,故障列表中包含了当前节点接收到的每一个节点对其他节点的状态信息
2.当前节点把主观下线的消息内容添加到自身的故障列表之后,会尝试对故障节点进行客观下线操作
故障列表的周期为:集群的node-timeout * 2,保证以前的故障消息不会对周期内的故障消息造成影响,保证客观下线的公平性和有效性
2.5.2、故障修复
故障修复有从节点 发起.
资格检查
每个从节点都要检查最后和主节点断线时间,判断是否有资格替换故障的主节点.
cluster-slave-validity-factor:
0: 断开时间 > cluster-node-timeout * cluster-slave-validity-factor,则没有资格
=0: 所有从节点都有资格
计算时间延迟
一旦 master 处于FAIL状态,slave 会在尝试被选举之前等待一小段时间。该延迟计算如下:
DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds +
SLAVE_RANK * 1000 milliseconds.
SLAVE_RANK :根据复制偏移量排列,偏移量越大,等级越高,数值越小,延迟越小
发起投票请求
从节点定时任务检测达到故障选举时间到达后,发起选举流程.
在集群内广播选举消息(FAILOVER_AUTH_REQUEST),并记录已经发送过消息的状态,保证从节点在一个配置纪元内只能发起一次选举.
选举投票
只有持有槽的主节点才会处理故障选举消息(FAILOVER_AUTH_REQUEST),因为每个持有槽的节点在一个配置纪元内都有唯一的一张选票,当接到第一个请求投票的从节点消息时回复FAILOVER_AUTH_ACK消息作为投票,之后相同配置纪元内其他从节点的选举消息将忽略。
如集群内有N个持有槽的主节点代表有N张选票。由于在每个配置纪元内持有槽的主节点只能投票给一个从节点,因此只能有一个从节点获得N/2+1的选票,保证能够找出唯一的从节点。
投票作废:每个配置纪元代表了一次选举周期,如果在开始投票之后的cluster-node-timeout * 2 时间内从节点没有获取足够数量的投票,则本次选举作废。从节点对配置纪元自增并发起下一轮投票,直到选举成功为止。
2.5.3、集群扩容、缩容
待补充
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 本地部署 DeepSeek:小白也能轻松搞定!
· 如何给本地部署的DeepSeek投喂数据,让他更懂你
· 在缓慢中沉淀,在挑战中重生!2024个人总结!
· 大人,时代变了! 赶快把自有业务的本地AI“模型”训练起来!
· 从 Windows Forms 到微服务的经验教训