分布式 | 布隆过滤器实战指南:原理、编码实现、应用与Redisson最佳实践
文章目录
- 介绍
- 工作原理
- 流传的经典名句:不存在一定不存在,存在那不一定存在。
- 1、添加元素
- 2、查询元素
- 问题:如何处理hash碰撞?
- 了解Java中HashMap处理哈希碰撞
- 布隆过滤器如何处理hash碰撞?
- 如何控制误报率?
- Java实现布隆过滤器
- 应用
- 1. 缓存系统:防止缓存穿透
- 2. 数据库优化:减少磁盘 I/O
- 3. 网络爬虫:去重 URL
- 4. 垃圾邮件过滤
- 应用图解
- 最佳实践:Bloom Filter
- 1. 底层存储:Redis 的 String 或 Hash 结构
- 2. 哈希函数:MurmurHash3
- 3. 原子性保障:Lua 脚本
- 小结
介绍
- 在处理海量数据时,如何快速判断一个元素是否存在于一个巨大的集合中?
- 如果,使用 HashSet、HashMap ,当数据量达到数亿甚至上百亿时,这些方法会消耗巨大的内存,性能急剧下降。
这时,布隆过滤器(Bloom Filter)闪亮登场。
- 空间效率极高
- 概率型数据结构(probabilistic data structure)
作用:
- 用于判断一个元素是否可能在一个集合中。
工作原理
流传的经典名句:不存在一定不存在,存在那不一定存在。
- 不允许漏报(False Negative):有就是有,一个不能少。
- 允许误报(False Positive):狼来了 是可以有的事情。
为什么会导致这种原因?
- 先说结论(图解):
前提:
- 在布隆过滤器中
- 预先将商品1ID和商品2ID,多次hash运算的结果,Redis的Bitmap中相应位:1、4、8 都 置1。
- 巧合的是,当商品3ID经过多次hash运算后,得到的结果,在Bitmap:1、8 置1
- 此时,Bitmap中位置1、8,因为商品1和商品2置为1了。布隆过滤器会判断为存在,进而去数据库中查询数据。
- 实际上,数据库中并没有商品3的数据。
布隆过滤器由两个核心组件构成:
- 一个长度为 m 的位数组(Bit Array):初始所有位为 0。
- redis中的 Bitmap 、Java中 BitSet 等
- k 个独立的哈希函数:每个函数将输入映射到位数组的一个位置
1、添加元素
当向布隆过滤器中添加一个元素时:
- 使用 k 个哈希函数对该元素进行哈希。
- 得到 k 个位置索引。
- 将这 k 个位置的值都设置为 1。
2、查询元素
当查询一个元素是否“可能存在”:
- 使用相同的 k 个哈希函数计算其位置。
- 检查这些位置的值:
- 如果所有位置都是 1 → 返回“存在”。
- 如果任一位置是 0 → 返回“一定不存在”。
问题:如何处理hash碰撞?
了解Java中HashMap处理哈希碰撞
哈希碰撞:
- 当多个键值对的 hashCode() 计算出的索引位置相同时(即发生哈希碰撞),HashMap 会通过以下机制来存储 和 查找数据。
- 开放寻址法:碰撞了,找新的位置。(打不过,就跑)
- 拉链法:碰撞了,跟在后面。(打不过,就加入)
反正就是打不过(碰撞了),两种策略去解决。
不过,开放寻址法无法优化,当碰撞发生时, 数组 没有空间,
- 无法添加数据。
拉链法 呢,当碰撞发生时, 数组 有无空间无所谓。
- 反正我碰撞了,我就要加入,具体加哪里呢。你拉出个链表,将碰撞的数据放进去。有点子嚣张。
优化:
- 在 JDK 8 之后,HashMap 处理哈希碰撞(Hash Collision)的核心策略是:
- 数组 + 链表 + 红黑树(自平衡的二叉查找树)。
- 阈值(边界值)> 8,且数组长度大于64,才会将链表转为红黑树,高效查询。
- 还采用:尾插入 的方法,不用移动其他节点的指针,相当方便。主要还是向性能看齐。
具体描述,如下图所示:
布隆过滤器如何处理hash碰撞?
本来,布隆过滤器是为了节省空间做大事。如果它要是使用以上的方法,那可就出不了名了。
那它要使用什么办法呢?
- 布隆过滤器一想:既然hash计算一次它容易碰撞重复,那我就多计算几次,最后在将它们的结果汇总判断。
- 多个hash方法计算出多个位置,汇总判断是否存在
- 多个位置都是1,则存在。
- 有一个为0,则不存在。
如何控制误报率?
- 既然一定会产生误报,那如何控制误报率呢?
布隆过滤器的性能取决于三个参数:
n:预计元素数量
m:位数组长度
k:哈希函数数量
在给定 n 和期望误报率 p 时,可通过公式计算最优参数:(知道就可)
Java实现布隆过滤器
import java.util.BitSet;
import java.security.MessageDigest;public class BloomFilter {private BitSet bitSet; // 位数组,存储0和1private int size; // 位数组的大小private int[] seeds = {3, 5, 7, 11, 13}; // 5 个哈希函数的“种子”// 创建一个长度为 size 的 BitSet,初始所有位都是 false(即 0)。public BloomFilter(int size) {this.size = size;this.bitSet = new BitSet(size);}// 添加public void add(String value) {// 遍历seeds种子,不同种子产生不同哈希结果for (int seed : seeds) {HashFunction hash = new HashFunction(size, seed);bitSet.set(hash.hash(value), true);}}// 判断一个字符串是否“可能”存在于集合中。public boolean mightContain(String value) {for (int seed : seeds) {HashFunction hash = new HashFunction(size, seed);if (!bitSet.get(hash.hash(value))) {return false; // 一定不存在}}return true; // 可能存在}// 实现一个简单的哈希函数,将字符串映射到位数组的一个索引上。static class HashFunction {private int cap;private int seed;public HashFunction(int cap, int seed) {this.cap = cap;this.seed = seed;}// public int hash(String value) {int result = 0;for (int i = 0; i < value.length(); i++) {// 种子 * 每次遍历的返回值 + 字符串的字符位置result = seed * result + value.charAt(i);}return (cap - 1) & result;}}
}
应用
1. 缓存系统:防止缓存穿透
- 在 Redis 等缓存系统中,如果黑客频繁查询不存在的 key,会导致大量请求打到数据库。
解决方案:优秀实现Redisson中的 Bloom Filter。
- 将所有合法 key 加入布隆过滤器。
- 查询前,先过布隆过滤器:
- 若返回“不存在” → 直接返回,不查数据库。
- 若返回“可能存在” → 查缓存 → 查数据库。
2. 数据库优化:减少磁盘 I/O
- 数据库(如 LevelDB、Cassandra)用布隆过滤器判断某个 key 是否可能存在于某个 SSTable 文件中,避免不必要的磁盘读取。
3. 网络爬虫:去重 URL
- 爬虫在抓取网页时,用布隆过滤器记录已抓取的 URL,避免重复抓取,节省带宽和资源。
4. 垃圾邮件过滤
- 将已知的垃圾邮件地址加入布隆过滤器,快速判断新邮件是否可能是垃圾邮件。
应用图解
最佳实践:Bloom Filter
Redisson:基于 Redis 的 Java 客户端
- 不仅封装了 Redis 的基本操作,还提供了许多高级分布式数据结构和工具,其中就包括 分布式布隆过滤器(Bloom Filter)。
1. 底层存储:Redis 的 String 或 Hash 结构
Redisson 使用 Redis 的 位图(Bitmap) 功能来模拟布隆过滤器的位数组。
每个布隆过滤器对应一个 Redis Key。
这个 Key 的值是一个大的位数组(bit array),每一位代表一个“槽”。
Redis 的 SETBIT 和 GETBIT 命令用于设置和读取位。
2. 哈希函数:MurmurHash3
Redisson 使用 MurmurHash3 算法作为基础哈希函数。
只需一次计算,即可生成多个不同的哈希值(通过种子偏移)。
高效且分布均匀,适合布隆过滤器场景。
3. 原子性保障:Lua 脚本
所有操作(添加、查询)都通过 Redis 的 Lua 脚本执行,确保原子性。
// 可抽取为配置文件
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);// 获取布隆过滤器实例
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("user:exists");// 初始化:预计100万个用户,允许3%误判率
bloomFilter.tryInit(100000L, 0.03);// 添加元素
bloomFilter.add("user123");
bloomFilter.add("user456");// 判断是否存在
boolean mightExist = bloomFilter.contains("user123"); // true
boolean definitelyNotExist = bloomFilter.contains("user999"); // falseredisson.shutdown();
小结
- 布隆过滤器是一种用空间换时间、并接受一定错误率的巧妙设计。
- 它不能告诉你“一定存在”,但可以非常高效地告诉你“一定不存在”。
各位再见!这里是 鳄鱼杆的空间,钓……鳄鱼的杆儿!
期待下次再会!
愿你的每一次垂钓之旅都能满载而归。