hash算法原理及应用漫谈【加图版】

原文:http://www.javashuo.com/article/p-geygixkm-ge.htmljava

提到hash,相信大多数同窗都不会陌生,以前很火如今也依旧很火的技术区块链背后的底层原理之一就是hash,下面就从hash算法的原理和实际应用等几个角度,对hash算法进行一个讲解。git

一、什么是Hash算法

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

echo md5(“这是一个测试文案”);数组

// 输出结果:2124968af757ed51e71e6abeac04f98d缓存

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

二、Hash的特色服务器

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

  • a)、从hash值不能够反向推导出原始的数据
  • 这个从上面MD5的例子里能够明确看到,通过映射后的数据和原始数据没有对应关系
  • b)、输入数据的微小变化会获得彻底不一样的hash值,相同的数据会获得相同的值echo md5(“这是一个测试文案”);
  • // 输出结果:2124968af757ed51e71e6abeac04f98d
  • echo md5(“这是二个测试文案”);
  • // 输出结果:bcc2a4bb4373076d494b2223aef9f702
  • 能够看到咱们只改了一个文字,可是整个获得的hash值产生了很是大的变化。
  • c)、哈希算法的执行效率要高效,长的文本也能快速地计算出哈希值
  • d)、hash算法的冲突几率要小因为hash的原理是将输入空间的值映射成hash空间内,而hash值的空间远小于输入的空间。根据抽屉原理,必定会存在不一样的输入被映射成相同输出的状况。那么做为一个好的hash算法,就须要这种冲突的几率尽量小。

桌上有十个苹果,要把这十个苹果放到九个抽屉里,不管怎样放,咱们会发现至少会有一个抽屉里面放很多于两个苹果。这一现象就是咱们所说的“抽屉原理”。抽屉原理的通常含义为:“若是每一个抽屉表明一个集合,每个苹果就能够表明一个元素,假若有n+1个元素放到n个集合中去,其中一定有一个集合里至少有两个元素。” 抽屉原理有时也被称为鸽巢原理。它是组合数学中一个重要的原理数据结构

三、Hash碰撞的解决方案

前面提到了hash算法是必定会有冲突的,那么若是咱们若是遇到了hash冲突须要解决的时候应该怎么处理呢?比较经常使用的算法是链地址法和开放地址法。

3.1 链地址法

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

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}

当使用链表法时,相应的数据结构以下图所示:

hash 算法原理及应用漫谈

链表法demo

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

hash 算法原理及应用漫谈

开放地址-线性探测法

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

四、hash算法在平常活动中的应用

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

4.1 信息加密

首先咱们看一下信息加密的应用。2011年CSDN脱库事件,致使超过600W的用户的密码泄露,让人失望的是,CSDN是明文存储用户的注册邮箱和密码的。做为用户的很是隐私的信息,最简单的保护措施就是对密码进行hash加密。在客户端对用户输入的密码进行hash运算,而后在服务端的数据库中保存用户密码的hash值。因为服务器端也没有存储密码的明文,因此目前不少网站也就再也不有找回密码的功能了。

  • 这里也友情提示一下你们:若是在使用中发现某网站还有提供找回密码的功能,就要好好担忧下这个网站的安全性了。

看到这里有些同窗会以为那么咱们是否是对用户输入的密码进行一次MD5加密就能够了呢,这样就算恶意用户知道了hash值,也没有办法拿到用户的真实密码。假设用户的密码是123456789,通过一次md5之后获得的值是:

25f9e794323b453885f5181f1b624d0b

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

你们能够看一下这个网站:

https://www.cmd5.com/

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

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

hash 算法原理及应用漫谈

md5反查结果

那么通常针对这种问题,咱们的解决之道就是引入salt(加盐),即利用特殊字符(盐)和用户的输入合在一块儿组成新的字符串进行加密。经过这样的方式,增长了反向查询的复杂度。可是这样的方式也不是万无一失,若是发生了盐被泄露的问题,就须要全部用到的地方来重置密码。

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

这里也留给你们一个思考点,若是恶意用户直接抓取了你的活动参与连接,也就是拿到了你计算后的hash值,那从技术的角度上说,咱们还有没有其余能够提高恶意用户的违法成本呢?

4.2 数据校验

– git commit id

使用过git的同窗都应该清楚,每次git提交后都有一个commit id,好比:

19d02d2cc358e59b3d04f82677dbf3808ae4fc40

就是一次git commit的结果,那么这个id是如何生成出来的呢?查阅了相关资料,使用以下代码能够进行查看:

printf “commit %s0” $(git cat-file commit HEAD | wc -c); git cat-file commit HEAD

git的commit id主要包括了如下几部份内容:Tree 哈希,parent哈希、做者信息和本次提交的备注。

hash 算法原理及应用漫谈

单次git commit相关信息

针对这些信息进行SHA-1 算法后获得值就是本次提交的commit id。简单来说,就是对于单次提交的头信息的一个校验和。

Linux kernel开创者和Git的开发者——Linus说,Git使用了sha1并不是是为了安全性,而是为了数据的完整性;它能够保证,在不少年后,你从新checkout某个commit时,必定是它多年前的当时的状态,彻底一摸同样,彻底值得信任。

但最新研究代表,理论上对其进行哈希碰撞(hash collision,不一样的两块数据有相同的hash值)的攻击能够在2^51(2的51次方)左右的次数内实现。不过因为commit id 是针对单个仓库里的,因此实际应用中咱们能够认为若是两个文件的SHA-1值是相同的,那么它们确是彻底相同的内容。

注:对于git里tree、parent等结构感兴趣的同窗,能够参考下这篇文章《Git 内部原理 – Git 对象》,这里因为篇幅缘由就不进行深刻分析了。

  • 版权校验
  • 在数据校验方面的另外一个应用场景就是版权的保护或者违禁信息的打击,好比某个小视频,第一个用户上传的时候,咱们认为是版权全部者,计算一个hash值存下来。当第二个用户上传的时候,一样计算hash值,若是hash值同样的话,就算同一个文件。这种方案其实也给用户传播违禁文件提升了一些门槛,不是简单的换一个名字或者改一下后缀名就能够躲避掉打击了。(固然这种方式也是能够绕过的,图片的你随便改一下颜色,视频去掉一帧就又是彻底不一样的hash值了。注意:我没有教你变坏,我只是和你在讨论这个技术。。。)另外咱们在社区里,也会遇到玩家重复上传同一张图片或者视频的状况,使用这种校验的方式,能够有效减小cos服务的存储空间。
  • 大文件分块校验
  • 使用过bt的同窗都有经验,在p2p网络中会把一个大文件拆分红不少小的数据各自传输。这样的好处是若是某个小的数据块在传输过程当中损坏了,只要从新下载这个块就好。为了确保每个小的数据块都是发布者本身传输的,咱们能够对每个小的数据块都进行一个hash的计算,维护一个hash List,在收到全部数据之后,咱们对于这个hash List里的每一块进行遍历比对。这里有一个优化点是若是文件分块特别多的时候,若是遍历对比就会效率比较低。能够把全部分块的hash值组合成一个大的字符串,对于这个字符串再作一次Hash运算,获得最终的hash(Root hash)。在实际的校验中,咱们只须要拿到了正确的Root hash,便可校验Hash List,也就能够校验每个数据块了。
hash 算法原理及应用漫谈

大文件分块示意图

4.3 负载均衡

活动开发同窗在应对高星级业务大用户量参与时,都会使用分库分表,针对用户的openid进行hashtime33取模,就能够获得对应的用户分库分表的节点了。

hash 算法原理及应用漫谈

活动分库分表示意图

如上图所示,这里实际上是分了10张表,openid计算后的hash值取模10,获得对应的分表,在进行后续处理就好。对于通常的活动或者系统,咱们通常设置10张表或者100张表就好。

下面咱们来看一点复杂的问题,假设咱们活动初始分表了10张,运营一段时间之后发现须要10张不够,须要改到100张。这个时候咱们若是直接扩容的话,那么全部的数据都须要从新计算Hash值,大量的数据都须要进行迁移。若是更新的是缓存的逻辑,则会致使大量缓存失效,发生雪崩效应,致使数据库异常。形成这种问题的缘由是hash算法自己的缘故,只要是取模算法进行处理,则没法避免这种状况。针对这种问题,咱们就须要利用一致性hash进行相应的处理了。

一致性hash的基本原理是将输入的值hash后,对结果的hash值进行2^32取模,这里和普通的hash取模算法不同的点是在一致性hash算法里将取模的结果映射到一个环上。将缓存服务器与被缓存对象都映射到hash环上之后,从被缓存对象的位置出发,沿顺时针方向遇到的第一个服务器,就是当前对象将要缓存于的服务器,因为被缓存对象与服务器hash后的值是固定的,因此,在服务器不变的状况下,一个openid一定会被缓存到固定的服务器上,那么,当下次想要访问这个用户的数据时,只要再次使用相同的算法进行计算,便可算出这个用户的数据被缓存在哪一个服务器上,直接去对应的服务器查找对应的数据便可。这里的逻辑其实和直接取模的是同样的。以下图所示:

hash 算法原理及应用漫谈

初始3台机器的状况

初始状况以下:用户1的数据在服务器A里,用户二、3的数据存在服务器C里,用户4的数据存储在服务器B里

下面咱们来看一下当服务器数量发生变化的时候,相应影响的数据状况:

  • 服务器缩容
hash 算法原理及应用漫谈

服务器缩容

服务器B发生了故障,进行剔除后,只有用户4的数据发生了异常。这个时候咱们须要继续按照顺时针的方案,把缓存的数据放在用户A上面。

  • 服务器扩容
  • 一样的,咱们进行了服务器扩容之后,新增了一台服务器D,位置落在用户2和3之间。按照顺时针原则,用户2依然访问的是服务器C的数据,而用户3顺时针查询后,发现最近的服务器是D,后续数据就会存储到d上面。
hash 算法原理及应用漫谈

服务器扩容示意图

  • 虚拟节点
  • 固然这只是一种理想状况,实际使用中,因为服务器节点数量有限,有可能出现分布不均匀的状况。这个时候会出现大量数据都被映射到某一台服务器的状况,以下图左侧所示。为了解决这个问题,咱们采用了虚拟节点的方案。虚拟节点是实际节点(实际的物理服务器)在hash环上的复制品,一个实际节点能够对应多个虚拟节点。虚拟节点越多,hash环上的节点就越多,数据被均匀分布的几率就越大。
hash 算法原理及应用漫谈

虚拟节点示意图

 

 

如右图所示,B、C、D 是原始节点复制出来的虚拟节点,本来都要访问机器D的用户一、4,分别被映射到了B,D。经过这样的方式,起到了一个服务器均匀分布的做用。

五、几种hash算法的扩展应用

下面介绍几种你们可能不常常遇到的应用,因为篇幅缘由,不作深刻介绍,只抛砖引玉。

5.1 SimHash

simHash是google用于海量文本去重的一种方法,它是一种局部敏感hash。那什么叫局部敏感呢,假定两个字符串具备必定的类似性,在hash以后,仍然能保持这种类似性,就称之为局部敏感hash。普通的hash是不具备这种属性的。simhash被Google用来在海量文本中去重。

simHash算法的思路大体以下:

  • 将Doc进行关键词抽取(其中包括分词和计算权重),抽取出n个(关键词,权重)对, 即图中的多个(feature, weight)。记为 feature_weight_pairs = [fw1, fw2 … fwn],其中 fwn = (feature_n,weight_n)。
  • 对每一个feature_weight_pairs中的feature进行hash。而后对hash_weight_pairs进行位的纵向累加,若是该位是1,则+weight,若是是0,则-weight,最后生成bits_count个数字,大于0标记1,小于0标记0
  • 最后转换成一个64位的字节,判断重复只须要判断他们的特征字的距离是否是
hash 算法原理及应用漫谈

SimHash计算流程

以下图所示,当两个文本只有一个字变化时,若是使用普通Hash则会致使两次的结果发生较大改变,而SimHash的局部敏感特性,会致使只有部分数据发生变化。

hash 算法原理及应用漫谈

SimHash结果

5.2 GeoHash

GeoHash将地球做为为一个二维平面进行递归分解。每一个分解后的子块在必定经纬度范围内拥有相同的编码。如下图为例,这个矩形区域内全部的点(经纬度坐标)都共享相同的GeoHash字符串,这样既能够保护隐私(只表示大概区域位置而不是具体的点),又比较容易作缓存。

hash 算法原理及应用漫谈

GeoHash示意图

下面以一个例子来理解下这个算法,咱们对纬度39.3817进行逼近编码 :

  • 地球纬度区间是[-90,90],对于这个区间进行二分划分左区间[-90,0), 右区间[0,90]。39.3817属于右区间,标记为1
  • 将右区间[0,90]继续进行划分,左区间[0,45) ,右区间[45,90]。39.3817属于左区间,标记为0
  • 递归上面的过程,随着每次迭代,区间[a,b]会不断接近39.3817。递归的次数决定了生成的序列长度。
  • 对于经度作一样的处理。获得的字符串,偶数位放经度,奇数位放纬度,把2串编码组合生成新串。对于新串转成对应10进制查出实际的base32编码就是相似WX4ER的hash值。

总体递归过程以下表所示:

hash 算法原理及应用漫谈

这里有一篇文章详细介绍了GeoHash,有兴趣的同窗能够移步这里:

是什么能让 APP 快速精准定位到咱们的位置?

5.3 布隆过滤器

布隆过滤器被普遍用于黑名单过滤、垃圾邮件过滤、爬虫判重系统以及缓存穿透问题。对于数量小,内存足够大的状况,咱们能够直接用hashMap或者hashSet就能够知足这个活动需求了。可是若是数据量很是大,好比5TB的硬盘上放满了用户的参与数据,须要一个算法对这些数据进行去重,取得活动的去重参与用户数。这种时候,布隆过滤器就是一种比较好的解决方案了。

布隆过滤器实际上是基于bitmap的一种应用,在1970年由布隆提出的。它其实是一个很长的二进制向量和一系列随机映射函数,用于检索一个元素是否在一个集合中。它的优势是空间效率和查询时间都远远超过通常的算法,缺点是有必定的误识别率和删除困难,主要用于大数据去重、垃圾邮件过滤和爬虫url记录中。核心思路是使用一个bit来存储多个元素,经过这样的方式来减小内存的消耗。经过多个hash函数,将每一个数据都算出多个值,存放在bitmap中对应的位置上。

布隆过滤器的原理见下图所示:

hash 算法原理及应用漫谈

布隆过滤器原理示意

上图所示的例子中,数据a、b、c通过三次hash映射后,对应的bit位都是1,表示这三个数据已经存在了。而d这份数据通过映射后有一个结果是0,则代表d这个数据必定没有出现过。布隆过滤器存在假阳率(断定存在的元素可能不存在)的问题,可是没有假阴率(判断不存在的缘由可能存在)的问题。即对于数据e,三次映射的结果都是1,可是这份数据也可能没有出现过。

误判率的数据公式以下所示:

hash 算法原理及应用漫谈

其中,p是误判率,n是容纳的元素,m是须要的存储空间。由公示能够看出,布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。哈希函数的个数也须要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;可是若是太少的话,则会致使误报率升高。

六、总结

Hash算法做为一种活动开发常常遇到的算法,咱们在使用中不只仅要知道这种算法背后真正的原理,才能够在使用上作到有的放矢。Hash的相关知识还有不少,有兴趣的同窗能够继续深刻研究。

相关文章
相关标签/搜索