3. 缓存穿透问题:定义、产生原因、危害分析、布隆过滤器原理与实现

3.1 缓存穿透的定义与产生原因

缓存穿透是指客户端请求的数据在缓存层和数据库层中都不存在,导致每次请求都直接穿透缓存层,直达数据库层。由于缓存层无法命中,所有请求压力全部落在数据库上,形成“穿透”效应。

对比维度 缓存穿透 缓存击穿 缓存雪崩
数据存在性 缓存和数据库均不存在 缓存过期,数据库存在 缓存大面积过期,数据库存在
请求特征 大量请求查询不存在的数据 高并发请求同一热点 key 大量 key 同时失效
根本原因 恶意攻击或业务漏洞 热点 key 过期 缓存集中失效
影响范围 数据库连接耗尽 单点数据库压力 数据库整体崩溃

产生原因主要包括以下三类:

  • 恶意攻击:攻击者构造大量不存在的 ID(如负数、超大值、随机字符串)发起请求,绕过缓存直接攻击数据库。
  • 业务逻辑缺陷:查询参数未做合法性校验,例如用户 ID 为 null 或格式错误时直接查询数据库。
  • 数据删除未同步:数据库中的数据已被删除,但缓存未及时清理,导致后续请求直接查库。
⚠️ 风险提示:缓存穿透是三种缓存异常中最容易被忽视的一种。很多团队只关注缓存击穿和雪崩,却忽略了“查不到”的请求同样能拖垮数据库。一次 DDoS 攻击利用缓存穿透,可在 10 秒内耗尽数据库连接池。

3.2 危害分析

缓存穿透的危害呈现指数级放大特征:

  • 数据库连接耗尽:每个穿透请求都会占用一个数据库连接,当并发量超过连接池上限时,数据库拒绝服务。
  • 响应延迟飙升:数据库 CPU 和 IO 被打满,正常请求的响应时间从毫秒级上升到秒级甚至超时。
  • 级联故障:数据库崩溃后,依赖该数据库的其他服务(如订单、支付)也会连锁故障。
  • 成本浪费:云数据库按连接数和 QPS 计费,穿透请求直接造成无意义的资源消耗。
📊 典型数据:某电商平台在促销期间遭遇缓存穿透攻击,10 分钟内数据库 QPS 从 2000 飙升至 50000,导致数据库连接池溢出,核心交易链路中断 23 分钟,直接经济损失超过 200 万元。

3.3 布隆过滤器原理

布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否可能存在于集合中。它由一个位数组和多个哈希函数组成。

核心原理:

  1. 初始化:创建一个长度为 m 的位数组,所有位初始化为 0。
  2. 添加元素:使用 k 个哈希函数对元素计算哈希值,将位数组中对应位置置为 1。
  3. 查询元素:同样使用 k 个哈希函数计算哈希值,检查对应位是否全部为 1。如果任意一位为 0,则元素一定不存在;如果全部为 1,则元素可能存在(存在误判率)。
💡 关键特性:布隆过滤器不会漏报(如果元素不存在,一定返回不存在),但可能误报(如果元素存在,可能返回不存在)。误判率可以通过调整位数组大小和哈希函数数量来控制。
布隆过滤器工作原理 输入元素 Hash 1 Hash 2 Hash k 位数组(m 位) 1 0 1 0 1 0 1 0 0 1 0 1 0 0 1 0 查询元素 检查对应位 全部为 1 → 可能存在 存在 0 → 一定不存在 误判率 p ≈ (1 - e^(-kn/m))^k,其中 n 为元素数量,k 为哈希函数数量,m 为位数组长度

3.4 布隆过滤器实现

在 Spring Boot 项目中,推荐使用 Google Guava 提供的 BloomFilter 实现。以下是完整的集成步骤和代码示例。

步骤一:添加 Maven 依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.3-jre</version>
</dependency>

步骤二:初始化布隆过滤器

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
public class UserIdBloomFilter {

    // 预计插入 100 万条数据,误判率 1%
    private static final int EXPECTED_INSERTIONS = 1_000_000;
    private static final double FPP = 0.01;

    private BloomFilter<Long> bloomFilter;

    @PostConstruct
    public void init() {
        bloomFilter = BloomFilter.create(
            Funnels.longFunnel(),
            EXPECTED_INSERTIONS,
            FPP
        );
        // 模拟从数据库加载已有用户 ID
        for (long i = 1; i <= 100_000; i++) {
            bloomFilter.put(i);
        }
    }

    public boolean mightContain(Long userId) {
        return bloomFilter.mightContain(userId);
    }

    public void put(Long userId) {
        bloomFilter.put(userId);
    }
}

步骤三:在缓存查询中使用布隆过滤器

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private UserIdBloomFilter userIdBloomFilter;

    public User getUserById(Long userId) {
        // 第一步:布隆过滤器前置拦截
        if (!userIdBloomFilter.mightContain(userId)) {
            // 一定不存在,直接返回空,避免穿透
            return null;
        }

        // 第二步:查询缓存
        String cacheKey = "user:" + userId;
        String userJson = redisTemplate.opsForValue().get(cacheKey);
        if (userJson != null) {
            return JSON.parseObject(userJson, User.class);
        }

        // 第三步:查询数据库
        User user = userMapper.selectById(userId);
        if (user != null) {
            // 写入缓存
            redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user), 1, TimeUnit.HOURS);
        }
        return user;
    }
}
💡 最佳实践:
  • 误判率设置:建议设置为 1% 以下,每降低一个数量级,位数组大小约增加 1.44 倍。
  • 动态更新:当新增用户时,需要同步调用 bloomFilter.put() 更新布隆过滤器。
  • 定期重建:布隆过滤器不支持删除元素,建议每天凌晨低峰期重建一次,从数据库全量加载。
  • 分布式场景:使用 Redis 的 BitMap 实现分布式布隆过滤器,避免单机内存限制。

3.5 缓存穿透解决方案对比

方案 原理 优点 缺点 适用场景
布隆过滤器 位数组 + 哈希函数前置过滤 空间效率极高,性能好 存在误判率,不支持删除 数据量大、ID 连续或可枚举
缓存空对象 查询不到的数据也缓存空值 实现简单,无误判 占用缓存空间,需设置短过期时间 数据量小、穿透频率低
参数校验 前置拦截非法参数 零成本,无副作用 只能拦截格式异常请求 所有场景必须做
限流降级 限制单位时间穿透请求数 保护数据库不被打满 影响正常请求,治标不治本 突发流量保护
🔧 推荐组合策略:参数校验(第一道防线)→ 布隆过滤器(第二道防线)→ 缓存空对象(第三道防线)→ 限流降级(兜底策略)。四层防护可有效抵御 99.9% 的缓存穿透攻击。