Spring Boot集成Redisson布隆過濾器

一、 什么是布隆過濾器

介紹布隆過濾器之前,先介紹一下哈希函數(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

https://blog.csdn.net/wuliang20/article/details/122874716

https://blog.csdn.net/qq_14855971/article/details/123650368

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容