Java实现 Hash一致性 节点的增删查及数据变动,代码都未测试
前言
内容不对概念做描述,网上描述一大堆,但是没找到有相应的实现代码。
该代码思想是基于redis的分片思想去做的,例如需要缓存10000条数据到2个真实的redis节点,该如何创建hash环?扩展redis节点时,该如何?删除一个redis节点时,该如何?
以下代码都未经测试,如果有更好的实现思想,请知会。
代码及方法解释
initAddServer初始化方法
系统第一次初始化hash环时调用的,这是还未缓存任何数据。
一致性Hash的核心就是一个Hash环,在Java的数据结构中已有实现好的SortedMap,该Map是一个已经排好序的。
resizeServer 扩容方法
扩容时会涉及到数据的重新分配,本来是想先分配数据,再去删除之前虚拟节点中的数据,这样就不会影响redis的缓存查询,问题是后删除数据,我找不到该删除这个虚拟节点哪些数据了(应该有办法实现)。我显现在是先删除缓存数据,再分配数据,这样会造成部分数据缓存失效。
getServer 计算该数据放在哪个虚拟节点上
返回该虚拟节点的地址;
removeServer 删除物理节点
该方法是先做数据重分配,分配完成后再把物理节点真正删除掉;
代码
package com.owinfo.custom.util;
import org.apache.commons.lang3.StringUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.*;
/**
* 一致性hash算法
* 用redis举例
* 在redis中存储结构:运用hash存储,{虚拟节点key: {商品id1:商品信息,商品id2:商品信息,商品id3:商品信息}}
*/
public class AgreementHashAlgorithm {
/*
* 虚拟节点的个数
*/
private static int virtualSum = 100;
/*
* 真实的物理节点集合
*/
private static List<String> realNodeList = new ArrayList<>(10);
/*
* 真实节点对应的虚拟节点集合, 如果要删除一个节点,就去这里把对应的List<Integer>全部删除掉
*/
private static Map<String, List<Integer>> virtualNodeMap = new HashMap<>(16);
/*
* 存放虚拟节点对应真实节点,方便后面查找
*/
private static volatile SortedMap<Integer, String> virtualToRealNodeMap = new TreeMap<>();
/**
* 初始化时添加物理机
*
* @param realNode 真实的物理机节点
*/
public static void initAddServer(String realNode) {
//该物理节点对应的所有虚拟节点
List<Integer> virtualNodeList = new ArrayList<>(virtualSum);
for (int i = 0; i < virtualSum; i++) {
//虚拟节点名称,自己随意取
String virtualNode = String.format("s%s%s%", realNode, UUID.randomUUID().toString(), i);
System.out.println(virtualNode);
int hash = getHash(virtualNode);
//判断当前hash在virtualToRealNodeMap中是否唯一,就是在整个hash虚拟节点中是不是唯一的
if (StringUtils.isEmpty(virtualToRealNodeMap.get(hash))) {
i--;//保证每个物理节点都有100个虚拟节点
continue;
}
virtualNodeList.add(hash);
//将虚拟节点放到环上去
virtualToRealNodeMap.put(hash, realNode);
}
realNodeList.add(realNode);
virtualNodeMap.put(realNode, virtualNodeList);
}
/**
* 扩容物理机
*
* @param realNode 真实的物理机节点
*/
public static void resizeServer(String realNode) throws Exception {
/*Integer虚拟节点的hash,List<String>该虚拟节点存放的数据。这个deleteDatas的作用:
1、是当把所有需要重新分配的数据都存放起来;
2、在重新分配前,把当前虚拟节点上的数据先清空掉(这里没想好如何先插入数据再清理数据),这个可能会造成服务器卡顿,所以扩容需要挑迸发小的时候扩容;
*/
Map<Integer, List<String>> deleteDatas = new HashMap<>();
//该物理节点对应的所有虚拟节点
List<Integer> virtualNodeList = new ArrayList<>(virtualSum);
//copy一个临时环,不影响当前缓存正常工作
SortedMap<Integer, String> virtualToRealNodeTempMap = depthClone(virtualToRealNodeMap);
for (int i = 0; i < virtualSum; i++) {
String virtualNode = String.format("s%s%s%", realNode, UUID.randomUUID().toString(), i);
int hash = getHash(virtualNode);
//判断当前hash在virtualToRealNodeMap中是否唯一,就是在全部的hash虚拟节点中是不是唯一的
if (StringUtils.isEmpty(virtualToRealNodeTempMap.get(hash))) {
i--;//保证每个物理节点都有100个虚拟节点
continue;
}
virtualNodeList.add(hash);
//将虚拟节点放到环上去
virtualToRealNodeTempMap.put(hash, realNode);
//新虚拟节点会让之前的哪个虚拟节点中的内容重新分配
SortedMap<Integer, String> subMap = virtualToRealNodeMap.tailMap(hash);
//这个firstNode表示是当前虚拟节点virtualNode的下一个节点,因为要去把他下一个节点里面的缓存全部拿出来,重新计算这部分数据该放在什么地方
Integer firstNode = null;
if (subMap.isEmpty()) {
//如果没有比该key的hash值大的,则从第一个node开始
firstNode = virtualToRealNodeMap.firstKey();
} else {
//第一个Key就是顺时针过去离node最近的那个结点
firstNode = subMap.firstKey();
}
if (deleteDatas.get(firstNode)== null) {
List<String> nodeDatas = redis.get(firstNode);//todo 伪代码,拿出当前虚拟节点的所有数据
deleteDatas.put(firstNode, nodeDatas);
}
}
realNodeList.add(realNode);
virtualNodeMap.put(realNode, virtualNodeList);
//将deleteDatas重新做分配,下面几步会导致部分缓存失效
//更新treeMap
virtualToRealNodeMap = virtualToRealNodeTempMap;
deleteDatas.forEach((node, nodeData) -> {
//todo 伪代码删除该node上的数据
//删除这个步骤也可以后面做,可以根据当前虚拟节点的Hash、和它上一个虚拟节点的hash值、和数据的hash值来判断这条数据该不该删除
redis.deleteData(node);
});
deleteDatas.forEach((node, nodeData) -> {
//todo 伪代码 将数据重新分配
nodeData.forEach(data -> {
getServer(data, virtualToRealNodeTempMap);
//todo 伪代码 将数据set到redis中去
});
});
}
/**
* 对外提供查询。存放数据时,根据该数据的key找到对应的服务器
*
* @param key 需要缓存数据的key
*/
public static String getServer(String key) {
return getRealServer(key, virtualToRealNodeMap);
}
/**
* 删除物理机时,重新放入数据时获取新的物理机节点
*
* @param key 需要缓存数据的key
* @param virtualMap 已经删除物理节点后的map
*/
private static String getServer(String key, SortedMap<Integer, String> virtualMap) {
return getRealServer(key, virtualMap);
}
/**
* 存放数据时,根据该数据的key找到对应的服务器
*
* @param key 需要缓存数据的key
* @param virtualMap 存放虚拟节点的map
*/
private static String getRealServer(String key, SortedMap<Integer, String> virtualMap) {
//得到该key的hash值
int hash = getHash(key);
//得到大于该Hash值的所有Map
//例如环:1>20>30>40>50>1这个闭环,如果key的hash是再50到1这个区间内,那么下面subMap的值时为空的
SortedMap<Integer, String> subMap = virtualMap.tailMap(hash);
if (subMap.isEmpty()) {
//如果没有比该key的hash值大的,则从第一个node开始
Integer firstKey = virtualMap.firstKey();
//返回对应的服务器
return virtualMap.get(firstKey);
} else {
//第一个Key就是顺时针过去离node最近的那个结点
Integer firstKey = subMap.firstKey();
//返回对应的服务器
return subMap.get(firstKey);
}
}
/**
* 删除物理机
*
* @param realNode 真实的物理机节点
*/
public static void removeServer(String realNode) throws Exception {
//获取当前所有的物理机对应的虚拟机节点
List<Integer> virtualNodes = virtualNodeMap.get(realNode);
//获取当前物理机存储的所有数据
List<String> allDatas = new ArrayList();
for (Integer virtualNode : virtualNodes) {
/**realNode+virtualNode 表示每个虚拟机在物理机上存储的key,我理解的是,虽然数据存放在一台物理机上,
但是还是得区分是那台虚拟节点的数据,因为物理机扩容时,就会设计到动虚拟节点里面的数据
*/
allDatas.addAll(redis.get(realNode + virtualNode));// todo 伪代码
}
//将原本的virtualToRealNodeMap备份到virtualToRealNodeTempMap,下面删除虚拟节点这个动作,先操作virtualToRealNodeTempMap,这个是防止在删除虚拟节点时,导致用户查询不到数据
SortedMap<Integer, String> virtualToRealNodeTempMap = depthClone(virtualToRealNodeMap);
//删除virtualToRealNodeTempMap虚拟机节点
for (Integer virtualNode : virtualNodes) {
virtualToRealNodeTempMap.remove(virtualNode);
}
//todo 伪代码了 将上面的allDatas放到已删除的hash环中去
for (String data : allDatas) {
String server = getServer(data, virtualToRealNodeTempMap);
redis.setkey(data, server);// todo 伪代码 ,这里就是放入新的缓存中去
}
//当把删除物理机上的缓存数据都重新缓存完成后,切换virtualToRealNodeMap的数据为virtualToRealNodeTempMap
virtualToRealNodeMap = virtualToRealNodeTempMap;
}
/**
* 使用FNV1_32_HASH算法计算服务器的Hash值,这里不使用重写hashCode的方法,最终效果没区别
*/
private static int getHash(String str) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < str.length(); 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;
}
/**
* 只能拷贝相同的对象
*
* @param source 被克隆的对象
* @return
*/
public static <T> T depthClone(T source) throws Exception {
//将对象序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(source);
//对象反序列化,生成新的对象
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (T) ois.readObject();
}
}