[C/C++后端开发学习]4 布隆过滤器与分布式一致性Hash

海量数据去重的Hash与布隆过滤器

  • 1 背景
  • 2 可用的数据结构对比
  • 3 散列表
    • 3.1 冲突处理方法
  • 4 布隆过滤器
    • 4.1 原理
    • 4.2 为什么不支持数据删除操作
    • 4.3 应用场景
      • 缓存穿透问题
    • 4.4 布隆过滤器的设计方法
      • 1)确定参数
      • 2)选择Hash函数
    • 4.5 其他补充
  • 5 分布式一致性 hash
    • 5.1 背景
    • 5.2 hash 偏移问题
      • 虚拟节点

1 背景

  • 使用 word 文档时,word 如何判断某个单词是否拼写正确?
  • 从海量数据中查询某个字符串是否存在?
  • 网络爬虫程序,怎么让它不去爬相同的 url 页面?
  • 垃圾邮件过滤算法如何设计?
  • 公安办案时,如何判断某嫌疑人是否在网逃名单中?
  • 缓存穿透问题如何解决?

2 可用的数据结构对比

  • 平衡二叉树
    增删改查时间复杂度为O(logn) ;通过比较保证有序,通过每次排除一半的元素达到快速索引的目的。
    包括:平衡二叉搜索树(AVL树、红黑树)、平衡多路搜索树(B树、B+树)

  • 多层级有序链表——跳表

  • 散列表
    根据关键码值 key 计算 key 在表中的位置,是 key 和其所在存储地址的映射关系,可实现根据关键码值直接访问表。一般不适用于范围搜索。

  • 布隆过滤器
    一种概率型数据结构,可以实现高效地插入和查询。

3 散列表

将关键码值映射到数据存储位置的函数称为Hash函数。
hash 函数可能会把两个或两个以上的不同 key 映射到同一地址,这种情况称之为冲突(或者 hash 碰撞)。

3.1 冲突处理方法

  • 开散列方法(链表法)——将冲突的元素记录到表外
    将冲突的元素利用链表链接起来。

    缺点:冲突元素过多时,会导致链表过长,导致查询性能退化

    处理方法:当链表的长度太大时,将链表转化为红黑树,降低查询时间复杂度;由于红黑树维持平衡性也需要性能开销,所以数据较少时使用红黑树并不划算;可以在元素超过 256(经验值)个节点的时候再进行转换。

  • 闭散列方法(线性探查、桶式散列)——把所有记录都直接存在散列表中,不使用额外的数据结构。
    线性探查法:出现冲突时,利用探查函数产生一个步长,将地址加上步长后再判断是否冲突。探查函数产生的步长序列称为探查序列,检索记录时应采用相同的探查序列。探查序列中必须至少有一个槽是空的,否则检索过程会陷入无限循环。

    缺点:存在 hash 聚集,会导致很长的探查序列。

    处理方法:可以使用双重哈希来解决,原理见散列函数之双重散列算法解决冲突问题

4 布隆过滤器

布隆过滤器是一种概率型数据结构,它的特点:
1)能确定某个字符串一定不存在或者可能存在
2)布隆过滤器不存储具体数据,所以占用空间小
3)查询结果存在误差,但是误差可控;
4)不支持删除操作;

4.1 原理

[C/C++后端开发学习]4 布隆过滤器与分布式一致性Hash_第1张图片

(图片为转载:www.0voice.com)

建立一个由bit位组成的位图,其数据全部初始化为0。当保存一个元素时,通过 k 个 hash 函数将这个元素映射到位图的 k 个点,并把这些点的数据置位1;

查询数据时,同样将查询的元素通过这 k 个 hash 函数映射到位图中的k个点。如果映射的k个点全部为1,则可以说明所查询的元素可能存在;否则,可以说明查询的数据一定不存在!

布隆过滤器示例:
[C/C++后端开发学习]4 布隆过滤器与分布式一致性Hash_第2张图片

(图片为转载:www.0voice.com)


这是一个二维的位图。所有bit为初始化为0。
存储数据时:输入数据的关键码val,通过hash函数输出得到索引结果为173,步骤1通过取余使结果处于位图索引的允许范围内;步骤2和3分别计算索引对应的行列索引,均为5,于是将位图中(5,5)的位置置为1,然后存储数据到其他地方。
查询数据时:输入一个关键码值val,如果按上述方法计算得到的位图索引位置处记录为1,则说明这个关键码对应的数据可能存在;若此处记录为0,则说明这个数据一定不存在!

4.2 为什么不支持数据删除操作

这里说的删除是指删除一个数据时,将其在位图中映射出的bit位也置为0。

显然,因为多个不同的数据都可能会把位图中某个bit置位为1,这可能导致其他使用相同bit位的数据被误判为不存在。

如何支持删除:使用两个布隆过滤器,讲删除的数据映射到第二个过滤器中,通过第二个过滤器来判断数据是否已删除;

4.3 应用场景

通常用于判断某个 key 一定不存在的场景,或允许判断为存在时有一定误差。

常见应用场景:① 缓存穿透问题的解决;② 热 key 限流;

缓存穿透问题

  • 场景:为了减轻数据库(mysql)的访问压力,在 server 端与数据库(mysql)之间加入Redis缓存来存储热点数据
  • 问题:当服务端反复某个不存在的数据时,由于缓存和数据库都不包含该数据,最终请求压力全部涌向数据库;黑客可以利用此漏洞导致数据库压力过大而瘫痪。此为缓存穿透问题。
  • 解决方案:1)在Redis上设置键值对,避免针对这个key重复访问mysql;2)服务端使用布隆过滤器进行过滤。

4.4 布隆过滤器的设计方法

1)确定参数

n -- 布隆过滤器中元素的个数,如上图 只有str1和str2 两个元素 那么 n=2
p -- 假阳率,在0-1之间 0.000000
m -- 位图所占空间
k -- hash函数的个数
公式如下:
n = ceil(m / (-k / log(1 - exp(log(p) / k))))
p = pow(1 - exp(-k / (m / n)), k)
m = ceil((n * log(p)) / log(1 / pow(2, log(2))));
k = round((m / n) * log(2));

根据设计需求确定n和p后,利用工具计算m和k,可以使用这个网站进行参数计算:Bloom Filter Calculator

2)选择Hash函数

可以通过线性增长的方式给 hash 传递不同的种子偏移值

4.5 其他补充

  • 计算哈希值为什么常选用31作为乘数:因为选取31这个质数时hash函数的随机分布性表现最好,其他的质数值,小了的话hash冲突的概率就会相对大一些,大了的话又太过于分散;并且31这个值的乘法可以很好地进行性能优化:i × 31 = i<<5- i

5 分布式一致性 hash

5.1 背景

分布式缓存:需要将数据地分散在不同的缓存服务器当中。服务端请求数据时,可通过Hash函数计算请求的数据位于哪一个服务器。比如下面这种情况,如果原来服务器是3台,后来要增加到4台,则Hash函数的计算结果将导致缓存失效。
[C/C++后端开发学习]4 布隆过滤器与分布式一致性Hash_第3张图片
为了解决这个问题,引入分布式一致性 hash 算法将哈希空间组织成一个虚拟的圆环,圆环上节点的大小是 232。利用类似 Hash()%(232) 的算法可生成一个位于环上的位置值。
[C/C++后端开发学习]4 布隆过滤器与分布式一致性Hash_第4张图片

(图片为转载:www.0voice.com)

多个服务器都通过该方式在 hash 环上映射一个点来标识本服务器在环上的位置。

将请求的key也通过该Hash函数计算一个位置值,从该位置开始沿环顺时针定位到某个服务器,那么这个 key 的数据就存储在该服务器中。这么一来,增加服务器后,只需在环上映射一个新的节点,可以减少缓存失效的可能性。

5.2 hash 偏移问题

当节点较少时,hash 算法无法保证服务器节点均匀地分布在圆环上,这就导致了服务器压力不均匀。这可以通过增加虚拟节点来解决。理论上,圆环上节点数越多,数据分布就越均衡。

虚拟节点

为每个服务器分配更多的虚拟节点,通过可以这样设计Hash函数:

hash(“IP:PORT:seqno”) % 232

其中seqno是服务器的序列号。

你可能感兴趣的:(C/C++后端开发学习笔记,数据结构)