码迷,mamicode.com
首页 > 其他好文 > 详细

什么是 hash?

时间:2021-06-18 19:01:19      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:uri   数据校验   font   extra   parent   ros   logs   pen   border   

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算法,就需要这种冲突的概率尽可能小

3、Hash碰撞的解决方案

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

3.1 链地址法

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

技术图片

 

 链地址在处理的流程如下:

添加一个元素的时候,首先计算元素key的hash值,确定插入数组中的位置。如果当前位置下没有重复数据,则直接添加到当前位置。当遇到冲突的时候,添加到同一个hash值的元素后面,行成一个链表。这个链表的特点是同一个链表上的Hash值相同。java的数据结构HashMap使用的就是这种方法来处理冲突,JDK1.8中,针对链表上的数据超过8条的时候,使用了红黑树进行优化。

一个优秀的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遇到冲突的时候依次添加到链表的后面进行处理。

技术图片

链地址在处理的流程如下:
添加一个元素的时候,首先计算元素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}
当使用链表法时,相应的数据结构如下图所示:

技术图片

 

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

技术图片

 

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

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

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

4.1 信息加密

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

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

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

25f9e794323b453885f5181f1b624d0b

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

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

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

4.2 数据校验

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

19d02d2cc358e59b3d04f82677dbf3808ae4fc40

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

printf "commit %s\0" $(git cat-file commit HEAD | wc -c); git cat-file commit HEAD

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

技术图片

 

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

4.3 负载均衡

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

技术图片

 

 

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

下面我们来看一点复杂的问题,假设我们活动初始分表了10张,运营一段时间以后发现需要10张不够,需要改到100张。这个时候我们如果直接扩容的话,那么所有的数据都需要重新计算Hash值,大量的数据都需要进行迁移。如果更新的是缓存的逻辑,则会导致大量缓存失效,发生雪崩效应,导致数据库异常。造成这种问题的原因是hash算法本身的缘故,只要是取模算法进行处理,则无法避免这种情况。针对这种问题,我们就需要利用一致性hash进行相应的处理了


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


技术图片

 

 

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

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

  • 服务器缩容
技术图片

 

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

  • 服务器扩容

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

技术图片

 

 

  • 虚拟节点
当然这只是一种理想情况,实际使用中,由于服务器节点数量有限,有可能出现分布不均匀的情况。这个时候会出现大量数据都被映射到某一台服务器的情况,如下图左侧所示。为了解决这个问题,我们采用了虚拟节点的方案。虚拟节点实际节点(实际的物理服务器)在hash环上的复制品,一个实际节点可以对应多个虚拟节点。虚拟节点越多,hash环上的节点就越多,数据被均匀分布的概率就越大。
技术图片

 

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

 

什么是 hash?

标签:uri   数据校验   font   extra   parent   ros   logs   pen   border   

原文地址:https://www.cnblogs.com/tracydzf/p/14894425.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!