布隆过滤器 + 多级缓存防穿透

当面临高并发查询时,如果用户或恶意脚本疯狂请求数据库中根本不存在的资源 ID,这些请求会穿透缓存直接打到数据库上,导致数据库瞬间压力飙升甚至宕机。这就是经典的缓存穿透问题。

本文将结合实际项目代码,分享如何通过 “布隆过滤器 + Redis 白名单 + 空对象缓存兜底” 的多级缓存策略,优雅且彻底地解决这一痛点。

什么是布隆过滤器 + 多级缓存?

布隆过滤器

布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否可能存在于一个集合中。

多级缓存架构

多级缓存策略是一种分层缓存设计,通常包括:

  • 第一级 :布隆过滤器(拦截 99% 的无效请求)
  • 第二级 :Redis 空对象缓存(拦截布隆过滤器的 1% 误判请求)
  • 第三级 :Redis 白名单 Set(精确判断有效资源)
  • 第四级 :数据库(最终兜底并进行缓存预热)

为什么需要布隆过滤器 + 多级缓存?

传统缓存逻辑的痛点

传统的缓存逻辑是:先查 Redis,Redis 没有再查 DB,DB 查到了写入 Redis 并返回,DB 没有则直接返回。 如果有人恶意遍历-1, -2, 9999999根本不存在的id,Redis 永远查不到,请求全部落到 DB。

解决方案

  1. 只用 Redis 缓存空对象:如果恶意攻击使用海量不同的无效 ID,Redis 会缓存大量空对象,最终导致内存撑爆。
  2. 只用布隆过滤器:布隆过滤器可以极其高效地判断一个 ID “绝对不存在”或“可能存在”。但它存在一定的误判率,即它认为存在的 ID,数据库里其实没有。
  3. 最终的方案就是通过三道防线来解决:
    • 第一道防线:布隆过滤器拦截大多数请求。
    • 第二道防线:空对象缓存缓存那布隆过滤器放过的小部分请求。
    • 第三道防线:Redis 白名单 Set缓存真正存在的ID进行放行。

核心代码实现

在项目中,我们在 UserAppointmentServiceImpl.java 中落地了这套多级缓存策略

初始化布隆过滤器

在 Spring 容器启动时,利用 @PostConstruct 初始化 Redisson 提供的布隆过滤器,并预热数据库中已有的资源 ID。

    // 布隆过滤器Key
    private static final String BLOOM_FILTER_RESOURCE = "bloom:resource";

    private RBloomFilter<Long> resourceBloomFilter;

    @PostConstruct
    public void init() {
        // 1. 初始化布隆过滤器
        resourceBloomFilter = redissonClient.getBloomFilter(BLOOM_FILTER_RESOURCE);
        // 2. 初始化预期插入量为 10000,误判率为 0.03 (3%)
        resourceBloomFilter.tryInit(10000L, 0.03);
        
        // 3. 启动时预加载一次(将数据库所有可用资源ID加载到布隆和 Redis Set 中)
        preprocessResources("RESOURCE");
    }

核心逻辑实现

当用户发起预约或查询请求时,我们严格按照防线顺序进行拦截。

    @Override
    public void submitAppointment(AppointmentDTO dto) {
        String type = dto.getType();
        Long resourceId = dto.getResourceId();
        String nullCacheKey = "campus:null:cache:" + type + ":" + resourceId;
        String redisKey = "campus:resource:available"; // Redis 白名单 Set Key

        // ================= 多级缓存防穿透校验 =================
        
        // 防线 0:先检查【空对象缓存】(针对布隆误判或已确认不存在的ID)
        if (Boolean.TRUE.equals(redisTemplate.hasKey(nullCacheKey))) {
            throw new RuntimeException("该资源不存在或已下架");
        }

        // 防线 1:【布隆过滤器】拦截(高效拦截绝大多数不存在的ID)
        // 如果布隆说没有,那绝对没有,直接拒绝!
        if (!resourceBloomFilter.contains(resourceId)) {
            throw new RuntimeException("该资源不存在或已下架");
        }

        // 防线 2:检查【Redis 白名单 Set】
        String idStr = resourceId.toString();
        Boolean exists = redisTemplate.opsForSet().isMember(redisKey, idStr);
        
        if (Boolean.FALSE.equals(exists)) {
            // 如果布隆说有,但 Redis Set 说没有 -> 可能是 Redis 缓存过期,也可能是布隆误判
            // 尝试懒加载预热(从数据库刷新最新有效资源到 Redis Set)
            preprocessResources(type);
            
            // 再次检查 Redis Set
            exists = redisTemplate.opsForSet().isMember(redisKey, idStr);
            
            // 防线 3:如果依然没有 -> 说明这是布隆过滤器的【误判】,或者是刚下架的资源
            if (Boolean.FALSE.equals(exists)) {
                // 写入【空对象缓存】,有效期 5 分钟
                // 这样未来 5 分钟内,即使布隆误判,也会被防线 0 拦截,不会再触发查 DB 预热
                redisTemplate.opsForValue().set(nullCacheKey, "", 5, TimeUnit.MINUTES);
                throw new RuntimeException("该资源不存在或已下架");
            }
        }
    }

避坑

  • 空对象缓存定期清理,不然会占满Redis的内存,可以设置TTL。
  • 布隆过滤器是会有误判的,一定要处理误判的逻辑。
  • 布隆过滤器不支持删除元素,必须引入Redis白名单Set,当资源下架的时候从Set中删除对应的Id。

总结

通过 布隆过滤器 -> Redis 白名单 Set -> 空对象缓存 -> 数据库的结构,实现了一个优雅的能够防止缓存穿透的业务接口。

  • 布隆过滤器以极小的内存代价挡住了 99% 的无效流量。
  • Redis 白名单保证了热点数据的高速处理。
  • 空对象缓存解决了布隆过滤器误判的情况。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇