- 哈希就是把任意长度的输入,通过散列算法,变换成固定长度的输出
- 如果两个散列值是不相同的,那么这两个散列值的原始输入也是不相同的
- 如果两个散列值相同,两个输入值很可能是相同的,但也可能不同(哈希平旷)
- 好的哈希函数的特点
- 单向性
- 不可逆,通过输出无法逆推出源数据
- 雪崩效应
- 在输入发生微小变化时,输出截然不同
- 抗冲突
- 产生2个相同的散列值是概率低,即对任意两个不同的x,y,使得h(x)=h(y)是困难的
- 单向性
- 常见的散列算法有:CRC-32、MD5、SHA-1,SM3, 像AES/DES/RSA是加解密算法不算散列函数
一般应用场景
- 快速查找,即构建哈希表
- 例如MurmurHash、FNV、xxhash算法
- 注重运算速度和哈希值分布均匀
- 为了避免哈希洪水攻击,很多算法还有随机性
- 即每次初始化哈希表时都有一个哈希种子
- 这会导致不同哈希表对同一个输入的输出不一样
- 例如使用Python 3.2以上,那么shell里运行hash(‘www’)后退出再运行 hash(‘www’),就会发现两次hash值不一样
- 参考:https://www.zhihu.com/question/286529973
- 消息签名,验证消息完整且未被篡改
- 电子签名、SSL公钥认证、资源授权等都会用到
- 例如MD5、SHA-1、SHA-256、CRC32算法
- 优先考虑抗碰撞性
- 长度越长,越难碰撞,因此安全性越好
- 参考生日攻击:http://www.ruanyifeng.com/blog/2018/09/hash-collision-and-birthday-attack.html
- 存储和验证密码
- 一般用MD5、SHA-256,重点是要加盐
- 否则容易被用彩虹表等破解
- 一定要对不同用户加不同的盐,否则只是增加了沉默成本,没有增加边际成本
- 参考: http://blog.sina.com.cn/s/blog_77e8d1350100wfc7.html
- MD5和SHA-1用作签名已经不安全了,但是用来存储密码还是比较安全的(加盐后)
- 消息签名主要用到了哈希的抗碰撞特性,王小云只是证明了MD5比较容易找到哈希值一样的输入,这导致签名不可信
- 而密码存储主要利用了哈希的不可逆性,现在还没有很好的办法通过输出得到输入
- 一般用MD5、SHA-256,重点是要加盐
布隆过滤器
- 一种概率型的数据结构,可以高效地插入和查询,能告诉你"某个数据一定不存在或可能存在
- 应用场景:
- 利用布隆过滤器减少磁盘 IO 或者网络请求
- 因为一旦一个值必定不存在的话,我们可以不用进行后续昂贵的查询请求
- 避免缓存击穿
- 将所有可能存在的数据缓存放到布隆过滤器中,当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉
- 垃圾邮件过滤
- 去重
- 利用布隆过滤器减少磁盘 IO 或者网络请求
- 优点:
- 需要内存极少,大量比特位被复用
- 比如一亿个数据,误判率万分之一的前提下,只需要250多MB内存
- 插入和查询很快
- 为了提高插入和查询速度,应该选择性能较高的哈希函数
- 例如 MurmurHash、Fnv
- 为了提高插入和查询速度,应该选择性能较高的哈希函数
- 需要内存极少,大量比特位被复用
- 缺点:
- 存在误判
- 判断某个数据不存在,那就一定不存在
- 但判断某个数据存在,可能并不一定存在,只是刚好哈希算法计算出来的位和某数据相同
- 布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小
- 不支持删除操作
- 存在误判
- 当需要的哈希函数很多时,设计k个独立的hash function是不现实并且困难的
- 对于一个输出范围很大的hash function(例如MD5产生的128 bits数),如果不同bit位的相关性很小,则可把此输出分割为k份
- 或者可将k个不同的初始值(例如0,1,2, … ,k-1)结合元素,feed给一个hash function从而产生k个不同的数
- 误判率是可以通过哈希函数的个数(k)和布隆过滤器数组长度(m) 控制的
GEO哈希
- GeoHash将二维的经纬度转换成字符串
- 字符串越长,表示的范围越精确
- 字符串相似的表示距离相近,这样可以利用字符串的前缀匹配来查询
- 基本原理是将地球理解为一个二维平面,将平面递归分解成更小的子块,每个子块在一定经纬度范围内拥有相同的编码
一致性哈希
- 一致性hash是尽可能减少在rehashing过程中进行数据迁移的算法
- 但是平均还是有
K/N
个可以需要rehash- K是表中key的个数,N是槽的个数
- 扩缩容或者节点失效(服务器崩溃) 都可能导致rehashing
- 防止可能的停机或性能问题
- 但是平均还是有
- 传统的hash算法的问题在于每次扩容和缩容是都会导致所有数据分布的重新计算
- 例如分布式存储系统,文件通过hash算法决定分布的机器
- 如果使用传统hash算法每次增加机器都要停下来等所有文件重新分布才能继续服务,而一台机器掉线后所有数据的访问都会出现问题
- 整个服务无法平滑扩缩容,成为了有状态服务
- 一致性哈希算法的原理就是设置虚拟桶
- 哈希算法还是同样的取模,只不过现在分桶分到的很可能是不存在的虚拟桶,那么就往下找找到第一个真实桶(存储服务器)放进去
- 也可以选择距离这个数字最近的桶
- 这样增加一个机器只需要和他后面的机器同步一下数据就可以开始工作了,下线一个机器只需要先把他的数据同步到后面一台机器再下线
- 实现中可以让每台机器同步一份自己前面机器的数据,这样即使掉线也不会影响这一部分的数据服务
- 哈希算法还是同样的取模,只不过现在分桶分到的很可能是不存在的虚拟桶,那么就往下找找到第一个真实桶(存储服务器)放进去
- 用一致性哈希还能实现部分的分布式系统无锁化,每个任务有自己的编号,由于哈希算法的确定性,分到哪个桶也是确定的就不存在争抢,也就不需要分布式锁了
时间复杂度
- 既然一致性哈希有这么多好的特性,那为啥主流的哈希都是非一致的呢?
- 主要一个原因在于查找效率上,普通的哈希查询一次哈希计算就可以找到对应的桶了,算法时间复杂度是 O(1),而一致性哈希需要将排好序的桶组成一个链表,然后一路找下去,k 个桶查询时间复杂度是 O(k)
- 可以用跳转表进行一个快速的跳转也能实现 O(logk) 的时间复杂度
- 在这个跳转表中,每个桶记录距离自己 1,2,4 距离的数字所存的桶
- 这样不管查询落在哪个节点上,对整个哈希环上任意的查询一次都可以至少跳过一半的查询空间,这样递归下去很快就可以定位到数据是存在哪个桶上
虚拟节点
- 原始数据在hash后分布是均匀的,而上述做法在添加服务器的过程中,会导致数据分布不再均匀,少量服务器承载了大部分数据
- 解决方式是把一个服务器拆分成多个虚拟节点,并且他们均匀分布在hash圆环上
- 解决方式是把一个服务器拆分成多个虚拟节点,并且他们均匀分布在hash圆环上
- 虚拟节点按什么顺序分布?
- 显然不是交叉分布
- 否则假设删除物理节点B时,B节点上的数据永远落在物理节点C上了,这和不设虚拟节点效果一样了
- 需要随机打乱虚拟节点顺序
- 显然不是交叉分布