为什么需要布隆过滤器
想象一下遇到下面的场景你会如何处理:
手机号是否重复注册用户是否参与过某秒杀活动伪造请求大量 id 查询不存在的记录板框过滤机结构体,此时缓存未命中,如何避免缓存穿透针对以上问题常规做法是:查询数据库,数据库硬扛,如果压力并不大可以使用此方法,保持简单即可。
改进做法:用 list/set/tree 维护一个元素集合,判断元素是否在集合内,时间复杂度或空间复杂度会比较高。如果是微服务的话可以用 redis 中的 list/set 数据结构, 数据规模非常大此方案的内存容量要求可能会非常高。
这些场景有个共同点,可以将问题抽象为:如何高效判断一个元素不在集合中板框过滤机结构体? 那么有没有一种更好方案能达到时间复杂度和空间复杂双优呢板框过滤机结构体?
有板框过滤机结构体!布隆过滤器。
什么是布隆过滤器布隆过滤器(英语:Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中,它的优点是空间效率和查询时间都远远超过一般的算法。
工作原理
布隆过滤器的原理是,当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组中的 K 个点(offset),把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了:如果这些点有任何一个 0,则被检元素一定不在;如果都是 1,则被检元素很可能在。这就是布隆过滤器的基本思想。
简单来说就是准备一个长度为 m 的位数组并初始化所有元素为 0,用 k 个散列函数对元素进行 k 次散列运算跟 len(m)取余得到 k 个位置并将 m 中对应位置设置为 1。
优点:
空间占用极小,因为本身不存储数据而是用比特位表示数据是否存在,某种程度有保密的效果。插入与查询时间复杂度均为 O(k),常数级别,k 表示散列函数执行次数。散列函数之间可以相互独立,可以在硬件指令层加速计算。缺点:
误差(假阳性率)。无法删除。误差(假阳性率)
布隆过滤器可以 100% 判断元素不在集合中,但是当元素在集合中时可能存在误判,因为当元素非常多时散列函数产生的 k 位点可能会重复。 维基百科有关于假阳性率的数学推导(见文末链接)这里我们直接给结论(实际上是我没看懂...),假设:
位数组长度 m散列函数个数 k预期元素数量 n期望误差_ε_在创建布隆过滤器时我们为了找到合适的 m 和 k ,可以根据预期元素数量 n 与 ε 来推导出最合适的 m 与 k 。
java 中 Guava, Redisson 实现布隆过滤器估算最优 m 和 k 采用的就是此算法:
// 计算哈希次数@VisibleForTestingstatic int optimalNumOfHashFunctions(long n, long m) { // (m / n) * log(2), but avoid truncation due to division! return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));}// 计算位数组长度@VisibleForTestingstatic long optimalNumOfBits(long n, double p) { if (p == 0) { p = Double.MIN_VALUE; } return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));}无法删除
位数组中的某些 k 点是多个元素重复使用的,假如我们将其中一个元素的 k 点全部置为 0 则直接就会影响其他元素。 这导致我们在使用布隆过滤器时无法处理元素被删除的场景。
可以通过定时重建的方式清除脏数据。假如是通过 redis 来实现的话重建时不要直接删除原有的 key,而是先生成好新的再通过 rename 命令即可,再删除旧数据即可。
go-zero 中的 bloom filter 源码分析core/bloom/bloom.go 一个布隆过滤器具备两个核心属性:
位数组:散列函数go-zero实现的bloom filter中位数组采用的是Redis.bitmap,既然采用的是 redis 自然就支持分布式场景,散列函数采用的是MurmurHash3
Redis.bitmap 为什么可以作为位数组呢板框过滤机结构体?
Redis 中的并没有单独的 bitmap 数据结构,底层使用的是动态字符串(SDS)实现,而 Redis 中的字符串实际都是以二进制存储的。 a 的ASCII码是 97,转换为二进制是:01100001,如果我们要将其转换为b只需要进一位即可:01100010。下面通过Redis.setbit实现这个操作:
set foo a OK get foo "a" setbit foo 6 1 0 setbit foo 7 0 1 get foo "b"
bitmap 底层使用的动态字符串可以实现动态扩容,当 offset 到高位时其他位置 bitmap 将会自动补 0,最大支持 2^32-1 长度的位数组(占用内存 512M),需要注意的是分配大内存会阻塞Redis进程。 根据上面的算法原理可以知道实现布隆过滤器主要做三件事情:
k 次散列函数计算出 k 个位点。插入时将位数组中 k 个位点的值设置为 1。查询时根据 1 的计算结果判断 k 位点是否全部为 1,否则表示该元素一定不存在。下面来看看go-zero 是如何实现的:
对象定义
// 表示经过多少散列函数计算// 固定14次maps = 14type ( // 定义布隆过滤器结构体 Filter struct { bits uint bitSet bitSetProvider } // 位数组操作接口定义 bitSetProvider interface { check([]uint) (bool, error) set([]uint) error })位数组操作接口实现
首先需要理解两段 lua 脚本:
// ARGV:偏移量offset数组// KYES[1]: setbit操作的key// 全部设置为1setScript = ` for _, offset in ipairs(ARGV) do redis.call("setbit", KEYS[1], offset, 1) end `// ARGV:偏移量offset数组// KYES[1]: setbit操作的key// 检查是否全部为1testScript = ` for _, offset in ipairs(ARGV) do if tonumber(redis.call("getbit", KEYS[1], offset)) == 0 then return false end end return true `为什么一定要用 lua 脚本呢? 因为需要保证整个操作是原子性执行的。
// redis位数组type redisBitSet struct { store *redis.Client key string bits uint}// 检查偏移量offset数组是否全部为1// 是:元素可能存在// 否:元素一定不存在func (r *redisBitSet) check(offsets []uint) (bool, error) { args, err := r.buildOffsetArgs(offsets) if err != nil { return false, err } // 执行脚本 resp, err := r.store.Eval(testScript, []string{r.key}, args) // 这里需要注意一下,底层使用的go-redis // redis.Nil表示key不存在的情况需特殊判断 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}// 将k位点全部设置为1func (r *redisBitSet) set(offsets []uint) error { args, err := r.buildOffsetArgs(offsets) if err != nil { return err } _, err = r.store.Eval(setScript, []string{r.key}, args) // 底层使用的是go-redis,redis.Nil表示操作的key不存在 // 需要针对key不存在的情况特殊判断 if err == redis.Nil { return nil } else if err != nil { return err } return nil}// 构建偏移量offset字符串数组,因为go-redis执行lua脚本时参数定义为[]stringy// 因此需要转换一下func (r *redisBitSet) buildOffsetArgs(offsets []uint) ([]string, error) { var args []string for _, offset := range offsets { if offset >= r.bits { return nil, ErrTooLargeOffset } args = append(args, strconv.FormatUint(uint64(offset), 10)) } return args, nil}// 删除func (r *redisBitSet) del() error { _, err := r.store.Del(r.key) return err}// 自动过期func (r *redisBitSet) expire(seconds int) error { return r.store.Expire(r.key, seconds)}func newRedisBitSet(store *redis.Client, key string, bits uint) *redisBitSet { return &redisBitSet{ store: store, key: key, bits: bits, }}到这里位数组操作就全部实现了,接下来看下如何通过 k 个散列函数计算出 k 个位点
k 次散列计算出 k 个位点
// k次散列计算出k个offsetfunc (f *Filter) getLocations(data []byte) []uint { // 创建指定容量的切片 locations := make([]uint, maps) // maps表示k值,作者定义为了常量:14 for i := uint(0); i < maps; i++ { // 哈希计算,使用的是"MurmurHash3"算法,并每次追加一个固定的i字节进行计算 hashValue := hash.Hash(append(data, byte(i))) // 取下标offset locations[i] = uint(hashValue % uint64(f.bits)) } return locations}插入与查询
添加与查询实现就非常简单了,组合一下上面的函数就行。
// 添加元素func (f *Filter) Add(data []byte) error { locations := f.getLocations(data) return f.bitSet.set(locations)}// 检查是否存在func (f *Filter) Exists(data []byte) (bool, error) { locations := f.getLocations(data) isSet, err := f.bitSet.check(locations) if err != nil { return false, err } if !isSet { return false, nil } return true, nil}改进建议整体实现非常简洁高效,那么有没有改进的空间呢?
个人认为还是有的,上面提到过自动计算最优 m 与 k 的数学公式,如果创建参数改为:
预期总数量expectedInsertions
期望误差falseProbability
就更好了,虽然作者注释里特别提到了误差说明,但是实际上作为很多开发者对位数组长度并不敏感,无法直观知道 bits 传多少预期误差会是多少。
// New create a Filter, store is the backed redis, key is the key for the bloom filter,// bits is how many bits will be used, maps is how many hashes for each addition.// best practices:// elements - means how many actual elements// when maps = 14, formula: 0.7*(bits/maps), bits = 20*elements, the error rate is 0.000067 < 1e-4// for detailed error rate table, see http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.htmlfunc New(store *redis.Redis, key string, bits uint) *Filter { return &Filter{ bits: bits, bitSet: newRedisBitSet(store, key, bits), }}// expectedInsertions - 预期总数量// falseProbability - 预期误差// 这里也可以改为option模式不会破坏原有的兼容性func NewFilter(store *redis.Redis, key string, expectedInsertions uint, falseProbability float64) *Filter { bits := optimalNumOfBits(expectedInsertions, falseProbability) k := optimalNumOfHashFunctions(bits, expectedInsertions) return &Filter{ bits: bits, bitSet: newRedisBitSet(store, key, bits), k: k, }}// 计算最优哈希次数func optimalNumOfHashFunctions(m, n uint) uint { return uint(math.Round(float64(m) / float64(n) * math.Log(2)))}// 计算最优数组长度func optimalNumOfBits(n uint, p float64) uint { return uint(float64(-n) * math.Log(p) / (math.Log(2) * math.Log(2)))}回到问题如何预防非法 id 导致缓存穿透?
由于 id 不存在导致请求无法命中缓存流量直接打到数据库,同时数据库也不存在该记录导致无法写入缓存,高并发场景这无疑会极大增加数据库压力。 解决方案有两种:
采用布隆过滤器数据写入数据库时需同步写入布隆过滤器,同时如果存在脏数据场景(比如:删除)则需要定时重建布隆过滤器,使用 redis 作为存储时不可以直接删除 bloom.key,可以采用 rename key 的方式更新 bloom
缓存与数据库同时无法命中时向缓存写入一个过期时间较短的空值。资料布隆过滤器(Bloom Filter)原理及 Guava 中的具体实现
布隆过滤器-维基百科
Redis.setbit
项目地址https://github.com/zeromicro/go-zero
欢迎使用 go-zero 并 star 支持我们!
微信交流群关注『微服务实践』公众号并点击 交流群 获取社区群二维码。
凡本网注明“来源:砂石装备网”的所有作品,版权均属于砂石装备网,转载请注明。
凡注明为其它来源的信息,均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点及对其真实性负责。
如有新闻稿件和图片作品的内容、版权以及其它问题的,请联系我们。
当前,全球供应链深度重构,中国工程机械行业正站在机遇与挑战并存的关键节点。 全球来看,地缘政治与贸易摩擦为出海之路增添变数。与此同时,国内政策托底与设备更新周
2025-10-21 10:16:25
2025长沙国际工程机械展览会东南亚分展 2025 SOUTHEAST ASIA SUB-EXHIBITION OF CHANGSHA INTERNATIONAL CONSTRUCTION EQUIPMENT EXHIBITION 2025年9月3-5日(三天) 地点:马来西亚·吉隆坡实达城会展中心(SCCC...
2025-07-03 16:25:30
核心摘要:2025年5月18日,为期四天的第四届长沙国际工程机械展览会(以下简称“2025CICEE”)在长沙国际会展中心正式落下帷幕。本届展会 2025年5月18日,为期四天的第四届长沙国际工程机械展览会(以下简称“2025CICEE”)在长沙国际会展中心正式落下帷幕。本届展会以"高端化、智能化、...
2025-05-19 11:21:21
2025年5月15日上午9时40分,第四届长沙国际工程机械展览会在长沙国际会展中心隆重开幕。本届展会规模达30万平方米,全球1806家参展企业齐聚一堂,集中展示工程机械领域的最新技术与产品。1000余名全球行业领袖、院士专家、企业代表及国际采购商共同参会,见证这一国际工程机械领域的年度盛事。...
2025-05-16 08:31:29
日立建机参展的首台设备——ZX900LCH-6A矿山液压挖掘机。抵达长沙国际会展中心的卡特彼勒挖掘机。红网时刻新闻5月8日讯(记者 彭超)第四届长沙国际工程机械展将于5月15日开幕。还有一周的时间,国内外品牌相继进驻场馆布展。5月8日,日本日立建机、美国卡特彼勒两家全球工程机械50强企业的参展设备进驻长沙国际会展中心,...
2025-05-10 11:48:38
4月30日,湖南省政府新闻办举办新闻发布会,宣布第四届长沙国际工程机械展览会将于5月15日至18日启幕。当天,装载三一集团蓝色电动装载机的运输车辆驶入长沙国际会展中心,这也是第二家正式布展企业。据悉,三一集团将携旗下18个事业部和子公司、72件套设备(含58台主机)震撼登场,以10089平米的超大展位,全面展现三一的高...
2025-05-06 20:27:36
2025年4月30日,湖南省新闻办公室召开新闻发布会,宣布第四届长沙国际工程机械展览会定于5月15日至18日在长沙国际会展中心、长沙国际会议中心举办。长沙市人民政府副市长康镇麟出席发布会并介绍展会筹备情况:本届展览会以高端化、智能化、绿色化新一代工程机械、应急装备、矿山装备、农业机械为主题,预计吸引全球1650家参展企...
2025-05-06 20:27:07
5月,世界的目光将聚焦长沙。第四届长沙国际工程机械展将于5月15日至18日在长沙国际会展中心、长沙国际会议中心举行。4月29日,随着“全球工程机械制造商50强”中联重科的首批百吨设备进场,展会进入布展时间。上午9时30分,装载着中联重科工程机械零部件的平板车缓缓驶入长沙国际会展中心,荧光绿的漆面在阳光下格外显眼。这些设...
2025-04-30 09:19:07
当全球工程机械产业的聚光灯再次投向湘江之畔,2025CICEE《高端化、智能化、绿色化——新一代工程机械、应急装备、矿山装备、农业机械》电子会刊第二期正式发布。继首期会刊创下超192万次阅读的行业纪录后,本期会刊汇聚183家全球领军企业,以尖端技术产品和创新解决方案,续写“工程机械之都”的传奇篇章。01硬核生态:解码产...
2025-04-30 09:15:38
5月15日,2025长沙国际工程机械展览会(2025 CICEE)即将在长沙国际会展中心盛大开幕,届时,来自全球60多个国家和地区的超1600家参展商,将在30万平方米的展区内陈列超2万件展品,涵盖工程机械全领域前沿技术和产品。目前,众多展商已蓄势待发,期待在这个推动全球工程机械行业交流与合作的超级平台上一展风采。...
2025-04-17 08:31:38
主营:破碎机
主营:破碎机
主营:破碎机
主营:破碎机
主营:破碎机
主营:破碎机、制砂楼
主营:破碎机
主营:破碎机
主营:筛网、筛机配件
主营:振动电机
主营:破碎机
主营:制砂机、筛分设备