前几天看视频,无意中看到一个Redis缓存会存在三大问题,分别是缓存穿透,缓存击穿,缓存雪崩。在处理缓存穿透的时候,介绍了两种方案,推荐使用布隆过滤器,还有一个是Redis中缓存null。之前只是有听过布隆过滤器,但是对其原理和特点,使用场景并不熟悉。于是网上查阅了挺多资料。总结成为自己看得懂的一篇个人对于布隆过滤器的理解文章。
布隆过滤器,英文 Bloom Filter。简单理解是一个叫布隆的人开发的一个过滤器,过滤器我的理解就是为了进行逻辑上的过滤校验。布隆过滤器是一个用于校验巨量数据中是否存在某个值。布隆过滤器校验的结果特点是:
- 若过滤器判断某个元素存在,那么元素不一定存在
- 若过滤器判断某个元素不存在,那么元素就一定不存在
其中存在不存在的理解是指:需要校验的值是否在布隆过滤器中存在映射关系。
下面通过布隆过滤器数据结构与原理,实际Java代码示例来巩固理解。
1. 数据结构与原理
我理解的布隆过滤器的数据结构是bit类型的map,值通过计算hash值,获取bit数组中对应的下标,hash值和bit数组下标之间有map的映射关系。映射关系对应的bit数组的值由0改为1。图示如下:
根据图片来理解其实现的原理,其中URL1,URL2,URL3就是需要在bit数组中需要建立映射关系的数据,hash1,hash2,hash3就是值经过hash函数计算出来的下标值,计算出下标值后,对应的下标就将值由原来的0改为1,就是图中绿色所显示的部分。其实我对布隆过滤器的原理理解就是这么短短的几句话总结完了。详细总结:其中我将需要建立关系的值定义为key
- 创建一个bit数组,bit数组中的值都初始化为0
- 将需要建立映射关系的key在建立映射关系前都需要进行多次的hash函数计算【使用多次hash函数计算是为了减少hash冲突,提交验证存在的准确率】
- 得到hash之后,在根据计算规则,得到key位于bit数组中的下标
- 在得到bit数组下标后,就下标对应的值改为1
- 将所有需要建立映射关系的key重复刚才的逻辑,建立好映射关系
- 将需要检验的key进过刚才的hash函数计算出该key对应的bit数组下标,判断该下标对应的值是否为1
- 如果bit数组中下标对应的值为1,布隆过滤器就认为校验的key存在,如果bit数组对应的值为0,布隆过滤器就认为校验的key不存在
为什么校验存在的话,实际可能是不存在呢?因为hash存在hash冲突导致结果不准!
2. 布隆过滤器的实现
我在网上看存在三种实现方式,我认为其中两种都是结合redis实现,我就将这两种归为一种。我的实现是基于redis中的一种,感兴趣的可以查阅相关资料。
2.1 Guava实现布隆过滤器
代码如下:
import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
/**
* @Author alen
* @DATE 2022/4/19 22:51
* 布隆过滤器测试
*/
public class BloomFilterTest {
/**
* 设置需要插入的数据量,100万条数据
*/
private static final int insertData = 1000000;
/**
* 设置误判率
*/
private static final double errorRatio = 0.02;
public static void main(String[] args) {
//初始化一个存储String类型的布隆过滤器,误判率设置为0.02,默认值是0.03
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), insertData, errorRatio);
//用于存放所有的key,现实场景中该数据来源于数据库,作用是用来判断是否存在
Set<String> setList = new HashSet<>(insertData);
//用于存放所有实际存在的key,用于取出
List<String> lists = new ArrayList<>(insertData);
//创建模拟数据
for (int i = 0; i < insertData; i++) {
String uuid = UUID.randomUUID().toString();
/**
* 向布隆过滤器中添加元素,跟踪put源码,里面有进行多次hash计算
*/
bloomFilter.put(uuid);
setList.add(uuid);
lists.add(uuid);
}
//统计判断实际存在的总个数
int rightCnt = 0;
//统计误以为存在实际不存在的个数
int wrongCnt = 0;
for (int i = 0; i < 1000; i++) {
String str = i % 100 == 0 ? lists.get(i / 100) : UUID.randomUUID().toString();
//mightContain(): 判断布隆过滤器中元素是否存在
if (bloomFilter.mightContain(str)) {
if (setList.contains(str)) {
rightCnt ++;
continue;
}
wrongCnt ++;
}
}
BigDecimal percent = new BigDecimal(wrongCnt).divide(new BigDecimal(990), 2, RoundingMode.HALF_UP);
BigDecimal bingo = new BigDecimal(990 - wrongCnt).divide(new BigDecimal(990), 2, RoundingMode.HALF_UP);
System.out.println("在1000000个元素中,布隆过滤器认为存在的:" + rightCnt +",误认为存在的:" + wrongCnt + ",命中率:" + bingo + ",误判率:" + percent);
}
}
Guava实现的布隆过滤器针有一定的局限性,由于现在基本上都是分布式部署服务,如果使用Guava实现的布隆过滤器,需要每个服务在启动的时候,都需要将目标数据加载到服务中,对内存资源的消耗较大。所以在一般真是的使用中,不会使用这种方式,而是采用基于Redis实现的模式。
2.2 基于Redis实现布隆过滤器
基于Redis实现布隆过滤器的话,需要我们自己造轮子了,实现代码如下。
1. 创建BloomFilterHelper类,该类是布隆过滤器的核心类,代码详情
import com.google.common.hash.Funnel;
import com.google.common.hash.Funnels;
import com.google.common.hash.Hashing;
import java.nio.charset.Charset;
/**
* @Author alen
* @DATE 2022/4/20 9:18
* Bloom Filter的核心类
*/
public class BloomFilterHelper<T> {
/**
* 定义hash函数执行次数
*/
private int numHashFunctions;
/**
* bit数组大小
*/
private int bitSize;
/**
* 数据类型
*/
private Funnel<T> funnel;
/**
* 设置默认的误判率
*/
private static final double errorRatio = 0.02;
/**
* 无参构造器
*/
public BloomFilterHelper() {
}
/**
* 构造方法
* @param insertions 需要插入的数据条数
*/
public BloomFilterHelper(int insertions) {
this.funnel = (Funnel<T>) Funnels.stringFunnel(Charset.defaultCharset());
bitSize = optimalBitsLength(insertions, errorRatio);
numHashFunctions = optimalHashFunctionsCnt(insertions, bitSize);
}
/**
* 构造方法
*
* @param funnel 数据类型
* @param insertions 插入的数据条数
* @param errorRatio 误判率
*/
public BloomFilterHelper(Funnel<T> funnel, int insertions, double errorRatio) {
this.funnel = funnel;
bitSize = optimalBitsLength(insertions, errorRatio);
numHashFunctions = optimalHashFunctionsCnt(insertions, bitSize);
}
/**
* hash计算
* 计算多次hash对应的下标
*
* @param value 需要hash的值
* @return 偏移量数组
*/
public int[] offsetArray(T value) {
int[] offset = new int[numHashFunctions];
long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong();
int hash1 = (int) hash64;
int hash2 = (int) (hash64 >>> 32);
for (int i = 1; i <= numHashFunctions; i++) {
int nextHash = hash1 + i * hash2;
if (nextHash < 0) {
nextHash = ~nextHash;
}
offset[i - 1] = nextHash % bitSize;
}
return offset;
}
/**
* 计算最佳的bit数组长度
*
* @param insertions 需要插入的数据总数
* @param errorRatio 误判率
* @return bit数组长度
*/
private int optimalBitsLength(long insertions, double errorRatio) {
if (errorRatio == 0) {
errorRatio = 0.03;
}
return (int) (-insertions * Math.log(errorRatio) / (Math.log(2) * Math.log(2)));
}
/**
* 计算hash方法执行的最佳次数
*
* @param insertions 插入数据总数
* @param bitSize bit数组大小
* @return 最佳hash次数
*/
private int optimalHashFunctionsCnt(long insertions, long bitSize) {
return Math.max(1, (int) Math.round((double) bitSize / insertions * Math.log(2)));
}
}
2. 创建RedisBloomFilter类,操作布隆过滤器,详情如下
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
/**
* @Author alen
* @DATE 2022/4/20 19:40
* redis操作布隆过滤器
*/
@Component
public class RedisBloomFilter {
@Resource
private RedisTemplate redisTemplate;
/**
* 删除缓存的key
*
* @param key 需要删除的key
*/
public void delete(String key) {
redisTemplate.delete(key);
}
/**
* 将值在布隆过滤器中添加映射关系
* 一个一个元素添加时使用
*
* @param bloomFilterHelper 布隆过滤器对象
* @param key key
* @param value value
* @param <T> 泛型
*/
public <T> void add(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
int[] offsetArray = bloomFilterHelper.offsetArray(value);
for (int offset : offsetArray) {
//setBit命令;
redisTemplate.opsForValue().setBit(key, offset, true);
}
}
/**
* 在布隆过滤器中添加值
*
* @param bloomFilterHelper 布隆过滤器
* @param key key
* @param valueList 值的集合
* @param <T> 泛型
*/
public <T> void addList(BloomFilterHelper<T> bloomFilterHelper, String key, List<T> valueList) {
redisTemplate.executePipelined(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
//开启连接
redisConnection.openPipeline();
for (T value : valueList) {
//hash计算
int[] offsetArray = bloomFilterHelper.offsetArray(value);
for (int offset : offsetArray) {
redisConnection.setBit(key.getBytes(), offset, true);
}
}
redisConnection.close();
return null;
}
});
}
/**
* 判断值是否存在
*
* @param bloomFilterHelper 布隆过滤器
* @param key key
* @param value 值
* @param <T> 泛型
* @return
*/
public <T> boolean isExists(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
int[] offsetArray = bloomFilterHelper.offsetArray(value);
for (int offset : offsetArray) {
//判断bit位的值是否为1,
if (!redisTemplate.opsForValue().getBit(key, offset)) {
return false;
}
}
return true;
}
}
以上就是布隆过滤器基于Redis实现的全部代码,测试部分可以自己写一个controller测试一下!
3. Spring Boot中使用布隆过滤器
3.1 引入redisson的jar包
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--redisSon-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.14.1</version>
</dependency>
3.2 bootstrap.yml中添加redis配置
spring:
redis:
host: 127.0.0.1
port: 6379
password: xxxxxx
database: 0
timeout: 1000
# 布隆过滤器配置
bloomfilter:
expected-insertions: 1000000
false-probability: 0.02
3.3 创建Redisson配置文件
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
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;
/**
* @Author alen
* @DATE 2022/3/27 11:13
*/
@Configuration
public class RedisSonConfiguration {
@Value("${spring.redis.host}")
private String address;
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.database}")
private String database;
@Value("${spring.redis.timeout}")
private String timeout;
//预估需要插入的值
@Value("${bloomfilter.expected-insertions}")
private Long expectedInsertions;
//容错率
@Value("${bloomfilter.false-probability}")
private double falseProbability;
@Bean
public RedissonClient redisson() {
Config config = new Config();
config.useSingleServer()
.setAddress(address())
.setPassword(password)
.setDatabase(Integer.parseInt(database))
.setConnectTimeout(Integer.parseInt(timeout));
return Redisson.create(config);
}
/**
* 注入布隆过滤器
* @return
*/
@Bean
public RBloomFilter bloomFilter() {
RBloomFilter<Object> bloomFilter = redisson().getBloomFilter("bloom-filter");
bloomFilter.tryInit(expectedInsertions, falseProbability);
return bloomFilter;
}
/**
* 生成address
* @return
*/
private String address() {
return "redis://" + address + ":" + port;
}
}
3.4 布隆过滤器中加载数据
import com.body.park.user.config.RedisSonConfiguration;
import com.body.park.user.entity.TUser;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
/**
* @Author alen
* @DATE 2022/4/21 9:01
*/
@Slf4j
@Component
public class InitBloomFilterData implements ApplicationRunner {
@Autowired
private RedisSonConfiguration redisSonConfiguration;
private RBloomFilter<Long> bloomFilter;
@PostConstruct
public void init() {
bloomFilter = redisSonConfiguration.bloomFilter();
}
/**
*
* @param args
* @throws Exception
*/
@Override
public void run(ApplicationArguments args) throws Exception {
List<TUser> userList = new ArrayList<>();
//模拟数据,真是数据从数据库中查询
for (long i = 0; i < 1000L; i++) {
TUser user = new TUser();
user.setId(i);
userList.add(user);
}
userList.forEach(tUser -> {
bloomFilter.add(tUser.getId());
});
}
}
3.5 在业务逻辑中使用
/**
* @Author alen
* @DATE 2022/3/13 20:32
*/
@DubboService
@Component
@Slf4j
public class UserTestServiceImpl implements UserTestService {
@Autowired
private RedisBloomFilter redisBloomFilter;
@Autowired
private RedisSonConfiguration redisSonConfiguration;
private RBloomFilter<Long> bloomFilter;
@PostConstruct
public void init() {
bloomFilter = redisSonConfiguration.bloomFilter();
}
@Override
public boolean checkBloomFilterData(Long value) {
boolean flag = bloomFilter.contains(value);
log.info("值:{},是否存在布隆过滤器中:{}", value, flag);
return flag;
}
}
然后写一个controller测试一下即可,到此所有的布隆过滤器相关的都已经梳理完成,有不足之处,欢迎讨论!