0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

一文理解布隆过滤器和布谷鸟过滤器

京东云 来源:京东保险 王奕龙 作者:京东保险 王奕龙 2024-11-07 10:10 次阅读

作者:京东保险 王奕龙

最近在大促中使用到了布隆过滤器,所以本次借着机会整理下相关内容,并了解了布谷鸟过滤器,希望对后续学习的同学有启发~

布隆过滤器

布隆过滤器是 概率性数据结构用于检查元素是否存在集合中。布隆过滤器并不存储集合中的所有元素,而是存储元素的哈希表示,因此牺牲了一些精确性:当布隆过滤报告某元素在集合中不存在时,那么它一定不存在;报告某元素存在时,允许出现“假阳性”,有时会错误地报告某个元素在集合中,而实际上它不存在,这样的权衡使得布隆过滤器非常节省空间且速度快。

布隆过滤器本质上是由许多位组成的数组。当一个元素 “添加” 到布隆过滤器时,该元素会被哈希,然后将位数组中索引为 [hashval % nbits] 的位置设为 1。这与哈希表中桶的映射方式类似,要检查一个元素是否存在,计算其哈希值,并查看相应的位是否被设置为 1。

wKgaoWcsIZCAd4CBAAJAiCQrz_o550.png

如果发生哈希碰撞,便会出现“假阳性”。为了减少碰撞风险,一个元素可以使用多个位:元素会进行多次哈希(每次哈希使用不同的 Seed 生成不同的哈希值),并将每个哈希值对应的 hashval % nbits 位设置为 1。要检查元素是否存在,该元素也会被多次哈希,如果有任何对应的位未被设置,则可以确定该项不存在。每个元素的位数 在创建过滤器时已经被确定了。通常,每个元素使用的位越多,假阳性的可能性就越低,如下所示为需要设置 3 位才能确定该元素存在的过滤器:

wKgZoWcsIZGAHN59AAJ67EWB3lk179.png

影响布隆过滤器准确度的另一个因素是填充率,即过滤器中实际设置了多少位。如果过滤器设置了绝大多数位,则任何特定查找返回 false 的可能性就会降低,过滤器误报的可能性就会增加,所以过滤器在初始化时会规定容量。

Redis 提供了 可扩展的布隆过滤器,来解决布隆过滤器容量固定的问题。当一个布隆过滤器达到容量时,会在其上创建一个新的过滤器。通常,新过滤器的容量比之前的更大,以减少再堆叠另一个过滤器的可能。在可扩展的布隆过滤器中,检查元素是否存在便涉及检查所有过滤器。即使是 Redis 提供了创建可扩展的布隆过滤器的功能,但是了解预期包含多少元素依然很重要,如果初始的过滤器只能包含少量元素,那么随着过滤器的扩展,性能会降低。

向布隆过滤器中插入的时间复杂度为 O(K),其中 k 为哈希函数的数量,对于扩展过滤器,检查元素的复杂度为 O(K) 或 O(K*(n + 1)),其中 n 是已扩展的过滤器数量。

Github - bloom 仓库是基于 Golang 实现的布隆过滤器,适合学习了解原理。因为该实现全量代码较少,所以将需要关注的逻辑全部列在下面,并表明了注释,供大家参考:

type BloomFilter struct {
	// m 位
	m uint
	// k 个 Hash
	k uint
	// 大小为 m 的 BitSet
	b *bitset.BitSet
}

// NewWithEstimates 创建布隆过滤器,根据容量和假阳率估算(estimate)位数和要Hash的次数
func NewWithEstimates(n uint, fp float64) *BloomFilter {
	m, k := EstimateParameters(n, fp)
	return New(m, k)
}

// EstimateParameters 根据容量和假阳率估算 位数和Hash次数
func EstimateParameters(n uint, p float64) (m uint, k uint) {
	// 位数 = (p 的对数的相反数 * 容量 / 2 的对数的平方)的最接近的整数
	m = uint(math.Ceil(-1 * float64(n) * math.Log(p) / math.Pow(math.Log(2), 2)))
	// hash次数 = (2 的对数 * 位数 / 容量) 最接近的整数
	k = uint(math.Ceil(math.Log(2) * float64(m) / float64(n)))
	return
}

func New(m uint, k uint) *BloomFilter {
	return &BloomFilter{max(1, m), max(1, k), bitset.New(m)}
}

func max(x, y uint) uint {
	if x > y {
		return x
	}
	return y
}

// Add 添加新的元素到布隆过滤器中,支持链式编程
func (f *BloomFilter) Add(data []byte) *BloomFilter {
	// 添加验证容量大小的逻辑,如果超过了提示 warn 信息
	// four unit64
	h := baseHashes(data)
	for i := uint(0); i < f.k; i++ {
		// 调用 bitset.BitSet 的 `Set` 方法,将计算出的位置添加到布隆过滤器中
		f.b.Set(f.location(h, i))
	}
	return f
}

// Test 检查某元素是否在布隆过滤器中
func (f *BloomFilter) Test(data []byte) bool {
	h := baseHashes(data)
	for i := uint(0); i < f.k; i++ {
		if !f.b.Test(f.location(h, i)) {
			return false
		}
	}
	return true
}

// baseHashes 计算元素的四个哈希值,用于 k 次哈希计算
func baseHashes(data []byte) [4]uint64 {
	var d digest128 // murmur hashing
	hash1, hash2, hash3, hash4 := d.sum256(data)
	return [4]uint64{
		hash1, hash2, hash3, hash4,
	}
}

// 将第 i 个哈希位置映射到布隆过滤器的位数组中
func (f *BloomFilter) location(h [4]uint64, i uint) uint {
	return uint(location(h, i) % uint64(f.m))
}

// 计算第 i 个哈希值
func location(h [4]uint64, i uint) uint64 {
	ii := uint64(i)
	return h[ii%2] + ii*h[2+(((ii+(ii%2))%4)/2)]
}

应用布隆过滤器

Redisson 提供了操作布隆过滤器的简单易用 API,以下是使用布隆过滤器的示例:

引入依赖

    < dependency >
        < groupId >org.redisson< /groupId >
        < artifactId >redisson< /artifactId >
        < version >3.37.0< /version >
    < /dependency >

使用示例

    private void redisson() {
        RedissonClient redissonClient = Redisson.create();

        RBloomFilter< Object > bloomFilter = redissonClient.getBloomFilter("bloomFilter");
        // 初始化大小为 10亿,假阳率为 0.001(在使用布隆过滤器之前,必须完成初始化操作)
        bloomFilter.tryInit(1000000000, 0.001);

        Object object = new Object();
        // 添加元素
        bloomFilter.add(object);

        // 检查元素是否存在
        boolean exist = bloomFilter.contains(object);
    }

Guava 也提供了 BloomFilter 实现,用于高效地判断一个元素是否存在于集合中,在 23.0 及之后版本中,是线程安全的。以下是 Guava 中布隆过滤器使用示例:

引入依赖

< dependency >
    < groupId >com.google.guava< /groupId >
    < artifactId >guava< /artifactId >
    < !-- 请根据需要选择合适的版本 -- >
    < version >33.3.1-jre< /version >
< /dependency >

使用示例

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class BloomFilterExample {
    public static void main(String[] args) {
        // 创建一个布隆过滤器,预计插入 3000000 个整数,假阳率为0.01
        BloomFilter< Integer > bloomFilter = BloomFilter.create(
                Funnels.integerFunnel(), 3000000, 0.01);

        // 向布隆过滤器中添加元素
        for (int i = 0; i < 3000000; i++) {
            bloomFilter.put(i);
        }

        // 测试布隆过滤器
        for (int i = 0; i < 3001000; i++) {
            if (bloomFilter.mightContain(i)) {
                System.out.println(i + " might be in the filter.");
            } else {
                System.out.println(i + " is definitely not in the filter.");
            }
        }
    }
}

布谷鸟过滤器

布隆过滤器不记录元素本身,并且存在一个位被多个元素共用的情况,所以它不支持删除元素。布谷鸟过滤器(详细了解可以参考这篇论文《布谷鸟过滤器:实际上优于布隆过滤器》)的提出解决了这个问题,它支持删除操作,此外它还带来了其他优势:

查找性能更高:布隆过滤器要采用多种哈希函数进行多次哈希,而布谷鸟过滤器只需一次哈希

节省更多空间:布谷鸟过滤器记录元素更加紧凑,论文中提到,如果期望误报率在 3% 以下,半排序桶布谷鸟过滤器每个元素所占用的空间要比布隆过滤器中单个元素占用空间要小

布谷鸟过滤器之所以被称为“布谷鸟”,是因为它的工作原理类似于布谷鸟在自然界中的行为。布谷鸟以将自己的蛋产在其他鸟类的巢中而闻名,这样一来,寄主鸟就会抚养布谷鸟的幼鸟。
在布谷鸟过滤器中,如果一个位置已经被占用,新元素会“驱逐”现有元素,将其移到其他位置。这种“驱逐”行为类似于布谷鸟将其他鸟蛋推出巢外,以便安置自己的蛋。因此,这种过滤器得名为“布谷鸟”过滤器。

布谷鸟过滤器本质上是一个 桶数组,每个桶中保存若干数量的 指纹(指纹由元素的部分 Hash 值计算出来)。定义一个布谷鸟过滤器,每个桶记录 2 个指纹,5 号桶和 11 号桶分别记录保存 a, b 和 c, d 元素的指纹,如下所示:

wKgaoWcsIZKAV0L6AALpG5iandI863.png

此时,向其中插入新的元素 e,发现它被哈希到的两个候选桶分别为 5 号 和 11 号,但是这两个桶中的元素已经添加满了:

wKgZoWcsIZOAMWuuAANnxvh4PCc155.png

按照布谷鸟过滤器的特性,它会将其中的一个元素重哈希到其他的桶中(具体选择哪个元素,由具体的算法指定),新元素占据该元素的位置,如下:

wKgZoWcsIZWAHQ2AAAOkuB2mYNc469.png

以上便是向布谷鸟过滤器中添加元素并发生冲突时的操作流程,在我们的例子中,重新放置元素 e 触发了另一个重置,将现有的项 a 从桶 5 踢到桶 15。这个过程可能会重复,直到找到一个能容纳元素的桶,这就使得布谷鸟哈希表更加紧凑,因此可以更加节省空间。如果没有找到空桶则认为此哈希表太满,无法插入。虽然布谷鸟哈希可能执行一系列重置,但其均摊插入时间为 O(1)

与布隆过滤器一样,布谷鸟过滤器同样会造成假阳性,造成假阳性的有以下原因:

有限的空间:布谷鸟过滤器使用有限数量的桶和每个桶中的有限空间来存储元素的指纹。当多个元素的指纹映射到相同的桶时,可能会导致不同元素的指纹存储在同一位置

指纹冲突:由于指纹是元素的哈希值的缩减版本,可能会有不同的元素产生相同的指纹。当查询一个不存在的元素时,可能会发现其指纹已经存在于过滤器中,从而导致假阳性

哈希函数的性质:哈希函数的选择和指纹长度决定了指纹的唯一性和冲突概率。较短的指纹更容易产生冲突,从而增加假阳性的概率

负载因子:随着过滤器接近满载,冲突的概率增加,这会导致更多的“驱逐”操作。在高负载情况下,假阳性率也可能上升

Github - cuckoofilter 是 Github 上 Star 数较多的一个仓库,它参考了论文内容,并用 Golang 实现了布谷鸟过滤器,大家感兴趣的话可以直接去参考它的源码。该过滤器重要的参数如下:

每个元素有 2 个候选桶,每个桶记录 4 个指纹:该配置能够使桶的利用率达到 95%,能够满足多数场景,当指定假阳性率在 0.00001 和 0.002 之间时,可以将每个元素占用空间最小化

指纹的静态大小为 8 位:指定误报率为 0.03,根据公式 f >= log2(2b/r) b为桶的大小 r为误报率,计算出指纹大小为 8。在 2 个候选桶和 4 个指纹的配置下,随着指纹大小变大,空间利用率不会再随之增加,仅降低假阳率

我们在此讨论下它的删除方法实现:

// Delete 删除过滤器中的指纹
func (cf *Filter) Delete(data []byte) bool {
	// 尝试在首选桶中删除
	i1, fp := getIndexAndFingerprint(data, cf.bucketPow)
	if cf.delete(fp, i1) {
		return true
	}
	// 删除失败,则尝试从备用桶删除
	i2 := getAltIndex(fp, i1, cf.bucketPow)
	return cf.delete(fp, i2)
}

它的删除方法实现比较简单:它检查给定元素的两个候选桶,如果在首选桶中匹配到则将该指纹移除,否则去备用桶中匹配,在备用桶中则移除备用桶指纹,如果备用桶中没有,则会提示删除失败。如果两个元素 a, b 发生碰撞(共享桶和指纹),那么在 a 元素删除后,因为 b 元素的存在,仍然会判断 a 元素在过滤器中,表现出假阳性。需要注意的是,想要安全的删除某元素,必须事先插入它,否则删除插入项可能会无意中删除共享指纹的真实存在的项,而且如果多次插入重复元素,想要将其删除干净还需要知道该元素插入了多少次。

此外,相比于布隆过滤器它也存在一些的劣势:

插入性能可能会受到影响:随着插入元素越多,空间利用率不断提高,发生冲突的可能性越大,发生冲突之后,可能会不断的触发元素的重定位,插入性能会变差,一般通过最大重试次数来限制

插入重复元素次数存在上限:布隆过滤器插入重复元素没有负面影响,只是再标记相同的位,而布谷鸟过滤器插入重复元素会触发元素的重定位,因此它的重复元素插入存在上限

对于过滤器缓存的使用,大部分情景都是读多写少的,而重复插入并没有什么意义,布谷鸟过滤器的删除虽然不完美但总好过没有(因为布隆过滤器想要删除元素便需要重建,上亿甚至几十亿的数据重建缓存也蛮花时间),同时还有更优的查询和存储效率,应该说在绝大多数情况下其都是一个性价比更高的选择。

适用场景

检测用户名是否存在:将所有已注册用户名使用布隆过滤器,新用户创建用户名时,检查该用户名是否存在于布隆过滤器中

广告投放:为每个用户创建一个布隆过滤器,保存所有已购买的商品,在进行商品广告投放时,检查该商品是否在布隆过滤器中

延保业务实践商城订单详情页延保信息只有在商品进入完成态时才有。在大促期间,用户在购买完商品时,会习惯性点击订详查看,此时会有大量的无效请求进来,直接查询数据库,为了避免数据库击穿,将所有完成的订单保存在布隆过滤器中,这样便能过滤大量请求

巨人的肩膀

Redis - Bloom filter

Github - bloom

Redisson - Bloom filter

Redis - Cuckoo filter

Linvon - 布谷鸟过滤器:实际上优于布隆过滤器

COOLSHELL - Cuckoo Filter:设计与实现

木鸟杂技 - 布谷鸟哈希和布谷鸟过滤器

Github - cuckoofilter

审核编辑 黄宇

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 过滤器
    +关注

    关注

    1

    文章

    427

    浏览量

    19549
  • 布隆过滤器
    +关注

    关注

    0

    文章

    4

    浏览量

    6137
收藏 人收藏

    评论

    相关推荐

    过滤组、过滤器编号介绍

    过滤组、过滤器编号介绍 在STM32互联型产品中,CAN1和CAN2分享28个过滤器组,其它STM32F103xx系列产品中有14个过滤器
    发表于 08-20 06:13

    CN过滤器原理

    CN过滤器原理 CN过滤器采用整体玻璃钢,耐酸耐碱,般耐温65℃。内部装有约半米高的悬浮介质层。悬浮过滤介质为1-2mm小球,采用高分子材料加工,密度大约
    发表于 02-25 15:00 26次下载

    过滤器的作用

    本视频主要详细介绍了过滤器的作用,分别是滤速高、过滤效果好;强度高、耐腐蚀;静电作用;过滤物质;拦截;其次介绍了水龙头过滤器的作用,最后介绍了活性炭
    的头像 发表于 12-12 16:23 4.4w次阅读

    如何使用计数型过滤器进行可排序密检索的方法概述

    云计算环境下密检索困难,已有的可搜索加密方案存在时间效率低、文件检索索引不支持更新、检索结果不能实现按精确度排序等问题。首先基于计数型过滤器构建文件检索索引,将文件集中的关键词哈
    发表于 01-02 15:17 1次下载
    如何使用计数型<b class='flag-5'>布</b><b class='flag-5'>隆</b><b class='flag-5'>过滤器</b>进行可排序密<b class='flag-5'>文</b>检索的方法概述

    解密高效空气过滤器的性能及要求

    高效过滤器生产厂商 三河市科丰电气有限公司高效过滤器。三河市科丰电气有限公司致力于为通信行业、暖通行业、节能行业,过滤行业等行业并提供专业配套产品和服务。高效过滤器产品具有
    发表于 03-19 14:56 2020次阅读

    创新陶瓷过滤器解决方案

    创新陶瓷过滤器解决方案
    发表于 10-27 14:56 16次下载

    丝扣Y过滤器

    丝扣Y过滤器是Y过滤器种,普通滤材是不锈钢或者碳钢,滤芯普通带有不锈钢骨架。 丝扣Y形过滤器有时也叫做·不锈钢内螺纹Y过滤器。    特
    的头像 发表于 08-13 17:24 4076次阅读

    丝扣Y过滤器过滤器测试原理简介

    丝扣Y过滤器是Y过滤器种,普通滤材是不锈钢或者碳钢,滤芯普通带有不锈钢骨架。 丝扣Y形过滤器有时也叫做·不锈钢内螺纹Y过滤器。  特性:
    发表于 09-05 09:27 2514次阅读

    丝扣Y形过滤器

    丝扣Y形过滤器是保送介质管道上不可短少的种安装,通常装置在减压阀、泄压阀、定水位阀或其它设备的进口端,用来消弭介质中的杂质,以维护阀门及设备的正常运用。 丝扣Y形过滤器有时也叫做·不锈钢内螺纹Y
    的头像 发表于 10-24 15:03 3768次阅读

    Y型过滤器

    Y型过滤器是保送介质的管道系统不可短少的一种过滤安装,Y型过滤器通常装置在减压阀、泄压阀、定水位阀或其它设备的进口端,用来   介质中的杂质,以维护阀门及设备的正常运用。Y型过滤用具有
    发表于 10-25 15:32 2615次阅读

    科普下12种管道过滤器

    Y型过滤器属于管道粗过滤器,可用于液体、气体或其他介质大颗粒物过滤
    的头像 发表于 01-12 09:57 6550次阅读

    汉克森过滤器系列介绍

    汉克森过滤器 【1】国产品牌滤芯均为我司生产的替代原厂品牌滤芯,其过滤滤材采用德国原装进口HV公司产品,注册商标为“佳洁”牌。本公司涉及的其它品牌均无品牌意义,只是作为产品型号参照和客户选型对照
    发表于 03-01 08:53 1093次阅读
    汉克森<b class='flag-5'>过滤器</b>系列介绍

    过滤器药液过滤器滤除率测试仪

    过滤器药液过滤器滤除率测试仪
    的头像 发表于 03-09 14:53 825次阅读
    <b class='flag-5'>过滤器</b>药液<b class='flag-5'>过滤器</b>滤除率测试仪

    杀菌过滤器 灭菌过滤器 除菌过滤器

    杀菌过滤器 灭菌过滤器 除菌过滤器
    的头像 发表于 03-03 14:03 2565次阅读
    杀菌<b class='flag-5'>过滤器</b> 灭菌<b class='flag-5'>过滤器</b> 除菌<b class='flag-5'>过滤器</b>

    聊聊过滤器

    过滤器个精巧而且经典的数据结构。
    的头像 发表于 06-30 10:03 583次阅读
    聊聊<b class='flag-5'>布</b><b class='flag-5'>隆</b><b class='flag-5'>过滤器</b>