本文已参与「新人创作礼」活动,一起开启掘金创作之路
本文是我在掘金平台发表于2022年4月份的,从发表最初累计第7篇博客,希望大家关注我,我将会持续在后端和大数据等领域进行书写更多的文章。
摘要
本文详细的介绍哈希算法,介绍了其原理,以及经典的哈希算法,以及分布式场景下的一致性哈希算法,并且描述了哈希算法的一些经典的应用。
一 引言
哈希,英文Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
计算机世界是一个讲求速度的世界,快速的查询和定位是非常被青睐的,而哈希算法最重要的目的就是实现致力于快速查询的一个算法。计算机存储原理中,最简单的是连续存储,代表就是数组,数组的查找根据下标来实现,而哈希算法则在数据的基础上,可以把一个字符串或者一个对象转换成数字,再把这个数字作为下标进行取查找,实现了更加广义的下标,也即是下标不限于数字。我们把这写个下标称为为“键”,和对应的值一起合称为“键值对”。
此外,哈希算法还不限于快速查找,还可以用来做唯一性标识,用来做加密等等。不用的用途利用了哈希算法的不同特性,下面将展开详细的介绍。
本文的结构是,在第二章节介绍了哈希算法作为查询的时候最基本的原理,以及rehash的方法,第三章讲了一些经典的流行的哈希算法,第四章节讲了分布式环境下的一致性算法,第五部分讲了哈希算法的应用,第六部分讲了相关工作,最后一部分是参考文献。
二 原理
我们通常使用数组或者链表来存储元素,一旦存储的内容数量特别多,需要占用很大的空间,而且在查找某个元素是否存在的过程中,数组和链表都需要挨个循环比较,而通过 哈希 计算,可以大大减少比较次数。如下图所示:
哈希通过一次计算大幅度缩小查找范围,自然比从全部数据里查找速度要快。
几种常见的哈希函数(散列函数)构造方法,有如下:
(1)直接定址法
- 取关键字或关键字的某个线性函数值为散列地址。 即 H(key) = key 或 H(key) = a*key + b,其中a和b为常数。
(2)除留余数法
- 取关键字被某个不大于散列表长度 m 的数 p 求余,得到的作为散列地址。
- 即 H(key) = key % p, p < m。
(3)数字分析法
- 当关键字的位数大于地址的位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为散列地址。
- 仅适用于所有关键字都已知的情况下,根据实际应用确定要选取的部分,尽量避免发生冲突。
(4)平方取中法
- 先计算出关键字值的平方,然后取平方值中间几位作为散列地址。
- 随机分布的关键字,得到的散列地址也是随机分布的。
(5)折叠法(叠加法)
- 将关键字分为位数相同的几部分,然后取这几部分的叠加和(舍去进位)作为散列地址。
- 用于关键字位数较多,并且关键字中每一位上数字分布大致均匀。
构造哈希函数的方法很多,实际工作中要根据不同的情况选择合适的方法,总的原则是尽可能少的产生冲突。
通常考虑的因素有关键字的长度和分布情况、哈希值的范围等。
哈希冲突的解决 选用哈希函数计算哈希值时,可能不同的 key 会得到相同的结果,一个地址怎么存放多个数据呢?这就是冲突。
常用的主要有两种方法解决冲突:
1.链接法(拉链法)
拉链法解决冲突的做法是: 将所有关键字为同义词的结点链接在同一个单链表中。
若选定的散列表长度为 m,则可将散列表定义为一个由 m 个头指针组成的指针数组 T[0..m-1] 。
凡是散列地址为 i 的结点,均插入到以 T[i] 为头指针的单链表中。 T 中各分量的初值均应为空指针。
在拉链法中,装填因子 α 可以大于 1,但一般均取 α ≤ 1。
2.开放定址法
用开放定址法解决冲突的做法是:
用开放定址法解决冲突的做法是:当冲突发生时,使用某种探测技术在散列表中形成一个探测序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探测到开放的地址则表明表中无待查的关键字,即查找失败。
简单的说:当冲突发生时,使用某种探查(亦称探测)技术在散列表中寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到。
按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、二次探查法、双重散列法等。
a.线性探查法
hi=(h(key)+i) % m ,0 ≤ i ≤ m-1
基本思想是: 探查时从地址 d 开始,首先探查 T[d],然后依次探查 T[d+1],…,直到 T[m-1],此后又循环到 T[0],T[1],…,直到探查到 有空余地址 或者到 T[d-1]为止。
b.二次探查法
hi=(h(key)+i*i) % m,0 ≤ i ≤ m-1
基本思想是: 探查时从地址 d 开始,首先探查 T[d],然后依次探查 T[d+1^2],T[d+2^2],T[d+3^2],…,等,直到探查到 有空余地址 或者到 T[d-1]为止。
缺点是无法探查到整个散列空间。
c.双重散列法
hi=(h(key)+i*h1(key)) % m,0 ≤ i ≤ m-1
基本思想是: 探查时从地址 d 开始,首先探查 T[d],然后依次探查 T[d+h1(d)], T[d + 2*h1(d)],…,等。
该方法使用了两个散列函数 h(key) 和 h1(key),故也称为双散列函数探查法。
定义 h1(key) 的方法较多,但无论采用什么方法定义,都必须使 h1(key) 的值和 m 互素,才能使发生冲突的同义词地址均匀地分布在整个表中,否则可能造成同义词地址的循环计算。
该方法是开放定址法中最好的方法之一。
三 经典哈希算法
DJB
下面是C语言实现。初始值是5381,遍历整个串,按照hash * 33 +c的算法计算。得到的结果就是哈希值。
unsigned long
hash(unsigned char *str)
{
unsigned long hash = 5381;
int c;
while (c = *str++)
hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
return hash;
}
里面涉及到两个神奇的数字,5381和33。为什么是这两个数?我还特意去查了查,说是经过大量实验,这两个的结果碰撞小,哈希结果分散。
还有一个事情很有意思,乘以33是用左移和加法实现的。
Java 字符串哈希
看了上面的再看Java内置字符串哈希就很有意思了。Java对象有个内置对象hash,它缓存了哈希结果,如果当前对象有缓存,直接返回。如果没有缓存,遍历整个字符串,按照hash * 31 + c的算法计算。
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
DJB相比,初始值从5381变成了0,乘的系数从33变成了31。
Murmur哈希
Murmur哈希是一种非加密散列函数,适用于一般的基于散列的查找。它在2008年由Austin Appleby创建,在Github上托管,名为“SMHasher” 的测试套件。 它也存在许多变种,所有这些变种都已经被公开。 该名称来自两个基本操作,乘法(MU)和旋转(R),在其内部循环中使用。 与加密散列函数不同,它不是专门设计为难以被对手逆转,因此不适用于加密目的。
2018年的版本是Murmur哈希3,它产生一个32位或128位散列值。 使用128位时,x86和x64版本不会生成相同的值,因为算法针对各自的平台进行了优化。
MD4
MD4(RFC 1320)是 MIT 的Ronald L. Rivest在 1990 年设计的,MD 是 Message Digest(消息摘要) 的缩写。它适用在32位字长的处理器上用高速软件实现——它是基于 32位操作数的位操作来实现的。
MD5
MD5(RFC 1321)是 Rivest 于1991年对MD4的改进版本。它对输入仍以512位分组,其输出是4个32位字的级联,与 MD4 相同。MD5比MD4来得复杂,并且速度较之要慢一点,但更安全,在抗分析和抗差分方面表现更好。
SHA-1及其他
SHA1是由NIST NSA设计为同DSA一起使用的,它对长度小于264的输入,产生长度为160bit的散列值,因此抗穷举(brute-force)性更好。SHA-1 设计时基于和MD4相同原理,并且模仿了该算法。
MD4,MD5,SHA-1都是加密算法,而前面的几种则是非加密算法。加密算法强调安全和不可破解,而非加密算法则重在利用其唯一性和快速定位的能力。
四 一致性哈希算法
当我们利用多个数据存储节点做缓存服务的时候,考虑到使用哈希算法来做负载均衡,然而在分布式环境中,可能经常出现节点的宕机以及扩容,这个时候,普通的哈希算法就会导致一个键值的实际存储定位失败,定向到其他的机器上去,引起访问命中率下降以及数据大量迁移,对网络和存储都造成比较大的负担。
一个设计良好的分布式哈希方案应该具有良好的单调性,即服务节点的变更不会造成大量的哈希重定位。一致性哈希算法由此而生。
一致性哈希算法在1997年由麻省理工学院的Karger等人在解决分布式Cache中提出的,设计目标是为了解决因特网中的热点(Hot spot)问题。简单来说,一致性哈希将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-2^32-1(即哈希值是一个32位无符号整形),整个空间按顺时针方向组织。0和2^32-1在零点中方向重合。下一步将各个服务器使用Hash进行一个哈希,具体可以选择服务器的ip或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置,这里假设将上文中四台服务器使用ip地址哈希后在环空间的位置如下:
现假设Node C不幸宕机,可以看到此时对象A、B、D不会受到影响,只有C对象被重定位到Node D。一般的,在一致性哈希算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。综上所述,一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
另外,一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题。例如系统中只有两台服务器,为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。各个虚拟节点交叉排列,从而可以在实际存储层面很好的消除数据倾斜问题。
简而言之,一致性哈希算法通过提供循环区间归属判断而非单个值的精确映射,来实现方便的支持节点加入和退出,防止数据大规模的移动,非常适合分布式不稳定的环境。
五 应用
哈希算法的应用很广泛。
经典的应用比如编程语言中的哈希表,例如 Java的 HashMap, C++中的 unordered_map,。
比如用来做唯一标识符,类似于UUID,比如文本版本管理工具Git的每一次提交哈希码。
比如加密算法,密码在数据库中的存储为了安全起见就必须使用加密算法,而md5加密算法就是一类哈希算法。
再有就是数据校验,比如从各种镜像网站下载文件,需要检查文件是否被篡改,因此需要用原始的文件做一个哈希编码。
分布式的哈希算法还可以用做负载均衡和数据分片,比较经典的比如一致性哈希算法。
六 相关工作
TianTianUp在他的文章中介绍了哈希算法的定义,并且介绍了一些著名的哈希算法,哈希算法的应用,同时就哈希算法在编程比赛中的应用列举了一些常用的题目[1]。
notfresh在他的项目中利用MurmurHash算法实现了一个短网址应用,并且给出了详细的流程解释和实现代码[2]。
Cyeam在他的博客中详细介绍了从C语言到Java到Go的内置字符串哈希算法的具体原理,些介绍了一些著名的数字哈希算法,比如MurmurHash,memHash, crc32, 利用代码检测了哈希算法的性能。[3] [4]
拭心在他的博客中比较通俗的回顾了经典哈希算法的入门知识,讲了哈希算法的本质以及避免哈希冲突的方法[6]。
mcxtzhang在他的博客中详细的分析了Java中HashMap的实现,从源码级别进行了详细的分析,并且给出了形象的流程图[7]。
小天秤在他的文章中以电话号码查找,基于C语言实现,给出了一套小而形象的的例子来帮助解释哈希算法的用途[8]。
敖丙在他的文章中介绍了利用哈希算法实现布隆过滤器的原理[9]。
AskHarries在他的文章中介绍了一致性哈希算法的论文原著,一致性哈希算法的特性, 以及一致性哈希算法的Java实现[10]。
广州芦苇科技在他的博客中通过一个形象的例子,即Redis作为缓存的应用场景,引入了一致性哈希算法在存储领域发挥负载均衡的作用[11]。
参考
[1]TianTianUp,「算法与数据结构」带你看哈希算法之美,juejin.cn/post/687470…
[2]notfresh, 短网址应用, github.com/notfresh/sh…
[3]Cyeam,常见的哈希算法和用途,blog.cyeam.com/hash/2018/0…
[4]Cyeam,Cyeam的个人博客,www.cyeam.com/
[5]烟草的香味,哈希算法的用途,juejin.cn/post/696068…
[6]拭心,数据结构书本上的关于hash的知识点, blog.csdn.net/u011240877/…
[7]mcxtzhang,面试必备:Java8的HashMap源码解析,juejin.cn/post/684490…
[8]小天秤,哈希查找算法 , juejin.cn/post/684490…
[9]敖丙,布隆过滤器,juejin.cn/post/684490…
[10]AskHarries, 一致性哈希的提出、形式化定义特征以及实现,juejin.cn/post/684490…
[11]广州芦苇科技,5分钟理解一致性哈希算法,juejin.cn/post/684490…
[12]SpringSun,MurmurHash算法,juejin.cn/post/703134…
[13]_自在飞花_,Redis Rehash的过程,juejin.cn/post/702775…