一篇文章读懂Java哈希与一致性哈希算法

目录
  • 哈希 Hash 算法介绍
  • 分布式存储场景
    • 场景描述:
    • 实现思路:
    • 缺点:
  • 一致性Hash算法
    • 节点增加场景
    • 节点减少场景
    • 节点分布不均匀
  • 虚拟节点
    • 增加节点
    • 节点减少
  • 总结

哈希 Hash 算法介绍

哈希算法也叫散列算法, 不过英文单词都是 Hash, 简单一句话概括, 就是可以把任意长度的输入信息通过算法变换成固定长度的输出信息, 输出信息也就是哈希值, 通常哈希值的格式是16进制或者是10进制, 比如下面的使用 md5 哈希算法的示例

md5("123456") => "e10adc3949ba59abbe56e057f20f883e"

主要特点:

  • 不可逆 从哈希值不能推导出原始数据, 所以Hash算法广泛应用在现代密码体系中
  • 无碰撞 不同的信息进行哈希后得到的值应该是不同的, 但是从理论上来说, 哈希算法其实是有可能发生碰撞的, 输入的信息是无穷的, 而输出的哈希值长度是固定的, 所以是有限的。好比要把10个苹果放到9个抽屉里面, 肯定会有一个抽屉装了多个苹果, 只不过哈希算法的碰撞的概率是非常小的, 比如128位的哈希值, 就有2的128次方的空间。
  • 效率高 在处理比较大的原生值时, 也能能快速的计算出哈希值
  • 无规律 原始输入信息修改一点信息, 得到的哈希值也是大不相同的

哈希算法的实现有很多, 常见的有 MD5, SHA-1, 还有像 C#, Java 一些语言都有直接的 GetHashCode(), hashCode() 函数可以直接来用。

分布式存储场景

在互联网场景中, 通常面对的都是海量的数据,海量的用户, 那为了要满足大量数据的写入和查询, 以及高可用, 一台单机的存储服务器肯定是不能满足需求的, 通常需要使用多台服务器形成分布式存储。

场景描述:

在本文中, 为了方便大家更好的理解, 这里列出了一个简单的例子, 有三位用户, 分别是 James、 Bob、 Lee, 我们需要把用户的图片写入到存储服务器节点, 这里有ABC三个节点, 而且当查询用户的图片时, 还需要快速定位到这个用户的图片是在哪个节点存储的, 然后直接从这个节点进行查询, 需要满足高效率的查询。

实现思路:

首先,我们可以对用户标识进行 Hash 计算, 这里我为了方便演示, 使用了用户名作为Hash对象, 当然你还可以对用户的IP或者是UserId 进行Hash计算, Hash计算后会生成一个int类型的数字, 然后再根据存储节点的数量进行取模, 这里的公式就是 hash(name) % 3, 计算得出的结果只有三种情况, 分别是 0,1,2, 然后我们再把这三种结果和三个存储节点做一个映射, 0 ==> A, 1 ==> B, 2 == C。 因为Hash算法对一个值多次计算后都会得到同样的hash值, 所以上面的公式, 一个用户的图片每次都会固定的写入的其中一个节点, 这样做查询的话, 也可以通过hash算法快速找到这个用户的图片所在的节点。

缺点:

上面我们使用Hash算法实现了负载均衡, 可以根据用户名路由到图片所在的节点, 但是上面的方法也有一个很大的缺点, 那就是当服务器的数量增加或者减少时, 我们通过Hash算法生成的路由规则就再不准确了。

试想一下, 如果新增或者减少一个节点, 上面的公式就会变成 hash(name) % 4 或者 hash(name) % 2, 这里的关键点是, 我们之前用3取模, 现在变成用4或者2取模, 结果当然大概率是不一样了, 当然如果 Hash后是12的话,用3或者4取模得到的结果都是为0, 不过这种概率比较小。

这个问题带来的影响是, 路由规则不再准确, 大部分的查询请求都扑了空。那么如何解决这个问题呢?相信有的同学这时应该有了一些想法, 是的没错, 关键点就在于, 不管节点的数量怎么变化, 都应该使用一个固定的值来进行取模! 只有这样才能保证每次进行Hash计算后, 得出的结果是不变的!

一致性Hash算法

同样的,一致性Hash算法也是利用取模的方式, 不过通常是用一个很大的数字进行求模, 你可以用整数的最大值 int.Max, 2的32次方, 当然这个并没有要求, 不过越大的数字, 平均分配的概率就越大(后面会具体介绍这个问题)。

为了方便理解, 这里我用 1000 来取模, 我们可以用一个长度为1000的数组表示它,就像这样

接下来, 我们先不对用户的图片进行Hash处理, 而是先对每个节点进行 Hash 处理和映射, 现在的公式分别是 hash(A) % 1000, hash(B) % 1000,hash(C) % 1000, 这样得出的结果一定是在0-999 之间, 然后把这个值映射到数组中, 在代码中, 你可以用一个字典集合来保存这个映射关系。

对应的存储节点和数组的映射可能如下:

那现在用户的图片怎么和存储节点对应呢? 因为存储节点已经映射到了数组上, 我们现在可以通过范围区间的方式, 来找到对应的节点, 映射在数组上的图片可以向右查找, 找到了一个节点, 那么这个图片就属于这个节点, 当往右查找到最大值时,再回到最左边从0开始。

我在图中用不同的颜色标记了每个存储服务器的范围区间, 你可以理解一下

接下来, 我们就需要对用户的图片进行Hash计算取模,同样的,计算结果一定还是在0-999的范围内, 然后再把这些值映射到数组上, 映射的结果可能如下图:

这样就可以通过往右查找的方式, 找到用户的图片相对应的存储节点! 总结下来上面做了几件事情, 首先我们取一个固定的并且比较大的整数进行求模, 然后在对每个节点进行Hash计算求模, 这样可以找出每个节点所对应的范围区间, 最后再把用户的图片进行Hash计算求模, 最后根据范围区间确定图片的所在的存储节点。

那我们看看使用了这种方式, 当存储节点的增加和减少时会有什么影响?

节点增加场景

这里我新增了一个存储节点D, 经过Hash计算后映射在数组上, 这样的话, 用户 James 本来是路由到C节点的,现在被路由到了D节点, 不过我们添加了D节点后, 受到影响的也只有C节点, 其实不管D节点映射到数组哪一个位置, 都只会有一个节点会受到影响, 其他的节点可以正常使用。

那么这种情况下, 如何做数据迁移? 我的思路是, 我们需要把C节点全部数据重新进行Hash计算, 然后根据计算结果, 一部分会移动到D节点, 一部分继续保留在C节点。

节点减少场景

假如现在 A 节点在晚上20点宕机了, 那么本来应该路由到A节点的数据, 现在就被路由到了节点C, 也就是图中的 Bob的数据, 但是这种情况下, 其他的节点是不会受到节点变动的影响, 等到晚上21点时, A节点又恢复了正常。

这种情况的数据迁移的思路是, 当A节点宕机后, 数据需要全部复制到C节点, 当A节点恢复正常后, 再把C节点20:00至21:00的数据重新Hash计算, 然后根据计算的结果, 一部分会移动到A节点, 一部分继续保留在C节点。

节点分布不均匀

因为节点是随机的散列分布在数组上,所以有的节点的范围比较大, 而有的节点的范围比较小, 这样我们的数据分布就不均匀, 有的节点服务器会有比较大的请求压力。

这种情况有的同学可能会说, 我可以根据数组的长度(1000)和节点(3)的个数, 求出平均值, 然后就可以平均的把节点散列到数组上, 这样每个节点的请求压力都是一样的, 这种方式看起来没什么问题, 但是当添加节点或者移除节点的时候, 每个节点的覆盖范围都需要重新计算, 然后也需要迁移数据, 影响的范围还是挺大的。

虚拟节点

之前我们用了三个存储节点, 发现有分布不均匀的情况, 上图是我做了一个简单的测试, x 轴是节点的数量, y 轴是标准偏差, 根据这个图的趋势得出的结论是, 节点越多的时候, 标准偏差也就越小, 节点分布的就越均匀。

实际中,服务器节点是有限的, 增加很多节点也肯定是不现实的, 那么就可以在服务器节点不变的情况下, 引入虚拟节点, 也叫做影子节点。

还记得我们之前是怎么对节点做hash映射的吗?公式是 hash(node) / 1000, 我们现在可以给A节点创建10个虚拟节点, 分别是 A1, A2,A3..., A10, 对应的虚拟节点的公式就是 hash(A1) / 1000 等等。这些虚拟节点和真实节点存在映射关系, 当图片映射到A节点的任意一个虚拟节点上时, 我们就把这个图片路由到A存储节点, 现在数组上已经有了30个虚拟节点做映射, 分布也比之前更均匀了, 当然你也可以创建更多的虚拟节点, 这些真实节点和虚拟节点的映射关系要保存在内存中或者是数据库中, 现在我们的映射图如下, 这里我给每个真实节点都添加了3个虚拟节点。

引入了虚拟节点后, 在数组的映射看起来平均很多了, 现在我们每个真实节点的请求压力都是一样的了, 接下来, 我们还要看下这个方案在节点的变动情况下应该怎么处理。

增加节点

现在增加了一个节点D,按照上面的规则, 实际上是要添加 D 的虚拟节点, D1,D2,D3,然后散列映射到数组上,如下图所示:

先看最左边, D1 插入到了 C2 和 A1 之间, 而A1和A节点对应, D1节点和D节点对应, 也就是说A节点的一部分数据要迁移到D节点, 这里我的思路是, 当在节点写入数据时, 同时把虚拟节点的信息也记录下来,这样就很方便做数据迁移了, 我们可以在A节点中只找出A1虚拟节点的数据, 而不是全部, 然后Hash计算映射后, 根据计算结果,一部分同步到D节点, 一部分保持不变。后边的数据也可以按照这个思路进行数据迁移。

节点减少

当C节点下线的时候, 我们同时要移除和C节点对应的虚拟节点,C1,C2,C3, 然后就是数据迁移的工作了, 根据图中的表示, 可以直接把 C节点中的C2节点的数据迁移到A1节点, C1迁移到B3,C3迁移到B1中, 完成!

总结

本文介绍了哈希和一致性哈希算法, 以及提供了一些数据迁移的思路, 回顾下这个方案, 首先需要定义一个比较大的固定值用于取模, 然后创建和真实节点对应的虚拟节点, 最后再把虚拟节点映射到数组上, 通过范围区间的方法, 来确定图片属于哪一个存储节点。

可能还有同学会说, 一致性hash也有缓存失效的时候,也需要手动迁移数据。 是的, 维基百科对一致性Hash的定义是, 当节点的数量变动时,可以允许少部分的数据进行迁移, 而大部分的数据还是不变的。

上面的一致性Hash算法其实是经典的哈希环算法, 当然还有其他的算法, 比如跳跃一致性哈希法, 有兴趣也可以看一下。

以上就是一篇文章读懂哈希与一致性哈希算法的详细内容,更多关于Java哈希与一致性哈希算法的资料请关注我们其它相关文章!

(0)

相关推荐

  • 详解Java分布式系统中一致性哈希算法

    业务场景 近年来B2C.O2O等商业概念的提出和移动端的发展,使得分布式系统流行了起来.分布式系统相对于单系统,解决了流量大.系统高可用和高容错等问题.功能强大也意味着实现起来需要更多技术的支持.例如系统访问层的负载均衡,缓存层的多实例主从复制备份,数据层的分库分表等. 我们以负载均衡为例,常见的负载均衡方法有很多,但是它们的优缺点也都很明显: 随机访问策略.系统随机访问,缺点:可能造成服务器负载压力不均衡,俗话讲就是撑的撑死,饿的饿死. 轮询策略.请求均匀分配,如果服务器有性能差异,则无法实现

  • Java实现哈希表的基本功能

    一.哈希表头插法放入元素 /** * user:ypc: * date:2021-05-20; * time: 11:05; */ public class HashBuck { class Node { public int key; int value; Node next; Node(int key, int value) { this.key = key; this.value = value; } } public int usedSize; public Node[] array;

  • 一文彻底搞定Java哈希表和哈希冲突

    一.什么是哈希表? 哈希表也叫散列表,它是基于数组的.这间接带来了一个优点:查找的时间复杂度为 O(1).当然,它的插入时间复杂度也是 O(1).还有一个缺点:数组创建后扩容成本较高. 哈希表中有一个"主流"思想:转换.一个重要的概念是将「键」或「关键字」转换成数组下标.这由"哈希函数"完成. 二.什么是哈希函数? 由上,其作用就是将非 int 的键/关键字转化为 int 的值,使可以用来做数组下标. 比如,HashMap 中就这样实现了哈希函数: static f

  • 一致性哈希算法以及其PHP实现详细解析

    在做服务器负载均衡时候可供选择的负载均衡的算法有很多,包括:  轮循算法(Round Robin).哈希算法(HASH).最少连接算法(Least Connection).响应速度算法(Response Time).加权法(Weighted )等.其中哈希算法是最为常用的算法. 典型的应用场景是: 有N台服务器提供缓存服务,需要对服务器进行负载均衡,将请求平均分发到每台服务器上,每台机器负责1/N的服务. 常用的算法是对hash结果取余数 (hash() mod N):对机器编号从0到N-1,按

  • 一篇文章读懂Java哈希与一致性哈希算法

    目录 哈希 Hash 算法介绍 分布式存储场景 场景描述: 实现思路: 缺点: 一致性Hash算法 节点增加场景 节点减少场景 节点分布不均匀 虚拟节点 增加节点 节点减少 总结 哈希 Hash 算法介绍 哈希算法也叫散列算法, 不过英文单词都是 Hash, 简单一句话概括, 就是可以把任意长度的输入信息通过算法变换成固定长度的输出信息, 输出信息也就是哈希值, 通常哈希值的格式是16进制或者是10进制, 比如下面的使用 md5 哈希算法的示例 md5("123456") =>

  • 一篇文章读懂Python赋值与拷贝

    变量与赋值 在 Python 中,一切皆为对象,对象通过「变量名」引用,「变量名」更确切的叫法是「名字」,好比我们每个人都有自己的名字一样,咱们通过名字来代指某个人,代码里面通过名字来指代某个对象. 变量赋值就是给对象绑定一个名字,赋值并不会拷贝对象.好比我们出生的时候父母就要给我们取一个名字一样,给人取个绰号并不来多出一个人来,只是多一个名字罢了. 两个对象做比较有两种方式,分别是:is 与 == ,is比较的是两个对象是否相同,通过对象的ID值可识别是否为相同对象,==比较的是两个对象的值是

  • 一篇文章读懂nginx的gzip_static模块

    Nginx支持静态和动态两种包体gzip压缩方式,分别对应模块ngx_http_gzip_static,ngx_http_gzip. 我们知道gzip是CPU密集型的应用,实时动态压缩比较消耗CPU资源.另外,如果使用gzip,则sendfile零拷贝技术无法使用.为进一步提高Nginx的性能,我们可以使用静态gzip压缩,提前将需要压缩的文件压缩好,当客服请求到达时,直接发送压缩好的.gz文件,如此就减轻了服务器CPU的压力,提高了性能.缺省ngx_http_gzip_static模块并未启用

  • 一篇文章读懂nginx的gzip功能

    目录 前言 语义: 拓扑: 仿真: 验证: gzip_proxied的参数解析: 小结: 总结 前言 HTTP中包体[body]压缩协商对应的头字段为Accept-Encoding/Content-Encoding.对于HTTP包体压缩,Nginx的ngx_http_gzip_module模块提供了动态gzip压缩功能,并且有很精细的控制. 包括: 开启关闭gzip压缩: gzip on|off 对指定类型的文件进行压缩: gzip_types 文件最小压缩阈值: gzip_min_length

  • 一篇文章弄懂Java和Kotlin的泛型难点

    Java 和 Kotlin 的泛型算作是一块挺大的知识难点了,涉及到很多很难理解的概念:泛型型参.泛型实参.类型参数.不变.型变.协变.逆变.内联等等.本篇文章就将 Java 和 Kotlin 结合着一起讲,按照我的个人理解来阐述泛型的各个知识难点,希望对你有所帮助

  • 一篇文章看懂Java异常处理

    目录 异常的定义 异常的分类 异常的处理方法 try-catch处理 throw 和throws 自定义异常 总结 异常的定义 在java中,异常就是java在编译.运行或运行过程中出现的错误 总共有三种:1.编译错误 2.运行错误 3.逻辑错误 1.编译错误是因为程序没有遵循语法规则,编译程序能够自己发现并且提示我们错误的原因和位置,这个也是新手在刚接触编程语言时经常遇到的问题. 2.运行时错误是因为程序在执行时,运行环境发现了不能执行的操作. 3.逻辑错误是因为程序没有按照预期的逻辑顺序执行

  • 一篇文章看懂Java字符串操作

    目录 字符, 字节与字符串 字符与字符串 字节与字符串 字符串常见操作 字符串比较 字符串查找  字符串替换  字符串拆分  字符串截取 其他操作方法 总结 字符, 字节与字符串 字符与字符串 字符串内部包含一个字符数组,String 可以和 char[] 相互转换. NO 方法名称 类型 描述 1 public String(char value[]) 构造 将字符数组中的所有内容变字符串 2 public String(char value[],int offset,int count) 构

  • 一篇文章读懂什么是MySQL索引下推(ICP)

    目录 一.简介 二.原理 三.实践 3.1 不使用索引下推 3.2 使用索引下推 四.使用条件 五.相关系统参数 总结 一.简介 ICP(Index Condition Pushdown)是在MySQL 5.6版本上推出的查询优化策略,把本来由Server层做的索引条件检查下推给存储引擎层来做,以降低回表和访问存储引擎的次数,提高查询效率. 二.原理 为了理解ICP是如何工作的,我们先了解下没有使用ICP的情况下,MySQL是如何查询的: 存储引擎读取索引记录: 根据索引中的主键值,定位并读取完

  • 一篇文章搞懂MySQL加锁机制

    目录 前言 锁的分类 乐观锁和悲观锁 共享锁(S锁)和排他锁(X锁) 按加锁粒度区分 全局锁 表级锁(表锁和MDL锁) 意向锁 行锁 间隙锁 next-key lock(临键锁) 加锁规则 死锁和死锁检测 总结 前言 在数据库中设计锁的目的是为了处理并发问题,在并发对资源进行访问时,数据库要合理控制对资源的访问规则. 而锁就是用来实现这些访问规则的一个数据结构. 在对数据并发操作时,没有锁可能会引起数据的不一致,导致更新丢失. 锁的分类 乐观锁和悲观锁 乐观锁: 对于出现更新丢失的可能性比较乐观

  • 一篇文章搞懂Go语言中的Context

    目录 0 前置知识sync.WaitGroup 1 简介 2 context.Context引入 3 context包的其他常用函数 3.1 context.Background和context.TODO 3.2 context.WithCancel和 3.3 context.WithTimeout 3.4 context.WithDeadline 3.5 context.WithValue 4 实例:请求浏览器超时 5 Context包都在哪些地方使用 6 小结 0 前置知识sync.Wait

随机推荐