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();
    }

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
一致性哈希是一种常见的分布式哈希算法,主要用于解决缓存、负载均衡、分布式数据库等领域的问题。下面是一个简单的 Java 代码实现一致性哈希算法的示例: ```java import java.util.*; public class ConsistentHash { private final SortedMap<Integer, String> circle = new TreeMap<>(); private final int virtualNodes; public ConsistentHash(int virtualNodes, Collection<String> nodes) { this.virtualNodes = virtualNodes; for (String node : nodes) { addNode(node); } } public void addNode(String node) { for (int i = 0; i < virtualNodes; i++) { String virtualNode = node + "#" + i; int hash = getHash(virtualNode); circle.put(hash, node); } } public void removeNode(String node) { for (int i = 0; i < virtualNodes; i++) { String virtualNode = node + "#" + i; int hash = getHash(virtualNode); circle.remove(hash); } } public String getNode(String key) { if (circle.isEmpty()) { return null; } int hash = getHash(key); if (!circle.containsKey(hash)) { SortedMap<Integer, String> tailMap = circle.tailMap(hash); hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey(); } return circle.get(hash); } private int getHash(String key) { // TODO: 实现自己的哈希算法 return key.hashCode(); } } ``` 上述代码中,我们通过 `SortedMap` 维护了一个环形空间,并将每个节点对应的虚拟节点散落在这个空间上。在查找节点时,首先计算出键值的哈希值,然后查找距离这个哈希值最近的节点
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT界的老菜鸟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值