数据结构与算法系列16--哈希算法

什么的哈希算法?

将任意长度的二进制值串映射为固定长度的二进制值串,这个映射的规则就是哈希算法。而通过原始数据映射后得到的二进制值串就是哈希值。

一个优秀的哈希算法应该满足哪几点?

  • 从原始数据计算得到的哈希值,不能反向推导出原始数据的值。
  • 对输入的数据非常敏感,只要对原始数据做一点改变,小到一个Bit的改变,最后得到的哈希值都大不相同。
  • 散列冲突的概率要很小,对于不同的数据,哈希出来的哈希值相同的概率要非常非常之小。
  • 哈希算法的执行效率要高,对于较长的文本,也能快速计算出哈希值。

哈希算法的应用:

哈希算法的应用有很多,分别有安全加密、唯一标识、数据校验、散列函数、负载均衡、数据分片、分布式存储等。

应用一,安全加密:

最常用于加密的哈希算法是MD5(MD5 Message-Digest Algorithm,MD5 消息摘要算法)和SHA(Secure Hash Algorithm,安全散列算法)。
对于应用于加密的哈希算法来说,上面提到的几点要求中,有两项是非常重要的,就是

  • 从原始数据计算得到的哈希值,不能反向推导出原始数据的值。
  • 散列冲突的概率要很小,对于不同的数据,哈希出来的哈希值相同的概率要非常非常之小。

对于散列冲突的问题,事情上我们是无法完全避免的,只能尽可能的让这个概率变小。为什么无法完全避免呢 ?
这里是基于组合数学中一个非常基础的的理论,叫做鸽巢原理(也叫抽屉原理)。这个原理是说,如果有10个鸽巢,有11只鸽子,那肯定有1个鸽巢中的鸽子数量多于1个,换句话说就是,肯定有2只鸽子在1个鸽巢内。应用到这里是怎么理位解呢?我们知道,哈希值的长度是有限的,比如MD5,它的哈希值是固定的128位,能表示的数据范围有限,最大是2^128个数据,但我们知道现实世界中,可哈希的数据是无穷的,所以基于前面我们讲的鸽巣原理,如果我们对2 ^128+1个数据进行求哈希值,那就必然会存在哈希值相同的情况。说到这里,你应该可以明白,如果我们选取的哈希算法哈希出来的值越长(表示的数据越大),那它发生散列冲突的概率就会越低,比如SHA要比MD5哈希出来的值的长度要大。
有人可能会说,那这样还是有可能被破解的,确实,理论上是可以通过穷举的方法来找到哈希值相同的情况,但事实是即便存在这样的散列冲突,但是因为哈希值的范围非常大,冲突的概率极低,即便像MD5这样的哈希算法,有2^128个数据,这个数据其实已经接近是一个天文数据了,散列冲突发生的概率要小于1/2 ^128,如果想通过穷举的方法来找到跟这个MD5值相同的数据,那耗费的时间将是一个天文数据,所以在有限的时间和资源下,哈希算法还是很难被破解的。
那是不是选择越复杂、越难破解的哈希算法就好呢?
并不是的,越复杂、越难破解的哈希算法,他们计算哈希值的时间就会长,比如SHA-256、SHA-1。因此我们在实际的开发中,要权衡好计算时长和破解难度来选择相应的加密算法。
最后再补充一下,没有绝对的安全加密,有的只是增加攻击的成本而已。

应用二,唯一标识

如何在海量图库中,搜索一张图片是否存在?
我们可以为每一张图片取一个唯一标识,或者说消息摘要。比如我们可以取一张图片的二进制码串的前100字节,中间100字节,再取最后100字节,然后将这300字节按顺序放到一块,通过哈希算法(比如使用MD5)计算这300字节的哈希值,用这个这个哈希值来做唯一标识,通过这个标识来判断这个图片是否存在图库中,这样可以减少很多工作量。

应用三,散列函数

前面我们讲到,散列函数是设计一个散列表的关键,他直接关系到散列冲突的概率和散列的性能。不过,相对哈希算法的其他应用,散列函数对散列冲突的要求要低得多。即便个别出现散列冲突,我们也可以通过开发寻址法或者链表法来解决。另外散列函数对于散列算法计算得到的值是否可以逆向推导出原始数据也没有要求,它更加关注散列后的值是否会平均分布。而且散列函数对于性能要求比较高,更加追求效率。

如何防止数据库中的用户信息被“脱库”?

我们可以应用上面讲到的哈希算法,数据库中对于用户密码这样的敏感信息,我们可以对用户提交的明文的密码进行哈希运算后,再将密码的哈希值储存到数据库中,这样即使被“脱库”,黑客拿到的也只是一串哈希值而已。
上面那样还不够安全?
事实上,很多用户的密码设置得比较简单,比如123456,111111,000000等,黑客可以通过维护一个常用密码的字典来进行猜测,这样简单的密码其实很容易被猜出来。针对这样的字典攻击,作为普通的用户群体,我们应该有意识的设置比较复杂的密码;而作为开发人员,我们可以引入一个salt(盐),跟用户的密码组合在一起,增加密码的复杂度,然后将组合后的密码通过哈希算法运算加密,再将他储存到数据库中,进一步增加破解的难度。

哈希算法在分布式系统的应用

应用四,负载均衡

负载均衡算法(或者说是调度策略)有很多,比如随机分配、轮询、加权轮询等。那如果我们想实现一个会话粘滞的负载均衡算法呢?
比较直接的方法是,我们通过维护一张映射关系表,这张表的内容是客户端IP或者会话id和服务器编号对应的映射关系,每次客户端发起请求,我们拿到客户端的IP后,在映射表中找到此IP对应映射的服务器编号,然后再请求对应编号的服务器。这种方法虽然简单,但是有几个弊端:

  • 如果客户端数量非常大,那映射关系表也会非常大,非常浪费内存空间。
  • 客户端上线,下线,服务器扩容,缩容都会导致映射关系失效,这样维护一个映射关系表的成本就会非常大。

如果我们借助哈希算法,就可以解决上面两个弊端。具体怎么做呢?
我们运用哈希算法对客户端ip或者会话id进行求哈希值,然后将这个哈希值与服务器列表长度进行取模运算,得到的结果就是应该被路由到的服务器编号。这样我们就可以把同一个ip过来的所有请求都路由到后端同一台服务器上。

应用五,数据分片

例子:怎样统计用户‘搜索‘关键字’出现的次数?
假设现在我们有一个1T的文件,里面记录着用户的搜索关键字的记录,现在我们要统计出每个关键字出现的次数?要怎么统计呢?
首先,1T的文件这么大,我们不可能放在一台机器的内存中,而且用一台机器来处理这么大的数据肯定也不行,那样耗费的时间会非常长。针对这两个问题,我们可以将数据进行分片处理,放到多台机器上分别处理,这样效率就会大大提高。
具体做法:假如我们现在有4台机器,我们先从搜索记录的文件中依次读取出每个搜索关键字,然后通过哈希算法计算此关键字的哈希值,将此哈希值与机器个数(这里是4)进行取模运算,得到的结果就是要被分配到的机器的编号。这样相同的关键字就会被分配到同一台机器上进行处理,每个机器分别统计关键词出现的次数,最后将各个机器计算的结果合并到一起就是我们最终要得到的结果。其实这里应用到的思想就是MapReduce的基本设计思想。
类似处理海量数据的例子还有很多,针对类问题,我们都可以采用多机分布式处理,借助这种分片思路,可以突破单机内存,CPU等资源的限制。

应用六,分布式储存

现如今的互联网面对的是海量的数据,为了进一步提高数据读取和写入的速度,我们一般都采用分布式储存的方式来储存数据,比如分布式缓存。我们缓存的数据也是非常大的,所以我们也要将缓存数据储存到多个机器上。
那我们怎么分配好数据? 我们可以借助前面讲到的数据分片的思想,通过哈希算法对数据求哈希值,然后与机器的个数做取模运算,得到的值就是要分配给的机器编号。
但是这里有个问题,如果我们想要增加机器的数量了,那这个时候就很麻烦了,我们所有的数据都要重新计算哈希值,然后重新搬移到新的机器上,这样之前缓存的数据就一下子都失效了。关于这个问题,我们可以使用一致性哈希算法进行解决,具体一致性哈希算法是什么,已经如何解决这个问题,大家可以看看这篇文章,个人觉得讲得通俗易懂。

猜你喜欢

转载自blog.csdn.net/qq_34493908/article/details/84112670