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 语言中和类型Sum Types的创新实现方案详解

    Go 语言中和类型Sum Types的创新实现方案详解

    本文介绍了Go语言中如何通过ProjectionStructs和SafeUnsafeConversion来实现和类型(SumTypes),该方案通过内存布局相同的多个结构体投影同一块数据,实现零自定义编解码、编译时类型安全和IDE自动补全友好,感兴趣的朋友跟随小编一起看看吧
    2026-02-02
  • 解决golang http.FileServer 遇到的坑

    解决golang http.FileServer 遇到的坑

    这篇文章主要介绍了解决golang http.FileServer 遇到的坑,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • 利用GoLang Fiber进行高性能Web开发实例详解

    利用GoLang Fiber进行高性能Web开发实例详解

    这篇文章主要为大家介绍了利用GoLang Fiber进行高性能Web开发实例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2024-01-01
  • Go基础教程系列之数据类型详细说明

    Go基础教程系列之数据类型详细说明

    这篇文章主要介绍了Go基础教程系列之数据类型详细说明,需要的朋友可以参考下
    2022-04-04
  • golang小游戏开发实战之飞翔的小鸟

    golang小游戏开发实战之飞翔的小鸟

    这篇文章主要给大家介绍了关于golang小游戏开发实战之飞翔的小鸟的相关资料,,本文可以带你你从零开始,一步一步的开发出这款小游戏,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-03-03
  • Go语言常见错误之误用init函数实例解析

    Go语言常见错误之误用init函数实例解析

    Go语言中的init函数为开发者提供了一种在程序正式运行前初始化包级变量的机制,然而,由于init函数的特殊性,不当地使用它可能引起一系列问题,本文将深入探讨如何有效地使用init函数,列举常见误用并提供相应的避免策略
    2024-01-01
  • 详解使用Go添加Nginx代理的方法示例

    详解使用Go添加Nginx代理的方法示例

    这篇文章主要介绍了详解使用Go添加Nginx代理的方法示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-11-11
  • Go语言利用标准库flag编写一个命令行参数解析器

    Go语言利用标准库flag编写一个命令行参数解析器

    在日常开发中,很多工具型程序都需要通过命令行参数来传递配置,本文将通过一个小实例,演示如何使用 Go 标准库 flag 开发一个简单的命令行参数解析器
    2025-09-09
  • 使用Go语言发送邮件的示例代码

    使用Go语言发送邮件的示例代码

    很多朋友想试试用Go语言发送邮件,所以接下来小编给大家介绍一下如何用Go语言发送邮件,文中通过代码实例讲解的非常详细,需要的朋友可以参考下
    2023-07-07
  • Go中recover与panic区别详解

    Go中recover与panic区别详解

    这篇文章主要介绍了Go中recover与panic区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-11-11

最新评论