一致性Hash算法在分布式缓存系统中的应用与历史解析

一致性hash的历史

一致性Hash算法在分布式缓存系统中的应用与历史解析

Consistent Hashing 算法在 1997 年被提出于论文 Consistent hashing and random trees 中。如今,它在 cache 系统中的应用越来越广泛。

一致性hash的目的

一致性哈希算法在分布式系统中较为常用。普通余数 Hash 算法存在伸缩性差的问题,而一致性哈希算法对此进行了有效解决。它能够确保在服务器上线或下线时,尽量让更多的请求命中原本路由到的服务器。

问题背景

在业务开发过程中,我们通常会将数据持久化到数据库里。当需要读取这些数据时,除了直接从数据库中进行读取之外,为了能够减轻数据库的访问压力并且提高访问速度,我们更多地会引入缓存来对数据进行存取操作。

分布式缓存

分布式缓存中,不同机器分别存储不同对象的数据。为达成这些缓存机器的负载均衡,通常会有两种 Hash 算法来进行数据节点存储的均匀分配,其中一种是普通 Hash 算法。

普通的Hash算法的 Hash取模做法的缺陷

在一个 Redis 集群中,若将一条数据进行 Hash 操作,接着依据集群节点数取模来确定该数据应放置在哪个节点。这种方式存在的缺陷为:在进行扩容(即增加一个节点)之后,会有大量的缓存失效。

普通Hash的案例分析

如果你有 N 个 cache 服务器(以下简称 cache ),那么怎样把一个对象 object 映射到 N 个 cache 上呢?你很有可能会运用类似如下的通用方法来计算 object 的 hash 值,接着将其均匀地映射到 N 个 cache 上。

hash(object)%N 

一切都运行正常,再考虑如下的两种情况;

一致性Hash算法

一致性 hash 算法是为了解决此类问题的方法。它能够保证,在机器增加或者减少的时候,对缓存访问命中的概率所产生的影响能够减至很小。接下来我们详细说一说一致性 hash 算法的具体过程。

改良版一致性Hash算法 一致性Hash算法 + 虚拟节点

为解决数据分布不均的问题,我们引入了虚拟节点的概念。我们针对每一个服务节点进行多个哈希的计算,将每个计算结果所对应的位置都放置一个此服务节点,此服务节点被称为虚拟节点。当定位到虚拟节点时,其数据就会被存放到该虚拟节点所对应的真实节点上。通过这种方式,数据分布就变得相对均匀了。并且,虚拟节点的数量越多,数据分布就越均匀。

引入“虚拟节点”之后,映射关系由 { 对象 -> 节点 } 转变为了 { 对象 -> 虚拟节点 } 。在查询物体所在 cache 时的映射关系

一般虚拟节点数32个以上,dubbo是160个。

处理机器增减的情况

对于线上的业务,增加或者减少一台机器的部署是常有的事情。

部署机器 c4 并将其加入到 hash 环中机器 c3 与 c2 之间。此时,仅机器 c3 与 c4 之间的对象需重新分配至新机器。就我们的例子而言,仅有对象 o4 被重新分配到了 c4,其余对象仍在原机器上。

一致性Hash算法的实现原理

在业务开发时,我们经常把数据保存在数据库里。要是需要读取这些数据,除了直接从数据库读取之外,为了降低数据库的访问压力并且提升访问速度,我们更多地采用引入缓存的方式来对数据进行存取。读取数据的过程通常是这样的:

Java代码实现Hash算法的实现

使用一个 TreeMap 当作环,其 key 是虚拟节点的下标,value 是真实节点的 hash。我个人觉得可以添加一个 Map 去对真实节点与虚拟节点的关系进行维护。

/**
 * 一致性Hash算法
算法的详细解释为:http://blog.csdn.net/sparkliang/article/details/5279393
算法的实现地址为:https://weblogs.java.net/blog/2007/11/27/consistent-hashing
 * @author xiaoleilu
 *
 * @param    节点类型
 */
public class ConsistentHash<T> implements Serializable{
    private static final long serialVersionUID = 1L;
    
    /** Hash计算对象,用于自定义hash算法 */
    Hash32<Object> hashFunc;
    /** 复制的节点个数 */
    private final int numberOfReplicas;
    /** 一致性Hash环 */
    private final SortedMap<Integer, T> circle = new TreeMap<>();
    
    /**
     * 构造,使用Java默认的Hash算法
参数 numberOfReplicas 表示复制的节点个数,增加每个节点的复制节点有助于实现负载均衡。
     * @param nodes 节点对象
     */
    public ConsistentHash(int numberOfReplicas, Collection<T> nodes) {
        this.numberOfReplicas = numberOfReplicas;
        this.hashFunc = key -> {
            //默认使用FNV1hash算法
            return HashUtil.fnvHash(key.toString());
        };
        //初始化节点
        for (T node : nodes) {
            add(node);
        }
    }
    /**
     * 构造
     * @param hashFunc hash算法对象
     * @param numberOfReplicas 复制的节点个数,增加每个节点的复制节点有利于负载均衡
     * @param nodes 节点对象
     */
    public ConsistentHash(Hash32<Object> hashFunc, int numberOfReplicas, Collection<T> nodes) {
        this.numberOfReplicas = numberOfReplicas;
        this.hashFunc = hashFunc;
        //初始化节点
        for (T node : nodes) {
            add(node);
        }
    }
    /**
     * 增加节点
* 每增加一个节点,就会在闭环上增加给定复制节点数
如果复制节点数为 2,那么每调用此方法一次,就会增加两个虚拟节点,且这两个节点指向同一个 Node 因为 hash 算法会调用 node 的 toString 方法,所以依据 toString 来进行去重。 * @param node 节点对象 */
public void add(T node) { for (int i = 0; i < numberOfReplicas; i++) { circle.put(hashFunc.hash32(node.toString() + i), node); } } /** * 移除节点的同时移除相应的虚拟节点 * @param node 节点对象 */ public void remove(T node) { for (int i = 0; i < numberOfReplicas; i++) { circle.remove(hashFunc.hash32(node.toString() + i)); } } /** * 获得一个最近的顺时针节点 给定键取 Hash 后,取得顺时针方向上最近的一个虚拟节点,该虚拟节点对应的是实际节点。 * @return 节点对象 */ public T get(Object key) { if (circle.isEmpty()) { return null; } int hash = hashFunc.hash32(key); if (false == circle.containsKey(hash)) { SortedMap<Integer, T> tailMap = circle.tailMap(hash); //返回此映射的部分视图,其键大于等于 hash hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey(); } //正好命中 return circle.get(hash); } }

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

24阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

27阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

23阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

25阅读