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

 更新时间:2023年05月24日 14:01:17   作者: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 偏移量进行操作:

  • lua 脚本可以将多个指令一次性发送
  • 原子操作

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

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

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

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

相关文章

  • Go语言并发爬虫的具体实现

    Go语言并发爬虫的具体实现

    本文主要介绍了Go语言并发爬虫的具体实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12
  • Go语言模拟while语句实现无限循环的方法

    Go语言模拟while语句实现无限循环的方法

    这篇文章主要介绍了Go语言模拟while语句实现无限循环的方法,实例分析了for语句模拟while语句的技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-02-02
  • Go中JSON解析时tag的使用

    Go中JSON解析时tag的使用

    本文主要介绍了Go中JSON解析时tag的使用,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • 浅析Go语言中内存泄漏的原因与解决方法

    浅析Go语言中内存泄漏的原因与解决方法

    这篇文章主要来和大家聊一聊Go语言中内存泄漏的那些事,例如内存泄漏的原因与解决方法,文中的示例代码讲解详细,需要的小伙伴可以参考下
    2024-02-02
  • Go常问的一些面试题汇总(附答案)

    Go常问的一些面试题汇总(附答案)

    通常我们去面试肯定会有些不错的Golang的面试题目的,所以总结下,让其他Golang开发者也可以查看到,同时也用来检测自己的能力和提醒自己的不足之处,这篇文章主要给大家介绍了关于Go常问的一些面试题以及答案的相关资料,需要的朋友可以参考下
    2023-10-10
  • Go 高效截取字符串的一些思考

    Go 高效截取字符串的一些思考

    这篇文章主要介绍了Go 高效截取字符串的一些思考,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-10-10
  • Go-ethereum 解析ethersjs中产生的签名信息思路详解

    Go-ethereum 解析ethersjs中产生的签名信息思路详解

    这篇文章主要介绍了Go-ethereum 解析ethersjs中产生的签名信息,我们解析签名的需要知道,签名的消息,签名,和公钥,按照这个思路,我们可以通过ethers实现消息的签名,也可以通过go-ethereum实现,需要的朋友可以参考下
    2022-08-08
  • Golang 实现Thrift客户端连接池方式

    Golang 实现Thrift客户端连接池方式

    这篇文章主要介绍了Golang 实现Thrift客户端连接池方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • Go语言单元测试的实现及用例

    Go语言单元测试的实现及用例

    在日常开发中,我们通常需要针对现有的功能进行单元测试,以验证开发的正确性,本文主要介绍了Go语言单元测试的实现及用例,具有一定的参考价值,感兴趣的可以了解一下
    2024-01-01
  • Go java 算法之括号生成示例详解

    Go java 算法之括号生成示例详解

    这篇文章主要为大家介绍了Go java 算法之括号生成示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08

最新评论