基础补充文档:
Redis集群模式介绍:
https://www.cnblogs.com/zhonglongbo/p/13128955.html
Redis主流集群模式:
主从模式、哨兵模式、集群模式
遇到的问题:
使用Rediscluster模式集群,出现单点热点key,既集群中N个数据节点中,单一节点承接了这个场景下的所有流量;导致单节点cpu飙升到60%,其余节点cpu10%;
这个场景是geohash运算,属于读流量中的cpu密集型操作;
为了分散读流量到各个节点,我们选择了,基于当前key数据生成N-1个副本的策略;
Rediscluster 集群模式的工作原理:
分片策略:
Redis 集群将数据划分为16384(2的14次方)个哈希槽(slots),每个节点负责其中一部分槽位。当客户端发起请求时,根据键(key)的CRC16值进行哈希计算,然后对16384取模,得到对应的槽位索引,从而确定请求应该发送到哪个节点。
以下图为例,该集群有4个 Redis 节点,每个节点负责集群中的一部分数据,数据量可以不均匀。比如性能好的实例节点可以多分担一些压力。
哈希槽(slots)的划分
这个前面已经说过了,我们会将整个Redis数据库划分为16384个哈希槽,你的Redis集群可能有n个实例节点,每个节点可以处理0个 到至多 16384 个槽点,这些节点把 16384个槽位瓜分完成。
而你实际存储的Redis键值信息也必然归属于这 16384 个槽的其中一个。slots 与 Redis Key 的映射是通过以下两个步骤完成的:
使用 CRC16 算法计算键值对信息的Key,会得出一个 16 bit 的值。
将 第1步中得到的 16 bit 的值对 16384 取模,得到的值会在 0 ~ 16383 之间,映射到对应到哈希槽中。
当然,可能在一些特殊的情况下,你想把某些key固定到某个slot上面,也就是同一个实例节点上。这时候可以用hash tag能力,强制 key 所归属的槽位等于 tag 所在的槽位。
其实现方式为在key中加个{},例如test_key{1}。使用hash tag后客户端在计算key的crc16时,只计算{}中数据。如果没使用hash tag,客户端会对整个key进行crc16计算。下面演示下hash tag使用:
127.0.0.1:6380> cluster keyslot user:case{1}
(integer) 1024
127.0.0.1:6380> cluster keyslot user:favor
(integer) 1023
127.0.0.1:6380> cluster keyslot user:info{1}
(integer) 1024
如上,使用hash tag 后会对应到通一个hash slot:1024中。
哈希槽(slots)的映射
一种是初始化的时候均匀分配 ,使用 cluster create 创建,会将 16384 个slots 平均分配在我们的集群实例上,比如你有n个节点,那每个节点的槽位就是 16384 / n 个了 。
另一种是通过 CLUSTER MEET 命令将 node1、node2、ndoe3、node4 4个节点联通成一个集群,刚联通的时候因为还没分配哈希槽,还是处于offline状态。我们使用 cluster addslots 命令来指定。
指定的好处就是性能好的实例节点可以多分担一些压力。
可以通过 addslots 命令指定哈希槽范围,比如下图中,我们哈希槽是这么分配的:实例 1 管理 0 ~ 7120 哈希槽,实例 2 管理 7121~9945 哈希槽,实例 3 管理 9946 ~ 13005 哈希槽,实例 4 管理 13006 ~ 16383 哈希槽。
redis-cli -h 192.168.0.1 –p 6379 cluster addslots 0,7120
redis-cli -h 192.168.0.2 –p 6379 cluster addslots 7121,9945
redis-cli -h 192.168.0.3 –p 6379 cluster addslots 9946,13005
redis-cli -h 192.168.0.4 –p 6379 cluster addslots 13006,16383
slots 和 Redis 实例之间的映射关系如下:
key testkey_1 和 testkey_2 经过 CRC16 计算后再对slots的总个数 16384 取模,结果分别匹配到了 cache1 和 cache3 上。
补充知识点:为什么选择0-16383即16384个槽位?
计算公式 HASH_SLOT = RCR16(key) mod 16384
如果槽位为65536(2^16),发送心跳信息的消息头达8k,发送的心跳包过于庞大。
在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为65536时,这块的大小是: 65536÷8÷1024=8kb
在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为16384时,这块的大小是: 16384÷8÷1024=2kb
因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。
redis的集群主节点数量基本不可能超过1000个。
集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者不建议redis cluster节点数量超过1000个。 那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。
槽位越小,节点少的情况下,压缩比高,容易传输
Redis主节点的配置信息中它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。 如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。
通过java代码计算槽位与节点的数据:
// 设定一个容器对象
HashMap<@Nullable Object, @Nullable Object> objectObjectHashMap = Maps.newHashMap();
// 初始化设定有32个节点的RedisCluster集群
int n = 32;
for (int i = 0; i < 200; i++) {
// 利用JedisClusterCRC16 计算 key的hashtag:"123:{1}" 属于哪一个slot
int slot = JedisClusterCRC16.getSlot("123:{" + i + "}");
for (int j = 0; j < n; j++) {
// 计算当前slot 属于哪一个 node;
if (slot > j * (16383 / n) && slot < (j + 1) * (16383 / n)) {
// 以下计算,只记录每个节点从小到大第一个明确的hashtag分配的数据
if (Objects.isNull(objectObjectHashMap.get(j + 1))) {
objectObjectHashMap.put(j + 1, i + "---" + slot);
if (objectObjectHashMap.size() >= n) {
outer:
break;
}
}
}
}
}
// 随机获取当前 key hashtag对应的 node和slot
// 下面的代码也可用于 随机分发查询流量,用于组装一个key
if(!objectObjectHashMap.isEmpty()) {
for (int j = 0; j < 100; j++) {
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
int i = threadLocalRandom.nextInt(n);
System.out.println(i+1);
System.out.println(objectObjectHashMap.get(++i));
}
}
补充知识点:hashtag
在Redis中,hashtag(哈希标签)是一个用于数据分片的机制,它允许将多个键映射到同一个哈希槽中。这是通过在键的名称中包含一对大括号({})来实现的,大括号内可以包含任意字符串。Redis集群使用CRC16算法对键进行哈希,然后对16384取模,以此确定键应该存储在哪个槽位上。
使用hashtag的目的是为了在集群环境中能够对一组相关的键执行原子操作。例如,如果你有一个用户信息存储在Redis中,可能包含用户的姓名、邮箱、年龄等多个字段,你可以使用hashtag将这些字段组织在一起,如下所示:
user{123}:username
user{123}:email
user{123}:age
在这个例子中,user{123}是哈希标签,它确保所有以user{123}:开头的键都会被分配到同一个哈希槽中。这意味着,即使用户的数据分布在不同的Redis节点上,你也可以通过一个命令来同时操作这些键,例如使用DEL命令删除一个用户的所有信息:
DEL user{123}:username user{123}:email user{123}:age
这种方式在Redis集群中特别有用,因为它允许跨多个节点执行命令,而不需要客户端知道每个键具体存储在哪个节点上。Redis集群会自动将命令路由到正确的节点,并执行相应的操作。
需要注意的是,hashtag只在Redis集群模式下有效,它不会影响非集群模式下的键。此外,hashtag的使用也需要谨慎,因为它可能会导致数据倾斜,如果不正确地使用,可能会将大量的键映射到同一个槽位,从而影响集群的负载均衡。
Redis源码关于hashtag计算:
源码:
源码有2处。
第一处:
https://github.com/redis/redis/blob/6.2.6/src/redis-cli.c
line:3282
方法:clusterManagetKeyHashSlot
第二处:
https://github.com/redis/redis/blob/6.2.6/src/cluster.c
line:749
方法:keyHashSlot
// 源码位置
// https://github.com/redis/redis/blob/6.2.6/src/cluster.c
unsigned int keyHashSlot(char *key, int keylen) {
// s代表{在key中的位置,e代表}在key中的位置
int s, e;
// 若无{,则s等于keylen
for (s = 0; s < keylen; s++)
// 遇到第一个{跳出
if (key[s] == '{') break;
// 若key中无{,则s等于keylen,整个key参与hash
// 0x3FFF对应10进制为16383
// 16383对应二进制为14个1
// 按位与运算时只取crc16结果的低14位
if (s == keylen) return crc16(key,keylen) & 0x3FFF;
// 若key中有{,查看是否有}
// 若key中无},则e等于keylen,整个key参与hash
for (e = s+1; e < keylen; e++)
// 遇到第一个}跳出
if (key[e] == '}') break;
// key中无},整个key参与hash
// key中有},但{}之间为空,整个key参与hash
if (e == keylen || e == s+1) return crc16(key,keylen) & 0x3FFF;
// {}中间部分参与hash
// key+s+1 指针操作,向右移动s+1
// e-s-1为{}中间字符串的长度
return crc16(key+s+1,e-s-1) & 0x3FFF;
}
hashtag场景注意事项介绍:
1.仅{...}里的部分参与hash。
2.如果有多个花括号,从左向右,取第一个花括号中的内容进行hash。
3.如果第一个花括号中内容为空如:a{}c{d},则整个key参与hash。
4.相同的hashtag被分配到相同的节点,相同的槽。
hash算法采用crc16。crc16算法为redis自己封装的,源码位置:https://github.com/redis/redis/blob/6.2.6/src/crc16.c
hashtag使用中的缺点:
在Redis集群中使用hashtag虽然提供了一定的便利性,但也存在一些缺点。以下是根据搜索结果得出的hashtag的主要缺点:
1. 数据倾斜:
使用hashtag可能会导致数据集中在集群的某个实例中,造成数据倾斜。例如,如果大量使用相同的hashtag,可能会导致所有相关的数据都被存储在同一个节点上。这种情况可能会影响集群的负载均衡,使得单个节点承受过大的压力,而其他节点资源未被充分利用。
2. 影响集群性能:
当大量数据因为hashtag而被集中存储在同一节点时,可能会影响该节点的性能,尤其是在高并发场景下,节点可能会成为瓶颈,导致响应速度变慢。
3. 迁移和扩展困难:
在使用hashtag后,如果需要对集群进行扩展或缩减,数据迁移可能会变得复杂。因为需要确保hashtag相关的数据在迁移过程中保持一致性,这可能需要额外的人工干预和复杂的数据迁移策略。
4. 限制批量操作:
在Redis集群中,批量操作(如pipeline)要求所有涉及的key必须位于同一个槽位中。如果使用hashtag导致数据分布在不同的槽位,将无法执行批量操作,这限制了某些操作的执行。
5. 增加复杂性:
引入hashtag机制增加了Redis集群使用和管理的复杂性。开发者和运维人员需要对hashtag的工作原理有深入的理解,才能有效地避免潜在的问题。
6. 热点问题:
hashtag可能导致某些key成为热点key,即频繁访问的key。当这些热点key集中在同一节点时,可能会导致该节点过载,影响整个集群的性能和稳定性。
综上所述:
在使用hashtag时,需要权衡其带来的便利性和可能引发的问题,合理规划数据分片策略,以确保集群的健康运行和数据的均衡分布。