Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > go-zero 组件布隆过滤器

go-zero 组件布隆过滤器使用示例详解

作者:Keson

这篇文章主要为大家介绍了go-zero组件介绍之布隆过滤器使用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

概述

布隆过滤器(英語:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。——引自:[维基百科]

如果对概念没有很深的理解,我们可以通过一个实际业务场景出发,来加深对这个组件的理解,假设我们需要判断一个值是否在一个集合中,这个判断结果允许有一定的误差,你灵光一闪而过的解决方案是不是这样?

func contains(list []any, item any) bool {
   for _, i := range list {
      if i == item {
         return true
      }
   }
   return false
}

这是最原始,最简单粗暴的解决方案,不难看出,该算法的时间复杂度为 O(n),当我们要从 100w 数据判断是否存在某一个元素是否存在时,你能想到哪些优化方案?是不是首先要降低时间复杂度,那么我们将该算法稍作修改,代码如下:

func contains(m map[any]struct{}, item any) bool {
   _, ok := m[item]
   return ok
}

将数据结构稍作修改,从数组改为 map,其时间复杂度由原来的 O(n) 降低至 O(1),简单从时间复杂度上来看,是不是已经能够完全解决问题了,如果我们将空间复杂度放进来一起考虑呢?那么数组和 map 的空间复杂度都是 O(n),100万的数据,如果一个数据空间暂用为 1k,那么 100万数据暂用空间约 980Mb,如果每个视频的评论积赞数都用这个算法,那以目前短视频这种量,一个视频得搞 1G 来存,这显然行不通的,有没有刚好的方案呢。

我们可以基于 redis bitmap 做操作,redis 的 bitmap 是基于字符串的,如果按照一个用户一个偏移量来计算,100万个用户的点赞大约会用约 12k 的空间,且读写的时间复杂度均是 O(1),这相对于 map 来看,这个优化空间量级非常大,也很可观,其实到这里一般的业务需求完全够用了,假设一个用户平均每个视频有100万赞,每个用户终身暂定有 10000 个视频,那么一个用户需要消耗 117 Mb,这个相比于用户给平台带来的收益那是微乎其微的。我们紧接着继续深讨,如果该公司有1亿用户,且每个用户都需要消耗117Mb,那么所有用户将消耗 11158 Tb,按照目前 redis 行情计算,集群版,2分片(每分片 1G 存储)计算,大约 2900元/年,因此 11158 Tb 一年要花费 331 亿元,真要这么搞,恐怕面试的时候就让你回家等消息了。

布隆过滤器原理

布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。——引自:[维基百科]

如果用布隆过滤器,则可以缩小 2^k,假设 k 为 16,那么上述费用立马会从331亿元减少至约 51万元,这个成本降幅那是相当哇塞。

优缺点

从上述描述我们已经清晰的感知到,布隆过滤器相比于其他数据结构,其时间复杂度和空间复杂度都有足够的优势,但空间复杂度的优势又是其劣势,可想而知,将1亿个用户,每个用户100万数据落在固定长度中的某 k 位位图上,其会有冲突概率的,因此给业务带来的感知是误算率会随着位图的长度降低而增高,由于冲突导致的误算,因此布隆过滤器是不允许做删除操作的,谁知道有多少个冲突数据落在了这 k 个点上。

go-zero 中的布隆过滤器算法

go-zero 中的布隆过滤器也是基于 redis bitmap 的,其主要由 4 个方法组成:// 代码为伪代码

type Bloom interface{
  Add(data []byte) error
  AddCtx(ctx context.Context, data []byte) error
  Exists(data []byte) (bool, error)
  ExistsCtx(ctx context.Context, data []byte) (bool, error)
}

算法时序图

计算偏移量 - getLocations

func (f *Filter) getLocations(data []byte) []uint {
   locations := make([]uint, maps)
   for i := uint(0); i < maps; i++ {
      hashValue := hash.Hash(append(data, byte(i)))
      locations[i] = uint(hashValue % uint64(f.bits))
   }
   return locations
}

根据维基百科定义,『通过K个散列函数将这个元素映射成一个位数组中的K个点』,那么期望结果是每个散列函数映射的偏移量都不同,在 go-zero 中,其巧妙通过散列函数的索引与数据字节组充足成一个字节组来对新的字节组进行 hash 计算得到不同的 hash 值,然后再将该值与用户期望的位图长度进行取模计算得到偏移量。

Bitmap setbit 操作

// 对 redis bitmap 进行 setbit 操作
var setScript = redis.NewScript(`
for _, offset in ipairs(ARGV) do
   redis.call("setbit", KEYS[1], offset, 1)
end
`)
func (r *redisBitSet) set(ctx context.Context, offsets []uint) error {
   ...
   _, err = r.store.ScriptRunCtx(ctx, setScript, []string{r.key}, args)
   if err == redis.Nil {
      return nil
   }
   return err
}

Bitmap getbit 操作

// 对 redis getbit 进行 setbit 操作
var testScript = redis.NewScript(`
for _, offset in ipairs(ARGV) do
   if tonumber(redis.call("getbit", KEYS[1], offset)) == 0 then
      return false
   end
end
return true
`)
func (r *redisBitSet) check(ctx context.Context, offsets []uint) (bool, error) {
   ...
   resp, err := r.store.ScriptRunCtx(ctx, testScript, []string{r.key}, args)
   if err == redis.Nil {
      return false, nil
   } else if err != nil {
      return false, err
   }
   exists, ok := resp.(int64)
   if !ok {
      return false, nil
   }
   return exists == 1, nil
}

FAQ

1. 为什么使用 LUA 脚本进行 bitmap 操作?

由于我们需要对 14 个 bitmap 偏移量进行操作:

2. 为什么采用 14 个散列函数,这个数值怎么来的?

最佳实践参考:https://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html

当散列函数个数为 14 时,且位图长度在20,其误算率可达到最低,在 0.000067。

以上就是go-zero 组件介绍:布隆过滤器的详细内容,更多关于go-zero 组件布隆过滤器的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文