如何用布隆过滤器防止缓存穿透?

wen java案例 65

本文目录导读:

如何用布隆过滤器防止缓存穿透?

  1. 第一步:先理解什么是缓存穿透
  2. 第二步:布隆过滤器的核心原理
  3. 第三步:如何用布隆过滤器防止缓存穿透
  4. 第四步:代码示例(基于Google Guava + Redis + Spring Boot)
  5. 第五步:布隆过滤器的优缺点
  6. 总结:何时使用布隆过滤器?

这是一个非常经典的分布式系统设计问题。布隆过滤器是防止缓存穿透的最有效手段之一。

下面我会用通俗的语言,结合一个极简的代码示例,详细解释其原理和实现步骤。

第一步:先理解什么是缓存穿透

正常流程:

  1. 客户端请求数据。
  2. 先去 Redis(缓存)里查。
  3. 缓存有:直接返回,很快。
  4. 缓存没有:去数据库查,把结果写回缓存,再返回。

缓存穿透: 是指缓存和数据库中都没有的数据被持续请求。

  • 攻击者伪造了一个 ID = -1 的用户,或者一个不存在的 ID。
  • 每次请求都直接打到了数据库。
  • 如果并发极高(如秒杀、爬虫攻击),数据库会因为扛不住查询压力而宕机。
  • 本质:缓存失去了“保护数据库”的作用。

第二步:布隆过滤器的核心原理

布隆过滤器本质上是一个很长的二进制位数组多个哈希函数

  • 特性
    1. 判断“肯定不存在”:非常准确,如果布隆过滤器说“不存在”,那这个数据绝对不存在
    2. 判断“可能存在”:有一定误判率,如果布隆过滤器说“存在”,这个数据可能真的存在,也可能不存在(哈希冲突导致)。
  • 工作流程
    • 添加数据:将数据(如用户ID)通过多个哈希函数计算出多个位置,把这些位置的位设置成 1。
    • 查询数据:对查询的数据同样进行哈希计算,检查对应位是否全部为 1。
      • 只要有任意一位是 0:数据一定不存在
      • 如果全部是 1:数据可能存在

第三步:如何用布隆过滤器防止缓存穿透

这是一个标准的“双保险”流程:

架构图(文字版): 客户端 -> 布隆过滤器 -> 缓存 (Redis) -> 数据库 (MySQL) (挡在第一道) (第二道) (最后防线)

具体步骤:

初始化(项目启动时) 预先将数据库中所有存在的业务ID(如用户ID、商品ID),全部加载到布隆过滤器中。

// 伪代码 - 初始化布隆过滤器
BloomFilter<Long> bloomFilter = BloomFilter.create(Funnels.longFunnel(), expectedInsertions, falsePositiveRate);
// 从数据库读取所有存在的ID,并存入布隆过滤器
List<Long> allUserIds = userMapper.getAllIds();
for (Long id : allUserIds) {
    bloomFilter.put(id);
}

请求处理流程

  1. 第一步:拦截(核心步骤)

    • 用户请求一个 ID。
    • 先问布隆过滤器bloomFilter.mightContain(ID)
    • 如果是 false:布隆过滤器说“这个 ID 绝对不在数据库里”。
      • 直接拒绝:返回空结果或错误信息。不查缓存,不查数据库
    • 如果是 true:布隆过滤器说“这个 ID 可能存在”。
  2. 第二步:查询缓存

    • 如果布隆过滤器放行,再去查 Redis。
    • Redis 有,直接返回。
    • Redis 没有,去查数据库。
  3. 第三步:查数据库并回写

    • 数据库查到了:写回缓存,返回数据。
    • 数据库没查到:这是一个罕见的“幽灵”情况(布隆过滤器误判导致),此时为了防止下一次同样请求再次穿透,可以在 Redis 中缓存一个空值(设置较短的过期时间)。

流程图:

请求来了(ID = -1)
    |
    v
布隆过滤器: mightContain(-1) ?
    |
    +-- false (不存在) --------> 直接返回 "无效ID"  (保护了数据库)
    |
    +-- true (可能存在)
            |
            v
        查缓存 (Redis)
            |
            +-- 有数据 -----> 返回 (正常)
            |
            +-- 无数据
                |
                v
            查数据库 (MySQL)
                |
                +-- 有数据 -----> 回写Redis并返回
                |
                +-- 无数据 -----> 缓存一个空值到Redis (防止下一次穿透)
                                  (设置较短的过期时间,如5分钟)

第四步:代码示例(基于Google Guava + Redis + Spring Boot)

@Service
public class UserService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private UserMapper userMapper;
    // 1. 项目启动时初始化布隆过滤器(实际应该用@PostConstruct或者在启动类中执行)
    private BloomFilter<Long> bloomFilter;
    @PostConstruct
    public void init() {
        // 预计插入100万条数据,误判率设为1%
        this.bloomFilter = BloomFilter.create(Funnels.longFunnel(), 1000000, 0.01);
        List<Long> allIds = userMapper.getAllUserIds();
        allIds.forEach(id -> bloomFilter.put(id));
        log.info("布隆过滤器初始化完成,共加载{}个ID", allIds.size());
    }
    public User getUserById(Long id) {
        // 步骤1:布隆过滤器前置拦截
        if (!bloomFilter.mightContain(id)) {
            // 一定不存在,直接返回null(防止穿透)
            return null;
        }
        // 步骤2:查缓存
        String cacheKey = "user:" + id;
        String json = redisTemplate.opsForValue().get(cacheKey);
        if (StringUtils.hasText(json)) {
            return JSON.parseObject(json, User.class);
        }
        // 步骤3:数据库查询
        User user = userMapper.selectById(id);
        // 步骤4:回写缓存(包括空值缓存)
        if (user != null) {
            redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user), Duration.ofHours(1)); // 正常数据1小时过期
            return user;
        } else {
            // 数据库里也没有,但布隆过滤器说可能存在(误判)
            // 为了防穿透,缓存一个空值,设置短过期时间
            redisTemplate.opsForValue().set(cacheKey, "", Duration.ofMinutes(5));
            return null;
        }
    }
}

第五步:布隆过滤器的优缺点

优点:

  • 内存占用极小:1亿条数据,误判率1%时,大约只占 114 MB 内存。
  • 查询速度极快:O(k) 复杂度,k 是哈希函数个数(通常几个到十几个)。
  • 完全防止了无效请求到达数据库:这是最核心的价值。

缺点:

  • 有误判率:可能把不存在的 ID 判断为存在(导致一次无效的缓存+数据库查询,但不会穿透到数据库导致崩溃)。
    • 解决办法:接受,因为误判率可控(如0.01%)。
  • 无法删除元素:标准的布隆过滤器不支持删除,如果数据库删掉了一个ID,布隆过滤器里对应的位无法清零(因为可能被多个ID复用)。
    • 解决办法
      1. 定时重建:每天凌晨低峰期,重新加载数据库全量ID到新的布隆过滤器,然后切换。
      2. 使用 Counting Bloom Filter(布谷鸟过滤器):支持删除,但更耗内存。
      3. 只做第一步拦截:即便有误判,后面的缓存空值机制也能兜底。
  • 需要预热:项目重启后,需要先加载所有ID,否则新布隆过滤器是空的,正常请求也会被误拦。

何时使用布隆过滤器?

场景 推荐度 说明
高并发、数据在DB中持续存在(如有ID的字典表) 强烈推荐 提前一次加载完所有ID,能完美防穿透。
动态新增、删除频繁的数据 谨慎使用 需要定时重建,或者配合布谷鸟过滤器。
数据量极小 用不着 直接缓存空值或加锁即可。
误判导致业务无法接受 不建议 比如实时对账,错判会导致一笔交易被拦截。

一句话总结: 布隆过滤器像一个“黑名单”守卫,说“不在”你就一定不在,它用极小的内存,把绝大部分无效请求挡在数据库之外,是防止缓存穿透的终极防线。

抱歉,评论功能暂时关闭!