布隆过滤器使用教程
文章目录
- 布隆过滤器使用教程
- 1.背景
- 2.什么是Bitmap
- 3.布隆过滤器
- 3.1 什么是布隆过滤器
- 3.2 布隆过滤器的作用
- 3.3 布隆过滤器的基本原理
- 4.布隆过滤器的实现Guava和Redisson
- 4.1 实现思路
- 4.2 SpringBoot实现这些操作Bitmap,guava,redisson布隆过滤器
1.背景
最近公司对所有请求响应的数据进行推送kafka到大数据,然后落入数仓进行数据分析, 但是要解决请求重复推送的问题,可以容忍重复存在但是不能太多,所以就想到了bitmap和已经成型的布隆过滤器
2.什么是Bitmap
操作
String
数据结构的key
所存储的字符串指定偏移量上的位,返回原位置的值
优点
- 节省空间:通过一个
bit
位来表示某个元素对应的值或者状态,其中key
就是对应元素的值。实际上8个bit
可以组成一个Byte
,所以是及其节省空间的。 - 效率高:
setbit
和getbit
的时间复杂度都是O(1),其他位运算效率也高。
缺点
- 本质上位只有
0
和1
的区别,所以用位做业务数据记录,就不需要在意value
的值。
使用场景
-
可作为简单的布尔过滤器来判断用户是否执行过某些操作;
-
可以计算用户日活、月活、留存率的统计;
-
可以统计用户在线状态和人数;
3.布隆过滤器
3.1 什么是布隆过滤器
介绍布隆过滤器之前,先介绍一下哈希函数,我们在Java中的HashMap,HashSet也接触过hashcode()这个函数。
哈希函数指将哈希表中元素的关键键值通过一定的函数关系映射为元素存储位置的函数。
哈希函数的特点
- 如果根据同一个哈希函数得到的哈希值不同,那么这两个哈希值的原始输入值肯定不同
- 如果根据同一个哈希函数得到的两个哈希值相等,两个哈希值的原始输入值有可能相等,有可能不相等
布隆过滤器实际上是一个非常长的二进制向量(bitmap)和一系列随机哈希函数。
布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。 Bloom Filter(BF)是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。
它是一个判断元素是否存在集合的快速的概率算法。Bloom Filter有可能会出现错误判断,但不会漏掉判断。也就是Bloom Filter判断元素不再集合,那肯定不在。如果判断元素存在集合中,有一定的概率判断错误。因此,Bloom Filter”不适合那些“零错误的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter比其他常见的算法(如hash,折半查找)极大节省了空间。
优点
- 布隆过滤器存储空间和插入/查询时间都是常数
- Hash函数相互之间没有关系,方便由硬件并行实现
- 布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势
- 布隆过滤器可以表示全集,其它任何数据结构都不能
缺点
-
有一定的误判率
常见的补救办法是建立一个小的白名单,存储那些可能被误判的元素。但是如果元素数量太少,使用散列表足矣。
-
一般情况下不能从布隆过滤器中删除元素。
我们很容易想到把位列阵变成整数数组,每插入一个元素相应的计数器加1, 这样删除元素时将计数器减掉就可以了。然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面,这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。
3.2 布隆过滤器的作用
- 解决Redis缓存穿透
- 邮件过滤,使用布隆过滤器来做邮件黑名单过滤
- 解决视频推荐过的不再推荐
3.3 布隆过滤器的基本原理
- 布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。
- 检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
- Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。
步骤
- 首先,建立一个二进制向量,并将所有位设置为0。
- 然后,选定K个散列函数,用于对元素进行K次散列,计算向量的位下标。
- 添加元素:当添加一个元素到集合中时,通过K个散列函数分别作用于元素,生成K个值作为下标,并将向量的相应位设置为1。
- 检查元素:如果要检查一个元素是否存在集合中,用同样的散列方法,生成K个下标,并检查向量的相应位是否全部是1。如果全为1,则该元素很可能在集合中;否则(只要有1个或以上的位为0),该元素肯定不在集合中。
4.布隆过滤器的实现Guava和Redisson
4.1 实现思路
4.2 SpringBoot实现这些操作Bitmap,guava,redisson布隆过滤器
引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>RedisBitMapProject</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>RedisBitMapProject</name>
<description>RedisBitMapProject</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--pool2,redis连接池使用-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--guava 布隆过滤器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.20</version>
</dependency>
<!--redisson 布隆过滤器-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.18.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
yml配置
server:
port: 20010
#redis????
spring:
redis:
host: localhost #redis??
port: 6379 #redis??
database: 0 #redis??(0-15,???0)
timeout: 1000 #redis??????
lettuce: #??lettuce???
pool:
max-active: 20 #????????(??????????)
max-wait: -1 #???????????(??????????)
min-idle: 0 #???????????
max-idle: 10 #???????????
guava布隆过滤器配置类
package com.example.bitmap.config;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.nio.charset.Charset;
/**
* @Author Emperor Kang
* @ClassName BloomFilterConfig
* @Description guava布隆过滤器配置类
* @Date 2022/12/7 15:37
* @Version 1.0
* @Motto 让营地比你来时更干净
*/
@Configuration
public class GuavaBloomFilterConfig {
@Bean
public BloomFilter<String> guavaBloomFilter(){
//expectedInsertions:容量期望大小
//fpp:期望的误判率,期望的误判率越低,布隆过滤器计算时间越长
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("UTF-8")), 1000, 0.000001);
return bloomFilter;
}
}
redisson布隆过滤器配置类
package com.example.bitmap.config;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author Emperor Kang
* @ClassName RedissonBloomFilterConfig
* @Description RedissonBloomFilterConfig
* @Date 2022/12/7 17:39
* @Version 1.0
* @Motto 让营地比你来时更干净
*/
@Configuration
public class RedissonBloomFilterConfig {
@Autowired
private RedissonClient redissonClient;
@Bean
public RBloomFilter<String> redissonBloomFilter(){
RBloomFilter<String> redissonBloomFilter = redissonClient.getBloomFilter("redissonBloomFilter");
//初始化布隆过滤器:预计元素为1000L,误差率为0.00001
redissonBloomFilter.tryInit(1000L,0.00001);
return redissonBloomFilter;
}
}
redisson配置
package com.example.bitmap.config;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
/**
* @Author Emperor Kang
* @ClassName RedissonConfig
* @Description redisson配置类
* @Date 2022/12/7 17:19
* @Version 1.0
* @Motto 让营地比你来时更干净
*/
@Component
@Configuration
@Slf4j
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://"+host+":"+port);
return Redisson.create(config);
}
}
redis配置类
package com.example.bitmap.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.support.atomic.RedisAtomicLong;
@Configuration
public class RedisConfig {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 实例化RedisAtomicLong
* @return
*/
@Bean
public RedisAtomicLong redisAtomicLong() {
RedisAtomicLong redisAtomicLong = new RedisAtomicLong("bitMapIncreaseKey", redisTemplate.getConnectionFactory());
return redisAtomicLong;
}
}
utils
package com.example.bitmap.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.support.atomic.RedisAtomicLong;
import org.springframework.stereotype.Component;
/**
* @Author Emperor Kang
* @ClassName RedisUtil
* @Description redis工具类
* @Date 2022/12/7 10:19
* @Version 1.0
* @Motto 让营地比你来时更干净
*/
@Component
@Slf4j
public class RedisUtil {
@Autowired
private RedisAtomicLong redisAtomicLong;
/**
* 生成自增的偏移量序号
* @return
*/
public Long bitMapOffsetSequence(){
long sequence = 0L;
try {
if(redisAtomicLong.get() == 0){
redisAtomicLong.getAndSet(0L);
}
sequence = redisAtomicLong.incrementAndGet();
log.info("当前序号为:{}",sequence);
} catch (Exception e) {
log.error("RedisUtil.bitMapOffsetSequence生成自增偏移量时发生异常",e);
throw e;
}
return sequence;
}
}
controller
package com.example.bitmap.controller;
import com.alibaba.fastjson.JSON;
import com.example.bitmap.vo.BloomFilterRequestVo;
import com.google.common.hash.Funnels;
import com.google.common.hash.Hashing;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.Charset;
/**
* @Author Emperor Kang
* @ClassName BitMapController
* @Description TODO
* @Date 2022/12/8 10:36
* @Version 1.0
* @Motto 让营地比你来时更干净
*/
@RestController
@RequestMapping("/bitmap")
@Slf4j
public class BitMapController {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* bitmap去重操作
* @param requestVo
* @return
*/
@RequestMapping("/distinct")
public Object distinctData(@RequestBody BloomFilterRequestVo requestVo){
log.info("bitmap-requestVo:{}", JSON.toJSONString(requestVo));
String key = buildKey(requestVo);
long offset = hash(key);
log.info("当前的offset:{}",offset);
//判断是否可能存在该key
if(redisTemplate.opsForValue().getBit(key,offset)){
log.info("该key:{}可能已经存在,所以不再推送kafka",key);
}else{
log.info("该key:{}不存在,开始推送kafka,将该key写入布隆过滤器",key);
redisTemplate.opsForValue().setBit(key,offset,true);
}
return true;
}
private String buildKey(BloomFilterRequestVo requestVo) {
return requestVo.getApplySeq() + "_" + requestVo.getInterfaceCode();
}
/**
* guava依赖获取hash值。
*/
private long hash(String key) {
Charset charset = Charset.forName("UTF-8");
return Math.abs(Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asInt());
}
}
package com.example.bitmap.controller;
import com.alibaba.fastjson.JSON;
import com.example.bitmap.vo.BloomFilterRequestVo;
import com.google.common.hash.BloomFilter;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author Emperor Kang
* @ClassName BloomFilterController
* @Description 布隆过滤器
* @Date 2022/12/7 15:44
* @Version 1.0
* @Motto 让营地比你来时更干净
*/
@RestController
@RequestMapping("bloomFilter")
@Slf4j
public class BloomFilterController {
@Autowired
@Qualifier("guavaBloomFilter")
private BloomFilter<String> guavaBloomFilter;
@Autowired
@Qualifier("redissonBloomFilter")
private RBloomFilter<String> redissonBloomFilter;
/**
* guava布隆过滤器
* @param requestVo
* @return
*/
@PostMapping("/guava")
public Object guavaBloomFilter(@RequestBody BloomFilterRequestVo requestVo){
log.info("guava-requestVo:{}", JSON.toJSONString(requestVo));
String key = buildKey(requestVo);
//判断是否可能存在该key
if(guavaBloomFilter.mightContain(key)){
log.info("该key:{}可能已经存在,所以不再推送kafka",key);
}else{
log.info("该key:{}不存在,开始推送kafka,将该key写入布隆过滤器",key);
guavaBloomFilter.put(key);
}
return true;
}
/**
* guava布隆过滤器
* @param requestVo
* @return
*/
@PostMapping("/redisson")
public Object redissonBloomFilter(@RequestBody BloomFilterRequestVo requestVo){
log.info("redisson-requestVo:{}", JSON.toJSONString(requestVo));
String key = buildKey(requestVo);
//判断是否可能存在该key
if(redissonBloomFilter.contains(key)){
log.info("该key:{}可能已经存在,所以不再推送kafka",key);
}else{
log.info("该key:{}不存在,开始推送kafka,将该key写入布隆过滤器",key);
redissonBloomFilter.add(key);
}
return true;
}
private String buildKey(BloomFilterRequestVo requestVo) {
return requestVo.getApplySeq() + "_" + requestVo.getInterfaceCode();
}
}
package com.example.bitmap.controller;
import com.example.bitmap.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @Author Emperor Kang
* @ClassName RedisController
* @Description TODO
* @Date 2022/12/7 10:21
* @Version 1.0
* @Motto 让营地比你来时更干净
*/
@RestController
@RequestMapping("/redis")
@Slf4j
public class RedisController {
public static final List<Long> list = Collections.synchronizedList(new ArrayList<>());
@Autowired
private RedisUtil redisUtil;
/**
* 自增主键ID
* @return
*/
@RequestMapping("incrId")
public Object generateIncrId(){
Long offsetSequence = redisUtil.bitMapOffsetSequence();
list.add(offsetSequence);
return true;
}
/**
* 查看生成的主键ID是否有重复
*/
@RequestMapping("/distinct")
public void getDis(){
log.info("当前集合size:{}",list.size());
List<Long> collect = list.stream()
.collect(Collectors.toMap(e -> e, e -> 1, Integer::sum))
.entrySet()
.stream()
.filter(e -> e.getValue() > 1)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
log.info("重复的数据为{}",collect);
}
}
jmeter进行调起测试
结果