一、 什么是布隆過濾器
介紹布隆過濾器之前,先介紹一下哈希函數(shù),我們在Java中的HashMap,HashSet也接觸過hashcode()這個(gè)函數(shù)。
哈希函數(shù)指將哈希表中元素的關(guān)鍵鍵值通過一定的函數(shù)關(guān)系映射為元素存儲位置的函數(shù)。
哈希函數(shù)的特點(diǎn):
- 如果根據(jù)同一個(gè)哈希函數(shù)得到的哈希值不同,那么這兩個(gè)哈希值的原始輸入值肯定不同
- 如果根據(jù)同一個(gè)哈希函數(shù)得到的兩個(gè)哈希值相等,兩個(gè)哈希值的原始輸入值有可能相等,有可能不相等
布隆過濾器實(shí)際上是一個(gè)非常長的二進(jìn)制向量(bitmap)和一系列隨機(jī)哈希函數(shù)。
布隆過濾器(英語:Bloom Filter)是1970年由布隆提出的。它實(shí)際上是一個(gè)很長的二進(jìn)制向量和一系列隨機(jī)映射函數(shù)。布隆過濾器可以用于檢索一個(gè)元素是否在一個(gè)集合中。它的優(yōu)點(diǎn)是空間效率和查詢時(shí)間都遠(yuǎn)遠(yuǎn)超過一般的算法,缺點(diǎn)是有一定的誤識別率和刪除困難。
Bloom Filter(BF)是一種空間效率很高的隨機(jī)數(shù)據(jù)結(jié)構(gòu),它利用位數(shù)組很簡潔地表示一個(gè)集合,并能判斷一個(gè)元素是否屬于這個(gè)集合。
它是一個(gè)判斷元素是否存在集合的快速的概率算法。Bloom Filter有可能會出現(xiàn)錯誤判斷,但不會漏掉判斷。也就是Bloom Filter判斷元素不再集合,那肯定不在。如果判斷元素存在集合中,有一定的概率判斷錯誤。因此,Bloom Filter”不適合那些“零錯誤的應(yīng)用場合。而在能容忍低錯誤率的應(yīng)用場合下,Bloom Filter比其他常見的算法(如hash,折半查找)極大節(jié)省了空間。
優(yōu)點(diǎn):
- 布隆過濾器存儲空間和插入/查詢時(shí)間都是常數(shù)
- Hash函數(shù)相互之間沒有關(guān)系,方便由硬件并行實(shí)現(xiàn)
- 布隆過濾器不需要存儲元素本身,在某些對保密要求非常嚴(yán)格的場合有優(yōu)勢
- 布隆過濾器可以表示全集,其它任何數(shù)據(jù)結(jié)構(gòu)都不能
缺點(diǎn):
- 有一定的誤判率
常見的補(bǔ)救辦法是建立一個(gè)小的白名單,存儲那些可能被誤判的元素。但是如果元素?cái)?shù)量太少,使用散列表足矣。
- 一般情況下不能從布隆過濾器中刪除元素。
我們很容易想到把位列陣變成整數(shù)數(shù)組,每插入一個(gè)元素相應(yīng)的計(jì)數(shù)器加1, 這樣刪除元素時(shí)將計(jì)數(shù)器減掉就可以了。然而要保證安全的刪除元素并非如此簡單。首先我們必須保證刪除的元素的確在布隆過濾器里面,這一點(diǎn)單憑這個(gè)過濾器是無法保證的。另外計(jì)數(shù)器回繞也會造成問題。
二、布隆過濾器的作用
布隆過濾器可以用于檢索一個(gè)元素是否在一個(gè)集合中,常用于解決如下問題
- 解決Redis緩存穿透
- 郵件過濾,使用布隆過濾器來做郵件黑名單過濾
- 解決視頻推薦過的不再推薦
三、布隆過濾器的基本原理
布隆過濾器的原理是,當(dāng)一個(gè)元素被加入集合時(shí),通過K個(gè)散列函數(shù)將這個(gè)元素映射成一個(gè)位數(shù)組中的K個(gè)點(diǎn),把它們置為1。
檢索時(shí),我們只要看看這些點(diǎn)是不是都是1就(大約)知道集合中有沒有它了:如果這些點(diǎn)有任何一個(gè)0,則被檢元素一定不在;如果都是1,則被檢元素很可能在。這就是布隆過濾器的基本思想。
Bloom Filter跟單哈希函數(shù)Bit-Map不同之處在于:Bloom Filter使用了k個(gè)哈希函數(shù),每個(gè)字符串跟k個(gè)bit對應(yīng)。從而降低了沖突的概率。

步驟:
- 首先,建立一個(gè)二進(jìn)制向量,并將所有位設(shè)置為0。
- 然后,選定K個(gè)散列函數(shù),用于對元素進(jìn)行K次散列,計(jì)算向量的位下標(biāo)。
- 添加元素:當(dāng)添加一個(gè)元素到集合中時(shí),通過K個(gè)散列函數(shù)分別作用于元素,生成K個(gè)值作為下標(biāo),并將向量的相應(yīng)位設(shè)置為1。
- 檢查元素:如果要檢查一個(gè)元素是否存在集合中,用同樣的散列方法,生成K個(gè)下標(biāo),并檢查向量的相應(yīng)位是否全部是1。如果全為1,則該元素很可能在集合中;否則(只要有1個(gè)或以上的位為0),該元素肯定不在集合中。
四、在Spring Boot中集成Redisson實(shí)現(xiàn)布隆過濾器

4.1 添加maven依賴
不再需要spring-boot-starter-data-redis依賴,但是都添加也不會報(bào)錯
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.5</version>
</dependency>
4.2 配置yml
http://www.itdecent.cn/p/00ddb0187468
4.3 配置RedissonConfig
http://www.itdecent.cn/p/00ddb0187468
4.4 工具類BloomFilterUtil
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @Author: huangyibo
* @Date: 2022/7/25 17:21
* @Description:
*/
@Component
public class BloomFilterUtil {
@Resource
private RedissonClient redissonClient;
/**
* 創(chuàng)建布隆過濾器
*
* @param filterName 過濾器名稱
* @param expectedInsertions 預(yù)測插入數(shù)量
* @param falsePositiveRate 誤判率
*/
public <T> RBloomFilter<T> create(String filterName, long expectedInsertions, double falsePositiveRate) {
RBloomFilter<T> bloomFilter = redissonClient.getBloomFilter(filterName);
bloomFilter.tryInit(expectedInsertions, falsePositiveRate);
return bloomFilter;
}
}
4.5 編寫service實(shí)現(xiàn)層
其它層正常編寫即可,與之前并無差別,此處不再展示
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @Author: huangyibo
* @Date: 2022/7/25 18:44
* @Description:
*/
@Service
public class UserServiceImpl {
// 預(yù)期插入數(shù)量
static long expectedInsertions = 200L;
// 誤判率
static double falseProbability = 0.01;
// 非法請求所返回的JSON
static String illegalJson = "[\"com.company.springboot.entity.User\",{\"id\":null,\"userName\":\"null\",\"sex\":null,\"age\":null}]";
private RBloomFilter<Long> bloomFilter = null;
@Resource
private BloomFilterUtil bloomFilterUtil;
@Resource
private RedissonClient redissonClient;
@Resource
private UserMapper userMapper;
@PostConstruct // 項(xiàng)目啟動的時(shí)候執(zhí)行該方法,也可以理解為在spring容器初始化的時(shí)候執(zhí)行該方法
public void init() {
// 啟動項(xiàng)目時(shí)初始化bloomFilter
List<User> userList = this.list();
bloomFilter = bloomFilterUtil.create("idWhiteList", expectedInsertions, falseProbability);
for (User user : userList) {
bloomFilter.add(user.getId());
}
}
@Cacheable(cacheNames = "user", key = "#id", unless = "#result==null")
public User findById(Long id) {
// bloomFilter中不存在該key,為非法訪問
if (!bloomFilter.contains(id)) {
System.out.println("所要查詢的數(shù)據(jù)既不在緩存中,也不在數(shù)據(jù)庫中,為非法key");
/**
* 設(shè)置unless = "#result==null"并在非法訪問的時(shí)候返回null的目的是不將該次查詢返回的null使用
* RedissonConfig-->RedisCacheManager-->RedisCacheConfiguration-->entryTtl設(shè)置的過期時(shí)間存入緩存。
*
* 因?yàn)槟嵌螘r(shí)間太長了,在那段時(shí)間內(nèi)可能該非法key又添加到bloomFilter,比如之前不存在id為1234567的用戶,
* 在那段時(shí)間可能剛好id為1234567的用戶完成注冊,使該key成為合法key。
*
* 所以我們需要在緩存中添加一個(gè)可容忍的短期過期的null或者是其它自定義的值,使得短時(shí)間內(nèi)直接讀取緩存中的該值。
*
* 因?yàn)镾pring Cache本身無法緩存null,因此選擇設(shè)置為一個(gè)其中所有值均為null的JSON,
*/
redissonClient.getBucket("user::" + id, new StringCodec()).set(illegalJson, new Random().nextInt(200) + 300, TimeUnit.SECONDS);
return null;
}
// 不是非法訪問,可以訪問數(shù)據(jù)庫
System.out.println("數(shù)據(jù)庫中得到數(shù)據(jù)*****");
return userMapper.selectById(id);
}
// 先執(zhí)行方法體中的代碼,成功執(zhí)行之后刪除緩存
@CacheEvict(cacheNames = "user", key = "#id")
public boolean delete(Long id) {
// 刪除數(shù)據(jù)庫中具有的數(shù)據(jù),就算此key從此之后不再出現(xiàn),也不能從布隆過濾器刪除
return userMapper.deleteById(id) == 1;
}
// 如果緩存中先前存在,則更新緩存;如果不存在,則將方法的返回值存入緩存
@CachePut(cacheNames = "user", key = "#user.id")
public User update(User user) {
userMapper.updateById(user);
// 新生成key的加入布隆過濾器,此key從此合法,因?yàn)樵摳路椒ú⒉桓耰d,所以也不會產(chǎn)生新的合法的key
bloomFilter.add(user.getId());
return user;
}
@CachePut(cacheNames = "user", key = "#user.id")
public User insert(User user) {
userMapper.insert(user);
// 新生成key的加入布隆過濾器,此key從此合法
bloomFilter.add(user.getId());
return user;
}
}
注意:
redisson利用redis存儲,布隆過濾器生成數(shù)組,但是長度限制為 4 294 967 296 ,但是根據(jù)布隆過濾器的原理來看,生成的數(shù)組長度是沒有限制的,判斷應(yīng)該是redis String類型最大是512M所導(dǎo)致的限制。
五、在Spring Boot中集成RedisTemplate實(shí)現(xiàn)布隆過濾器
首先是pom.xml文件
加入我們這次使用redis & BloomFilter 的核心依賴包
<!--使用Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--借助guava的布隆過濾器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
5.2 配置yml
spring:
redis:
database: 3
host: 127.0.0.1
port: 6379
password: 12345
jedis.pool.max-idle: 100
jedis.pool.max-wait: -1ms
jedis.pool.min-idle: 2
timeout: 2000ms
5.3 RedisConfig.class
如果是一般的使用redis存字符串的話,使用StringRedisTemplate,就不需要配置序列化。
但是咱們這里使用的是RedisTemplate<String, Object> redisTemplate ,存儲的是對象,所以為了防止存入的對象值在查看的時(shí)候不顯示亂碼,就需要配置相關(guān)的序列化(其實(shí)我們存的bit結(jié)構(gòu)數(shù)據(jù),布隆過濾器存值分分鐘都是百萬級別的,會因?yàn)閿?shù)據(jù)量太大redis客戶端也沒辦法顯示,不過不影響使用)。
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Charsets;
import com.google.common.hash.Funnel;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
/**
* @Author: huangyibo
* @Date: 2022/7/25 19:00
* @Description:
*/
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheManager redisCacheManager = RedisCacheManager.create(connectionFactory);
return redisCacheManager;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//序列化設(shè)置 ,這樣計(jì)算是正常顯示的數(shù)據(jù),也能正常存儲和獲取
redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(factory);
return stringRedisTemplate;
}
//初始化布隆過濾器,放入到spring容器里面
@Bean
public BloomFilterHelper<String> initBloomFilterHelper() {
return new BloomFilterHelper<>((Funnel<String>) (from, into) -> into.putString(from, Charsets.UTF_8).putString(from, Charsets.UTF_8), 1000000, 0.01);
}
}
5.4 BloomFilterHelper .calss:
import com.google.common.base.Preconditions;
import com.google.common.hash.Funnel;
import com.google.common.hash.Hashing;
/**
* @Author: huangyibo
* @Date: 2022/7/25 18:59
* @Description:
*/
public class BloomFilterHelper<T> {
private int numHashFunctions;
private int bitSize;
private Funnel<T> funnel;
public BloomFilterHelper(Funnel<T> funnel, int expectedInsertions, double fpp) {
Preconditions.checkArgument(funnel != null, "funnel不能為空");
this.funnel = funnel;
// 計(jì)算bit數(shù)組長度
bitSize = optimalNumOfBits(expectedInsertions, fpp);
// 計(jì)算hash方法執(zhí)行次數(shù)
numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize);
}
public int[] murmurHashOffset(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;
}
/**
* 計(jì)算bit數(shù)組長度
*/
private int optimalNumOfBits(long n, double p) {
if (p == 0) {
// 設(shè)定最小期望長度
p = Double.MIN_VALUE;
}
int sizeOfBitArray = (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
return sizeOfBitArray;
}
/**
* 計(jì)算hash方法執(zhí)行次數(shù)
*/
private int optimalNumOfHashFunctions(long n, long m) {
int countOfHash = Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
return countOfHash;
}
}
5.5 RedisBloomFilter.class
然后是具體的布隆過濾器配合redis使用的 方法類
import com.google.common.base.Preconditions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
/**
* @Author: huangyibo
* @Date: 2022/7/25 19:02
* @Description:
*/
@Component
public class RedisBloomFilter<T> {
@Autowired
private RedisTemplate<String, T> redisTemplate;
/**
* 根據(jù)給定的布隆過濾器添加值
*/
public <T> void addByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能為空");
int[] offset = bloomFilterHelper.murmurHashOffset(value);
for (int i : offset) {
System.out.println("key : " + key + " " + "value : " + i);
redisTemplate.opsForValue().setBit(key, i, true);
}
}
/**
* 根據(jù)給定的布隆過濾器判斷值是否存在
*/
public <T> boolean includeByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能為空");
int[] offset = bloomFilterHelper.murmurHashOffset(value);
for (int i : offset) {
System.out.println("key : " + key + " " + "value : " + i);
if (!redisTemplate.opsForValue().getBit(key, i)) {
return false;
}
}
return true;
}
}
六、在Spring Boot中使用lua腳本的集成RedisTemplate實(shí)現(xiàn)布隆過濾器
bf_add.lua
local bloomName = KEYS[1]
local value = KEYS[2]
local result = redis.call('BF.ADD',bloomName,value)
return result
bf_exist.lua
local bloomName = KEYS[1]
local value = KEYS[2]
local result = redis.call('BF.EXISTS',bloomName,value)
return result
實(shí)現(xiàn)代碼
@Service
public class RedisBloomFilterService {
@Autowired
private RedisTemplate redisTemplate;
//我們依舊用剛剛的那個(gè)過濾器
public static final String BLOOMFILTER_NAME = "test-bloom-filter";
/**
* 向布隆過濾器添加元素
* @param str
* @return
*/
public Boolean bloomAdd(String str) {
DefaultRedisScript<Boolean> LuaScript = new DefaultRedisScript<Boolean>();
LuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("bf_add.lua")));
LuaScript.setResultType(Boolean.class);
//封裝傳遞腳本參數(shù)
List<String> params = new ArrayList<String>();
params.add(BLOOMFILTER_NAME);
params.add(str);
return (Boolean) redisTemplate.execute(LuaScript, params);
}
/**
* 檢驗(yàn)元素是否可能存在于布隆過濾器中 * @param id * @return
*/
public Boolean bloomExist(String str) {
DefaultRedisScript<Boolean> LuaScript = new DefaultRedisScript<Boolean>();
LuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("bf_exist.lua")));
LuaScript.setResultType(Boolean.class);
//封裝傳遞腳本參數(shù)
ArrayList<String> params = new ArrayList<String>();
params.add(BLOOMFILTER_NAME);
params.add(String.valueOf(str));
return (Boolean) redisTemplate.execute(LuaScript, params);
}
}
參考:
https://blog.csdn.net/dreaming9420/article/details/124153422
https://developer.aliyun.com/article/951745
https://www.cnblogs.com/woshixiangshang/p/11412474.html