在检索技术中,索引一直需要研究的核心技术。当下,索引技术主要分为三类:基于树的索引技术(tree-based index)、基于哈希的索引技术(hashing-based index)与基于词的倒排索引(visual words based inverted index)。在检索中,需要解决的问题是给定一个查询样本query,返回与此query相似的样本,线性搜索耗时耗力,不能承担此等重任,要想快速找到结果,必须有一种方法可以将搜索空间控制到一个可以接受的范围,哈希在检索中就是承担这样的任务.

1、什么是Hash

Hash也称散列、哈希,对应的英文都是Hash。基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出。这个映射的规则就是对应的Hash算法,而原始数据映射后的二进制串就是哈希值。活动开发中经常使用的MD5和SHA都是历史悠久的Hash算法。

echo md5("这是一个测试文案");
// 输出结果:2124968af757ed51e71e6abeac04f98d

在这个例子里,这是一个测试文案是原始值,2124968af757ed51e71e6abeac04f98d 就是经过hash算法得到的Hash值。整个Hash算法的过程就是把原始任意长度的值空间,映射成固定长度的值空间的过程。

2、Hash的特点

一个优秀的hash算法,需要什么样的要求呢?

  • a) 从hash值不可以反向推导出原始的数据
    这个从上面MD5的例子里可以明确看到,经过映射后的数据和原始数据没有对应关系
  • b) 输入数据的微小变化会得到完全不同的hash值,相同的数据会得到相同的值
echo md5("这是一个测试文案");// 输出结果:2124968af757ed51e71e6abeac04f98d
echo md5("这是二个测试文案");// 输出结果:bcc2a4bb4373076d494b2223aef9f702

          可以看到我们只改了一个文字,但是整个得到的hash值产生了非常大的变化。

  • c)、哈希算法的执行效率要高效,长的文本也能快速地计算出哈希值
  • d)、hash算法的冲突概率要小
    由于hash的原理是将输入空间的值映射成hash空间内,而hash值的空间远小于输入的空间。根据抽屉原理,一定会存在不同的输入被映射成相同输出的情况。那么作为一个好的hash算法,就需要这种冲突的概率尽可能小。抽屉原理:桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。这一现象就是我们所说的“抽屉原理”。抽屉原理的一般含义为:“如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定有一个集合里至少有两个元素。” 抽屉原理有时也被称为鸽巢原理。它是组合数学中一个重要的原理。

3、Hash碰撞的解决方案

       前面提到了hash算法是一定会有冲突的,那么如果我们如果遇到了hash冲突需要解决的时候应该怎么处理呢?比较常用的算法是链地址法开放地址法

3.1 链地址法

       链表地址法是使用一个链表数组,来存储相应数据,当hash遇到冲突的时候依次添加到链表的后面进行处理。

<img src="https://s2.51cto.com/images/blog/202307/12064400_64addb3070c6630754.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=" data-captinotallow="" data-size="normal" data-rawwidth="1080" data-rawheight="281" data-default-watermark-src="https://pic2.zhimg.com/50/v2-5162734951b33321b120ab1a72428676_hd.jpg?source=1940ef5c" width="1080" data-original="https://pic2.zhimg.com/v2-976fe3afe0d50dbafa97ae47f6f6a53b_r.jpg?source=1940ef5c">

hash索引 mongodb 唯一 hash索引原理_数据

链地址在处理的流程如下:
添加一个元素的时候,首先计算元素key的hash值,确定插入数组中的位置。如果当前位置下没有重复数据,则直接添加到当前位置。当遇到冲突的时候,添加到同一个hash值的元素后面,行成一个链表。这个链表的特点是同一个链表上的Hash值相同。java的数据结构HashMap使用的就是这种方法来处理冲突,JDK1.8中,针对链表上的数据超过8条的时候,使用了红黑树进行优化。由于篇幅原因,这里不深入讨论相关数据结构,有兴趣的同学可以参考这篇文章:

《Java集合之一—HashMap》

3.2 开放地址法

开放地址法是指大小为 M 的数组保存 N 个键值对,其中 M > N。我们需要依靠数组中的空位解决碰撞冲突。基于这种策略的所有方法被统称为“开放地址”哈希表。线性探测法,就是比较常用的一种“开放地址”哈希表的一种实现方式。线性探测法的核心思想是当冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。简单来说就是:一旦发生冲突,就去寻找下 一个空的散列表地址,只要散列表足够大,空的散列地址总能找到。

线性探测法的数学描述是:h(k, i) = (h(k, 0) + i) mod m,i表示当前进行的是第几轮探查。i=1时,即是探查h(k, 0)的下一个;i=2,即是再下一个。这个方法是简单地向下探查。mod m表示:到达了表的底下之后,回到顶端从头开始。

对于开放寻址冲突解决方法,除了线性探测方法之外,还有另外两种比较经典的探测方法,二次探测(Quadratic probing)和双重散列(Double hashing)。但是不管采用哪种探测方法,当散列表中空闲位置不多的时候,散列冲突的概率就会大大提高。为了尽可能保证散列表的操作效率,一般情况下,我们会尽可能保证散列表中有一定比例的空闲槽位。我们用装载因子(load factor)来表示空位的多少。

散列表的装载因子=填入表中的元素个数/散列表的长度。装载因子越大,说明冲突越多,性能越差。

3.3 两种方案的demo示例

假设散列长为8,散列函数H(K)=K mod 7,给定的关键字序列为{32,14,23,2, 20}
当使用链表法时,相应的数据结构如下图所示:

<img src="https://s2.51cto.com/images/blog/202307/12064401_64addb313830251621.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=" data-captinotallow="" data-size="normal" data-rawwidth="1080" data-rawheight="741" data-default-watermark-src="https://pic1.zhimg.com/50/v2-e7f048fa3c6e75b749573e5e493d20fd_hd.jpg?source=1940ef5c" width="1080" data-original="https://pic1.zhimg.com/v2-dabac86f74bb61605f93258a01917590_r.jpg?source=1940ef5c">

hash索引 mongodb 唯一 hash索引原理_数据_02

当使用线性探测法时,相应的数据结果如下图所示:

<img src="https://s2.51cto.com/images/blog/202307/12064401_64addb31a89fe93638.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=" data-captinotallow="" data-size="normal" data-rawwidth="1080" data-rawheight="253" data-default-watermark-src="https://pic1.zhimg.com/50/v2-784c2f1078118ed29ff6c1a301759c98_hd.jpg?source=1940ef5c" width="1080" data-original="https://pic4.zhimg.com/v2-cf46c1e0d22be22882ebc1974ae3c2cf_r.jpg?source=1940ef5c">

hash索引 mongodb 唯一 hash索引原理_hash索引 mongodb 唯一_03

这里的两种算法的区别是2这个元素,在链表法中还是在节点2的位置上,但是在线性探测法遇到冲突时会将冲突数据放到下一个空的位置下面。

4、hash算法在日常活动中的应用

在日常运营活动中,我们活动开发经常遇到的应用场景是信息加密、数据校验、负载均衡。下面分别对这三种应用场景进行讲解。

4.1 信息加密

首先我们看一下信息加密的应用。作为用户的非常隐私的信息,最简单的保护措施就是对密码进行hash加密。在客户端对用户输入的密码进行hash运算,然后在服务端的数据库中保存用户密码的hash值。由于服务器端也没有存储密码的明文,所以目前很多网站也就不再有找回密码的功能了。

看到这里有些同学会觉得那么我们是不是对用户输入的密码进行一次MD5加密就可以了呢,这样就算恶意用户知道了hash值,也没有办法拿到用户的真实密码。假设用户的密码是123456789,经过一次md5以后得到的值是:25f9e794323b453885f5181f1b624d0b

那么是不是使用了这个加密后的字符串来存密码就万无一失了呢,理想总是很丰满,而现实总是很骨感的。

大家可以看一下这个网站:https://www.cmd5.com/

这里是该网站的相关介绍:

本站针对md5、sha1等全球通用公开的加密算法进行反向查询,通过穷举字符组合的方式,创建了明文密文对应查询数据库,创建的记录约90万亿条,占用硬盘超过500TB,查询成功率95%以上,很多复杂密文只有本站才可查询。已稳定运行十余年,国内外享有盛誉

<img src="https://s2.51cto.com/images/blog/202307/12064402_64addb322980f10898.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=" data-captinotallow="" data-size="normal" data-rawwidth="1080" data-rawheight="345" data-default-watermark-src="https://pic4.zhimg.com/50/v2-b1165f12b8f4d0fbd71acc4d83559782_hd.jpg?source=1940ef5c" width="1080" data-original="https://pic2.zhimg.com/v2-0254fb3b64eefecac297c6c6b4c1fb15_r.jpg?source=1940ef5c">

hash索引 mongodb 唯一 hash索引原理_链表_04

那么针对这种可以被反向破解的问题,我们的解决之道就是引入salt(加盐),即利用特殊字符(盐)和用户的输入合在一起组成新的字符串进行加密。通过这样的方式,增加了反向查询的复杂度。但是这样的方式也不是万无一失,如果发生了盐被泄露的问题,就需要所有用到的地方来重置密码。

针对salt泄露的问题,其实还有一种解决办法,即使用HMAC进行加密(Hash-based Message Authentication Code)。这种算法的核心思路是加密使用的key是从服务器端获取的,每一个用户的是不一样的。如果发生了泄露,那么也就是这一个用户的会被泄露,不会影响到全局。