携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情
背景
上一章讲解了如何解决缓存雪崩、击穿、穿透问题,针对缓存穿透的问题,我们可以采取布隆过滤器来解决,本文将讲解布隆过滤器的原理和使用。
简介
布隆过滤器一种基于概率的数据结构,主要用来判断某个元素是否在集合内,它能够告诉你某个元素一定不在集合内或可能在集合内。
应用场景
- Redis缓存穿透问题
- 邮件过滤,使用布隆过滤器来做邮件黑名单过滤
- 对爬虫网址进行过滤
- 解决新闻推荐过的不再推荐问题
- HBase\RocksDB\LevelDB等数据库内置布隆过滤器,用于判断数据是否存在,可以减少数据库的IO请求
基本原理
布隆过滤器是一个位数组(与bitmap数据结构类似)和K个映射函数,在初始状态时,对于长度为M的位数组array,它的所有位置都被初始为0。当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点(offset)把它们置为1,检索时如果这些点有任何一个0,则被检元素一定不在,如果都是1则被检元素很可能存在。
如下图所示:
说明:如上图中的Bloom Filter假设具有三个哈希函数,预先存入a、b、c三个元素并根据哈希函数计算出不同的bit位,将这些bit位置都为1。
-
判断c,根据三个哈希函数计算出不同的bit位置,发现这些位置的bit值都是1,Bloom Filter返回true表示该元素存在,实际上该元素确实存在。
-
判断d,根据三个哈希函数计算出不同的bit位置,发现函数hash3(d)结果对应的的bit索引位的值是0,Bloom Filter返回false表示该元素不存在,实际上该元素确实不存在。
-
判断e,根据三个哈希函数计算出不同的bit位置,发现这些位置的bit值都是1,Bloom Filter返回true表示该元素存在,实际上该元素是不存在,说明此时发生了误判。
具体实现
基于Guava实现
采用Google的Guava实现的布隆过滤器(Bloom Filter)来实现。
jar包引入
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
测试示例
public class BLFilter
{
private static Logger logger=LoggerFactory.getLogger(BLFilter.class);
public static void main(String[] args)
{
// 1.创建布隆过滤器,预期数据量10000,错误率0.0001
BloomFilter<CharSequence> bloomFilter =
BloomFilter.create(Funnels.stringFunnel(
Charset.forName("utf-8")),10000, 0.0001);
// 2.添加数据
for (int i = 0; i < 5000; i++)
{
bloomFilter.put("" + i);
}
logger.info("数据插入完毕");
// 3.测试结果输出
for (int i = 0; i < 10000; i++)
{
if (bloomFilter.mightContain(String.valueOf(i)))
{
logger.info(i + "存在");
}
else
{
logger.info(i + "不存在");
}
}
}
}
说明:
基于Redisson实现
引入jar包
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.7</version>
</dependency>
具体实现
@Service
public class BloomFilterService
{
@Autowired
private RedissonClient redissonClient;
public <T> RBloomFilter<T> create(String filterName, long expectedInsertions, double falseProbability) {
RBloomFilter<T> bloomFilter = redissonClient.getBloomFilter(filterName);
bloomFilter.tryInit(expectedInsertions, falseProbability);
return bloomFilter;
}
}
说明:如果是Redis Cluster的集群环境,则需要采用如下方式:
`RClusteredBloomFilter<SomeObject> bloomFilter = redisson.getClusteredBloomFilter("sample");`
测试代码
@RequestMapping("/bloomFilterTest")
public void bloomFilterTest()
{
// 预期插入数量
long expectedInsertions = 10000L;
// 错误比率
double falseProbability = 0.01;
RBloomFilter<Object> bloomFilter = bloomFilterService.create("ipBlackList", expectedInsertions, falseProbability);
// 布隆过滤器增加元素
for (long i = 0; i < expectedInsertions; i++)
{
bloomFilter.add("js"+i);
}
//用来统计误判的个数
int count = 0;
//查询不存在的数据一千次
for (int i = 0; i < 1000; i++) {
if (bloomFilter.contains("zhangsan" + i)) {
count++;
}
}
logger.info("判断错误的个数:"+count);
logger.info("js1是否在过滤器中存在:"+bloomFilter.contains("js1"));
logger.info("js222是否在过滤器中存在:"+bloomFilter.contains("js222"));
logger.info("预计插入数量:" + bloomFilter.getExpectedInsertions());
logger.info("容错率:" + bloomFilter.getFalseProbability());
logger.info("hash函数的个数:" + bloomFilter.getHashIterations());
logger.info("插入对象的个数:" + bloomFilter.count());
}
输出结果
优缺点
优点
- 时间复杂度低,增加和查询元素的时间复杂为O(N)
- 保密性强,布隆过滤器不存储元素本身
- 存储空间小,非常节省空间的
缺点
- 存在一定的误判率,可以通过调整参数来降低。
- 无法获取元素本身
- 删除元素困难,因为删除会把相应的k个bits位置为0,而其中很有可能有其他元素执行哈希计算之后也会对应该bit位,从而造成更多的误判.
总结
本文详细讲解了布隆过滤器的原理和实现,如有疑问请随时提问。