使用 Guava 布隆过滤器防止缓存穿透(含 Spring Boot 实现代码,黑马点评)

        在高并发场景下,缓存是提升系统性能的关键技术之一。但当恶意用户或爬虫访问大量不存在的数据时,会导致所有请求直接打到数据库,缓存无法生效,进而导致数据库压力陡增,这种现象被称为缓存穿透

        本文将介绍 布隆过滤器(Bloom Filter)+缓存空值 如何有效防止缓存穿透,并基于 Spring Boot + MyBatis-Plus + Redis 进行代码实现。

1. 什么是缓存穿透?

缓存穿透 是指:

  • 客户端请求的数据在缓存和数据库中都不存在。
  • 由于缓存没有这个数据,所有请求都会直接打到数据库。
  • 如果请求量大,会严重影响数据库性能,甚至导致宕机。

示例:

假设我们的数据库里只有 ID=1,2,3 的店铺信息,但某些用户频繁请求 不合理ID,如ID=-1

客户端请求 ID=-1 -->  缓存无数据 -->  数据库也无数据 -->  请求直接打数据库

这些请求永远不会被缓存命中,导致 数据库负载过高

布隆过滤器的特点:

操作说明
可能存在数据可能存在,但不能 100% 确定
一定不存在该数据绝对不存在

布隆过滤器的核心思想是存储的是元素的哈希值,可以快速判断一个元素是否可能存在,这样可以有效拦截非法请求,减少对数据库的直接访问。

3. 在 Spring Boot 项目中实现布隆过滤器

3.1 引入 Guava 依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version> <!-- 最新版本 -->
</dependency>

3.2 初始化布隆过滤器

在应用启动时,从数据库加载所有合法的店铺 ID,并存入布隆过滤器

package com.hmdp.Interceptor;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.List;
import java.util.stream.Collectors;

@Component
@Slf4j
public class BloomFilterInitializer {
    @Autowired
    private ShopMapper shopMapper;

    private static BloomFilter<Long> bloomFilter;

    @PostConstruct
    public void initBloomFilter() {
        // 1. 从数据库加载所有存在的店铺ID
        List<Long> ids = shopMapper.selectObjs(
                        Wrappers.<Shop>query().select("id")
                ).stream()
                .map(obj -> ((Number) obj).longValue())  // 避免 BigInteger 转换错误
                .collect(Collectors.toList());

        // 2. 初始化布隆过滤器(预期元素 100 万,误判率 1%)
        bloomFilter = BloomFilter.create(
                Funnels.longFunnel(),
                1_000_000L,
                0.01
        );

        // 3. 将数据库中的店铺 ID 加入布隆过滤器
        ids.forEach(bloomFilter::put);
        log.info("布隆过滤器初始化完成,共存入 {} 个店铺 ID", ids.size());
    }

    public static BloomFilter<Long> getBloomFilter() {
        return bloomFilter;
    }
}
📌 代码解析
  • selectObjs(Wrappers.<Shop>query().select("id")):从数据库查询所有店铺 ID
  • BloomFilter.create(Funnels.longFunnel(), 1_000_000L, 0.01)
    • 预计存储 100 万 个 ID。
    • 误判率 1%,即1% 的可能性会误判为“可能存在”

3.3 在查询逻辑中使用布隆过滤器

ShopServiceImpl 中修改 queryById(Long id) 方法,先通过布隆过滤器判断 ID 是否可能存在

@Override
public Result queryById(Long id) {
    // 0. 使用布隆过滤器拦截不存在的 ID
    if (!BloomFilterInitializer.getBloomFilter().mightContain(id)) {
        log.debug("店铺不存在,id={}", id);
        return Result.fail("店铺不存在!");
    }

    String key = RedisConstants.CACHE_SHOP_KEY + id;
    // 1. 先查询 Redis 缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);

    // 2. 缓存命中,直接返回
    if (StrUtil.isNotBlank(shopJson)) {
        return Result.ok(JSONUtil.toBean(shopJson, Shop.class));
    }

    // 3. Redis 已缓存空值(防止缓存穿透)
    if (shopJson != null) {
        return Result.fail("店铺不存在");
    }

    // 4. 查询数据库
    Shop shop = getById(id);
    if (shop == null) {
        // 缓存空值(短时间缓存,防止缓存穿透)
        stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
        return Result.fail("店铺不存在");
    }

    // 5. 写入 Redis
    stringRedisTemplate.opsForValue().set(
            key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES
    );

    return Result.ok(shop);
}

4. 方案对比

方案缓存穿透风险适用场景额外开销
不做处理低并发、小型系统
缓存空值ID 范围有限的场景占用 Redis 内存
布隆过滤器极低高并发、大数据量误判 1%,但影响较小

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值