一、Redis哈希槽
1、哈希槽介绍
Redis Cluster在设计中没有使用一致性哈希(Consistency Hashing),而是使用数据分片引入哈希槽(hash slot)来实现;
一个 Redis Cluster包含16384(0~16383)个哈希槽(补充:为什么redis集群的最大槽数是16384个?),存储在Redis Cluster中的所有键都会被映射到这些slot中,集群中的每个键都属于这16384个哈希槽中的一个。按照槽来进行分片,通过为每个节点指派不同数量的槽,可以控制不同节点负责的数据量和请求数.
当前集群有3个节点,槽默认是平均分的:
- 节点 A (6381)包含 0 到 5499号哈希槽.
- 节点 B (6382)包含5500 到 10999 号哈希槽.
- 节点 C (6383)包含11000 到 16383号哈希槽.
2、哈希槽计算公式
集群使用公式slot=CRC16(key)/16384来计算key属于哪个槽,其中CRC16(key)语句用于计算key的CRC16 校验和。
3、哈希槽怎么工作
我们看到的是master节点在 Redis Cluster中的实现时,都存有所有的路由信息。
当客户端的key 经过hash运算,发送slot 槽位不在本节点的时候:
- (1)如果是非集群方式连接,则直接报告错误给client,告诉它应该访问集群中那个IP的master主机。
- (2)如果是集群方式连接,则将客户端重定向到正确的节点上。
注意这里并不是127.0.0.1:7001 帮client去连接127.0.0.1:7003获取数据的,而是将客户端请求重定向了。
4、哈希槽的优点
很容易添加或者删除节点:
- 比如如果我想新添加个节点D, 我需要从节点 A, B, C中转移部分槽到D上即可.
- 如果我像移除节点A,需要将A中得槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可.
由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态.
5、补充知识
查看哈希槽分区情况
通过cluster nodes命令,可以清晰看到Redis Cluster中的每一个master节点管理的哈希槽。比如 127.0.0.1:7001 拥有哈希槽 0-5460, 127.0.0.1:7002 拥有哈希槽 5461-10922, 127.0.0.1:7003 拥有哈希槽 10923-16383。
//查看集群中,各个master节点的哈希槽分区情况
127.0.0.1:7001> cluster nodes
e51711eb03d 127.0.0.1:7002@17002 master - 0 1590126183862 2 connected 5461-10922
68c5fc14287 127.0.0.1:7003@17003 master - 0 1590126181856 3 connected 10923-16383
903322e4431 127.0.0.1:7001@17001 myself,master - 0 1590126182000 1 connected 0-5460
二、Java一致性Hash算法实现
1、一致性哈希介绍
一致性哈希的主要作用就是用来进行负载均衡,我们设想这么一个场景,我们有四台服务器,然后需要将一批资源均匀的分配到这四台服务器上。
2、不含虚拟节点的一致性哈希
首先我们先想象有这么一个圆环(也叫哈希环),在哈希环上有2^32个节点;
1、对服务器(能唯一标识该服务的属性,比如服务器的IP)进行求哈希值,使其落到哈希环,如图所示Node0-Node3(四个箭头所示)对应四台服务器;
2、对我们要分配的资源进行求哈希值,同样落到哈希环上,如图所示(图中的星标标识落在哈希环上的资源);
3、规定落在哈希环上的资源,分配到该资源所在节点顺时针遇到的第一个服务节点上,如图所示;
如果服务器所在的哈希环上的节点如上图所示,均匀分布,那自然是最好的,但如果像下图所示,那么就会导致资源的分配不均。为了解决这种情况出现的问题,出现了虚拟节点的一致性哈希。
3、虚拟节点的一致性哈希
所谓的虚拟节点,就是我们假想出来的节点,将这些虚拟节点映射到哈希环上,然后将这些虚拟节点与实际服务器进行关联,例如虚拟节点Node0,Node1与服务器1关联,那么分配到Node0,Node1的资源,也即是分配到实际的服务器1上。也就是说我们实际的服务器有四台,但我们可以将N个虚拟的节点映射到哈希环上,这样就能通过调节虚拟节点与实际服务器的关联关系,来达到调节分配到不同服务器资源的数量的目的。
3.1、一致性哈希的代码实现
/**
* 计算哈希值的工具类
*/
public class Hash {
/**
* 使用String自带的hashCode函数
* @param str
* @return
*/
public static Integer stringHashcode(String str){
int hash = str.hashCode();
if (hash<0){
hash = Math.abs(hash);
}
return hash;
}
/**
* 使用FNV1_32_HASH算法计算hash值
* @param str
* @return
*/
public static Integer FNV1_32_HASH(String str){
final int p = 1677769;
int hash = (int)0;
int len = str.length();
for (int i=0;i<len;i++){
hash = (hash^str.charAt(i))*p;
}
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
if (hash<0){
hash=Math.abs(hash);
}
return hash;
}
}
3.2、不含虚拟节点的实现
/**
* 不带虚拟节点的一致性哈希实现
*/
public class HashTest {
private static String[] servers = {"192.168.0.1:8999","192.168.0.2:8999","192.168.0.0.3:8999"};
/**
* key 服务器的哈希值、
* value 服务器信息
*/
private static SortedMap<Integer,String> map = new TreeMap<Integer, String>();
static {
/**
* 将服务器信息存放到map中
*/
int size = servers.length;
for (int i = 0;i<size;i++){
int hash = Hash.FNV1_32_HASH(servers[i]);
map.put(hash, servers[i]);
System.out.println("hash="+hash+" "+"server="+servers[i]);
}
}
/**
* 获取资源所在的节点服务器
* @param node
* @return
*/
public static String getServer(String node){
Integer hash = Hash.FNV1_32_HASH(node);
//获取大于当前node的hash值的所有服务节点
SortedMap<Integer, String> sortedMap = map.tailMap(hash);
Integer key = 0;
if (sortedMap.isEmpty()){
//如果没有比node节点hash值大的服务节点,则取hash值最小的服务节点
key = map.firstKey();
}else{
//获取过滤出的服务节点的第一个值,也就是从node开始顺时针遇到的第一个服务节点
key = sortedMap.firstKey();
}
String server = map.get(key);
return server;
}
public static void main(String[] args) {
String[] index = {"582639584","582639585","582639586","582639587"};
System.out.println("-------------");
int size = index.length;
for(int i=0;i<size;i++){
String server = getServer(index[i]);
System.out.println("index="+index[i] +" hash="+Hash.FNV1_32_HASH(index[i]) +" server="+server);
}
}
}
3.3、带虚拟节点的代码实现
/**
* 带虚拟节点的代码实现
*/
public class HashVirtualNodeTest {
/**
* 实际的服务器
*/
private static String[] servers = {"192.168.0.1:8999","192.168.0.2:8999","192.168.0.0.3:8999"};
/**
*每个服务节点对应的虚拟服务节点数目
*/
private final static int virtualNode = 5;
/**
* 哈希环上的虚拟服务节点
* key 虚拟服务节点的hash值
* value 虚拟服务节点名称
*/
private static SortedMap<Integer,String> virtualNodeMap = new TreeMap<Integer, String>();
static{
/**
* 将虚拟节点放到virtualNodeMap中
* 虚拟节点与实际的服务器的对应关系
* 服务器名称:server
* 虚拟节点名:server&&Node0 server&&Node1 server&&Node2 ...
*/
int len = servers.length;
for (int i=0;i<len;i++){
for(int j=0;j<virtualNode;j++){
String virtualNodeName = servers[i]+"&&"+"Node"+j;
Integer hash = Hash.FNV1_32_HASH(virtualNodeName);
virtualNodeMap.put(hash,virtualNodeName);
System.out.println("hash="+hash+" virtualNode=" +virtualNodeName);
}
}
}
/**
* 获取一个节点对应的分配到的服务节点
* @param node 待查询的节点
* @return 实际的服务节点名称
*/
public static String getServer(String node){
Integer hash = Hash.FNV1_32_HASH(node);
//获取大于当前节点的虚拟节点服务器
SortedMap<Integer, String> tailMap = virtualNodeMap.tailMap(hash);
Integer key = 0;
if(virtualNodeMap.isEmpty()){
//没有大于当前节点的虚拟服务节点时,取虚拟服务节点的第一个
key = virtualNodeMap.firstKey();
}else{
//取出大于当前节点的第一个虚拟服务节点
key = tailMap.firstKey();
}
//取出对应的虚拟节点
String virtualNode = virtualNodeMap.get(key);
//获取实际的服务名称
String server = virtualNode.split("&&")[0];
return server;
}
public static void main(String[] args){
String[] index = {"582639584","582639585","582639586","582639587","582639588"};
System.out.println("------------------");
int size = index.length;
for (int i=0;i<size;i++){
String server = getServer(index[i]);
System.out.println("index="+index[i]+" hash="+ Hash.FNV1_32_HASH(index[i]) +" server="+server);
}
}
}