最近公司对所有请求响应的数据进行推送kafka到大数据,然后落入数仓进行数据分析, 但是要解决请求重复推送的问题,可以容忍重复存在但是不能太多,所以就想到了bitmap和已经成型的布隆过滤器
操作
String数据结构的key所存储的字符串指定偏移量上的位,返回原位置的值
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YXQ8sGx6-1670483948742)(D:\学习乐园\文档总结\杂记\picture\bitmap.png)]](/uploadfile/202403/61a74c8bc788a14.png)
优点
bit位来表示某个元素对应的值或者状态,其中key就是对应元素的值。实际上8个bit可以组成一个Byte,所以是及其节省空间的。setbit和getbit的时间复杂度都是O(1),其他位运算效率也高。缺点
0和1的区别,所以用位做业务数据记录,就不需要在意value的值。使用场景
可作为简单的布尔过滤器来判断用户是否执行过某些操作;
可以计算用户日活、月活、留存率的统计;
可以统计用户在线状态和人数;
介绍布隆过滤器之前,先介绍一下哈希函数,我们在Java中的HashMap,HashSet也接触过hashcode()这个函数。
哈希函数指将哈希表中元素的关键键值通过一定的函数关系映射为元素存储位置的函数。
哈希函数的特点
布隆过滤器实际上是一个非常长的二进制向量(bitmap)和一系列随机哈希函数。
布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。 Bloom Filter(BF)是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。
它是一个判断元素是否存在集合的快速的概率算法。Bloom Filter有可能会出现错误判断,但不会漏掉判断。也就是Bloom Filter判断元素不再集合,那肯定不在。如果判断元素存在集合中,有一定的概率判断错误。因此,Bloom Filter”不适合那些“零错误的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter比其他常见的算法(如hash,折半查找)极大节省了空间。
优点
缺点
有一定的误判率
常见的补救办法是建立一个小的白名单,存储那些可能被误判的元素。但是如果元素数量太少,使用散列表足矣。
一般情况下不能从布隆过滤器中删除元素。
我们很容易想到把位列阵变成整数数组,每插入一个元素相应的计数器加1, 这样删除元素时将计数器减掉就可以了。然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面,这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。

![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KzrbZgG3-1670483948745)(D:\学习乐园\文档总结\杂记\picture\布隆过滤器设计流程.png)]](/uploadfile/202403/2abfe5668e822a2.png)
引入依赖
4.0.0 org.springframework.boot spring-boot-starter-parent 2.2.2.RELEASE com.example RedisBitMapProject 0.0.1-SNAPSHOT RedisBitMapProject RedisBitMapProject 1.8 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 com.google.guava guava 23.0 org.projectlombok lombok com.alibaba fastjson 2.0.20 org.redisson redisson-spring-boot-starter 3.18.0 org.springframework.boot spring-boot-maven-plugin
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 {@Beanpublic BloomFilter guavaBloomFilter(){//expectedInsertions:容量期望大小//fpp:期望的误判率,期望的误判率越低,布隆过滤器计算时间越长BloomFilter 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 {@Autowiredprivate RedissonClient redissonClient;@Beanpublic RBloomFilter redissonBloomFilter(){RBloomFilter redissonBloomFilter = redissonClient.getBloomFilter("redissonBloomFilter");//初始化布隆过滤器:预计元素为1000L,误差率为0.00001redissonBloomFilter.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;@Beanpublic 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 {@Autowiredprivate StringRedisTemplate redisTemplate;/*** 实例化RedisAtomicLong* @return*/@Beanpublic 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 {@Autowiredprivate 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 {@Autowiredprivate 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);//判断是否可能存在该keyif(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 guavaBloomFilter;@Autowired@Qualifier("redissonBloomFilter")private RBloomFilter redissonBloomFilter;/*** guava布隆过滤器* @param requestVo* @return*/@PostMapping("/guava")public Object guavaBloomFilter(@RequestBody BloomFilterRequestVo requestVo){log.info("guava-requestVo:{}", JSON.toJSONString(requestVo));String key = buildKey(requestVo);//判断是否可能存在该keyif(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);//判断是否可能存在该keyif(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 list = Collections.synchronizedList(new ArrayList<>());@Autowiredprivate 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 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进行调起测试
结果
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DJi8JObQ-1670483948746)(D:\学习乐园\文档总结\杂记\picture\image-20221208151649700.png)]](/uploadfile/202403/93e7970538ac693.png)