Redis + SpringBoot:缓存穿透解决方案实战

发布于:2025-07-25 ⋅ 阅读:(22) ⋅ 点赞:(0)

技术栈:Spring Boot 2.7 + Redis 6 + Redisson 3.17
压力测试:JMeter模拟10万QPS场景下的性能对比

一、缓存穿透的致命危害

1.1 典型事故案例
某电商平台遭遇恶意攻击,攻击者持续请求不存在的商品ID:
数据库QPS:从500激增至12万
CPU负载:达到800%
恢复时间:38分钟(手动介入后)

1.2 穿透 vs 击穿 vs 雪崩
在这里插入图片描述

二、六大解决方案深度实战

2.1 方案一:空对象缓存(基础防御)
实现代码:

@Cacheable(value = "products", key = "#id", 
           unless = "#result == null") // 不缓存null结果
public Product getProduct(Long id) {
    Product product = productDao.findById(id);
    if (product == null) {
        // 缓存空对象(设置较短TTL)
        redisTemplate.opsForValue().set(
            "product:null:" + id, 
            new NullValue(), 
            5, TimeUnit.MINUTES);
    }
    return product;
}

优化技巧:

使用特殊标记对象(如NullValue)而非字符串"null"
动态调整空对象TTL:夜间时段延长至30分钟

2.2 方案二:布隆过滤器(终极防御)
Guava实现:

// 初始化布隆过滤器(预期元素量100万,误判率1%)
private static final BloomFilter<Long> productBloomFilter = 
    BloomFilter.create(Funnels.longFunnel(), 1_000_000, 0.01);

// 启动时加载所有有效ID
@PostConstruct
public void initBloomFilter() {
    productDao.findAllIds().forEach(productBloomFilter::put);
}

// 查询拦截
public Product getProductWithBloom(Long id) {
    if (!productBloomFilter.mightContain(id)) {
        throw new IllegalArgumentException("商品不存在");
    }
    return getProduct(id); // 走正常缓存逻辑
}

Redisson分布式布隆过滤器:

RBloomFilter<Long> filter = redissonClient.getBloomFilter("productFilter");
filter.tryInit(1_000_000L, 0.01); // 分布式环境下使用

2.3 方案三:互斥锁重建(防击穿)

public Product getProductWithLock(Long id) {
    String lockKey = "product:lock:" + id;
    try {
        // 尝试获取分布式锁
        boolean locked = redissonClient.getLock(lockKey).tryLock(3, 10, TimeUnit.SECONDS);
        if (locked) {
            // 双重检查
            Product product = getFromCache(id);
            if (product == null) {
                product = loadFromDB(id);
                updateCache(id, product);
            }
            return product;
        }
        // 未获取锁则短暂休眠后重试
        Thread.sleep(50);
        return getProductWithLock(id);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException("获取商品信息中断");
    }
}

2.4 方案四:缓存预热(防雪崩)

@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行
public void preloadHotProducts() {
    List<Long> hotIds = productDao.findHotProductIds(1000);
    hotIds.parallelStream().forEach(id -> {
        Product product = productDao.findById(id);
        redisTemplate.opsForValue().set(
            "product:" + id, 
            product, 
            12 + ThreadLocalRandom.current().nextInt(6), // 12-18小时随机过期
            TimeUnit.HOURS);
    });
}

2.5 方案五:多级缓存架构

// Caffeine本地缓存 + Redis二级缓存
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
    return new CaffeineRedisCacheManager(
        Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(5, TimeUnit.MINUTES),
        RedisCacheWriter.nonLockingRedisCacheWriter(factory),
        RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(1))
    );
}

2.6 方案六:弹性过期时间

// 动态TTL策略
public void setWithRandomTtl(String key, Object value) {
    int baseTtl = 3600; // 基础1小时
    int randomTtl = ThreadLocalRandom.current().nextInt(600); // 0-10分钟随机值
    redisTemplate.opsForValue().set(
        key, 
        value, 
        baseTtl + randomTtl, 
        TimeUnit.SECONDS);
}

三、性能压测对比

3.1 测试环境
硬件:4核8G云服务器
数据量:100万商品数据
攻击模式:90%请求为不存在的随机ID

3.2 结果对比
在这里插入图片描述

四、生产环境注意事项

4.1 布隆过滤器维护

// 数据变更时同步更新
@Transactional
public void updateProduct(Product product) {
    productDao.update(product);
    if (productBloomFilter != null) {
        productBloomFilter.put(product.getId());
    }
}

4.2 监控指标配置

management:
  metrics:
    export:
      redis:
        enabled: true
    distribution:
      percentiles:
        redis.command.execution: 0.5,0.95,0.99

五、终极解决方案组合

推荐架构:

前端:请求参数基础校验(如ID范围检查)
网关层:频率限制+黑名单过滤
服务层:布隆过滤器+空对象缓存
存储层:多级缓存+弹性TTL


网站公告

今日签到

点亮在社区的每一天
去签到