💥 缓存三大灾难:穿透、击穿、雪崩全攻略!

22 阅读14分钟

副标题:防御缓存攻击,守护系统稳定!🛡️


🎬 开场:那些年,我们遇到的缓存灾难

真实事故案例

案例1:某电商平台双11故障 💥

23:58  流量开始暴增
23:59  缓存大量key同时过期
00:00  数据库瞬间被打爆
00:01  响应时间从10ms飙升到5000ms
00:02  系统雪崩,服务不可用
00:05  紧急切换到降级页面
损失:约2000万交易额

根因:缓存雪崩 ❄️

案例2:某社交网站被攻击 🔥

10:00  黑客发起攻击
       大量查询不存在的用户ID
       user_999999999, user_888888888...
10:01  缓存全部未命中
10:02  请求全部打到数据库
10:03  数据库CPU 100%
10:05  数据库连接池耗尽
10:10  整个服务瘫痪

根因:缓存穿透 🕳️

📚 三大问题对比

问题类型现象原因影响
缓存穿透 🕳️查询不存在的数据缓存和DB都没有数据DB压力大
缓存击穿 💥热点key过期单个key过期瞬时压力
缓存雪崩 ❄️大量key同时过期批量过期系统崩溃

🕳️ 问题1:缓存穿透

什么是缓存穿透?

正常流程:
客户端 → 查询 user:1000
         ↓
       缓存命中
         ↓
       返回数据 ✅

缓存穿透:
客户端 → 查询 user:999999999 (不存在的ID)
         ↓
       缓存未命中 ❌
         ↓
       查询数据库
         ↓
       数据库也没有 ❌
         ↓
       无法缓存 (NULL不缓存)
         ↓
       每次都打到数据库 💥

攻击示例

/**
 * 模拟缓存穿透攻击
 */
public class CachePenetrationAttack {
    
    public static void attack() {
        ExecutorService executor = Executors.newFixedThreadPool(1000);
        
        // 发起10万次查询不存在的数据
        for (int i = 0; i < 100000; i++) {
            final int id = 999999900 + i;  // 不存在的ID
            
            executor.submit(() -> {
                // 每次都穿透到数据库
                userService.getUser(id);
            });
        }
        
        // 结果:数据库被打爆!💥
    }
}

解决方案

方案1:缓存空对象 ⭐⭐⭐

@Service
public class UserService {
    
    @Autowired
    private RedisTemplate<String, User> redisTemplate;
    
    @Autowired
    private UserMapper userMapper;
    
    // 空对象标记
    private static final User NULL_USER = new User();
    
    /**
     * 缓存空对象
     */
    public User getUser(Long userId) {
        String cacheKey = "user:" + userId;
        
        // 1. 查询缓存
        User user = redisTemplate.opsForValue().get(cacheKey);
        
        // 2. 如果是空对象,直接返回null
        if (NULL_USER.equals(user)) {
            log.info("命中空对象缓存: {}", userId);
            return null;
        }
        
        if (user != null) {
            return user;
        }
        
        // 3. 查询数据库
        user = userMapper.selectById(userId);
        
        if (user != null) {
            // 缓存真实数据
            redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
        } else {
            // 缓存空对象(设置较短过期时间)
            redisTemplate.opsForValue().set(cacheKey, NULL_USER, 5, TimeUnit.MINUTES);
            log.info("缓存空对象: {}", userId);
        }
        
        return user;
    }
}

优缺点

优点 ✅:
- 实现简单
- 保护数据库

缺点 ❌:
- 占用缓存空间
- 可能缓存大量无效数据
- 攻击者可以构造大量不同的key

适用场景:
- 随机攻击
- 误操作

方案2:布隆过滤器 ⭐⭐⭐⭐⭐

/**
 * 布隆过滤器防止缓存穿透
 */
@Component
public class BloomFilterService {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    // 布隆过滤器key
    private static final String BLOOM_FILTER_KEY = "user:bloom:filter";
    
    // 预期插入数量
    private static final long EXPECTED_INSERTIONS = 10000000L;  // 1000万
    
    // 误判率
    private static final double FPP = 0.01;  // 1%
    
    /**
     * 初始化布隆过滤器
     */
    @PostConstruct
    public void initBloomFilter() {
        // 使用Redisson的布隆过滤器
        RBloomFilter<Long> bloomFilter = 
            redissonClient.getBloomFilter(BLOOM_FILTER_KEY);
        
        // 初始化
        bloomFilter.tryInit(EXPECTED_INSERTIONS, FPP);
        
        // 加载所有用户ID
        List<Long> allUserIds = userMapper.selectAllIds();
        allUserIds.forEach(bloomFilter::add);
        
        log.info("布隆过滤器初始化完成,加载{}个用户ID", allUserIds.size());
    }
    
    /**
     * 检查用户是否存在
     */
    public boolean userExists(Long userId) {
        RBloomFilter<Long> bloomFilter = 
            redissonClient.getBloomFilter(BLOOM_FILTER_KEY);
        
        return bloomFilter.contains(userId);
    }
}

@Service
public class UserServiceWithBloom {
    
    @Autowired
    private BloomFilterService bloomFilterService;
    
    /**
     * 使用布隆过滤器
     */
    public User getUser(Long userId) {
        // 1. 先用布隆过滤器判断
        if (!bloomFilterService.userExists(userId)) {
            log.info("布隆过滤器拦截: {}", userId);
            return null;  // 一定不存在
        }
        
        // 2. 可能存在,查询缓存
        String cacheKey = "user:" + userId;
        User user = redisTemplate.opsForValue().get(cacheKey);
        
        if (user != null) {
            return user;
        }
        
        // 3. 查询数据库
        user = userMapper.selectById(userId);
        
        if (user != null) {
            redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
        }
        
        return user;
    }
}

布隆过滤器原理

布隆过滤器原理:

1. 数据结构:
   位数组 + 多个hash函数
   
   例如:100位的数组
   [0,0,0,0,0,0,0,0,0,0,...,0,0,0]

2. 添加元素 "user:1000":
   hash1("user:1000") % 100 = 23  → 置1
   hash2("user:1000") % 100 = 67  → 置1
   hash3("user:1000") % 100 = 89  → 置1
   
   [0,0,...,1,...,1,...,1,...,0]
         ↑23    ↑67    ↑89

3. 检查元素 "user:1000":
   hash1("user:1000") % 100 = 23  → 检查位23
   hash2("user:1000") % 100 = 67  → 检查位67
   hash3("user:1000") % 100 = 89  → 检查位89
   
   如果3个位都是1 → 可能存在
   如果任何一个位是0 → 一定不存在 ✅

4. 误判:
   可能把不存在的判断为存在(hash碰撞)
   但绝不会把存在的判断为不存在

本地布隆过滤器(Guava)

/**
 * 本地布隆过滤器(适合数据量小的场景)
 */
@Component
public class LocalBloomFilter {
    
    private BloomFilter<Long> bloomFilter;
    
    @PostConstruct
    public void init() {
        // 创建布隆过滤器
        this.bloomFilter = BloomFilter.create(
            Funnels.longFunnel(),
            10000000L,  // 预期元素数量
            0.01        // 误判率
        );
        
        // 加载数据
        List<Long> allUserIds = userMapper.selectAllIds();
        allUserIds.forEach(bloomFilter::put);
        
        log.info("本地布隆过滤器初始化完成");
    }
    
    public boolean mightContain(Long userId) {
        return bloomFilter.mightContain(userId);
    }
    
    /**
     * 添加新用户时更新布隆过滤器
     */
    public void addUser(Long userId) {
        bloomFilter.put(userId);
    }
}

优缺点

优点 ✅:
- 内存占用极小
- 查询速度快 O(k)
- 能拦截大部分不存在的key

缺点 ❌:
- 存在误判(约1%)
- 无法删除元素
- 需要定期重建

适用场景:
- 大量数据
- 可以容忍小概率误判
- 读多写少

方案3:参数校验 ⭐⭐⭐⭐

/**
 * 严格的参数校验
 */
@RestController
@RequestMapping("/user")
public class UserController {
    
    /**
     * 参数校验
     */
    @GetMapping("/{userId}")
    public User getUser(@PathVariable Long userId) {
        // 1. 参数合法性校验
        if (userId == null || userId <= 0) {
            throw new IllegalArgumentException("用户ID非法");
        }
        
        // 2. 范围校验
        if (userId > 100000000) {  // 最大用户ID
            throw new IllegalArgumentException("用户ID超出范围");
        }
        
        // 3. 格式校验
        if (!isValidUserId(userId)) {
            throw new IllegalArgumentException("用户ID格式错误");
        }
        
        return userService.getUser(userId);
    }
    
    /**
     * 校验用户ID格式
     */
    private boolean isValidUserId(Long userId) {
        // 例如:用户ID必须是10位
        return userId >= 1000000000L && userId < 10000000000L;
    }
}

💥 问题2:缓存击穿

什么是缓存击穿?

场景:热点商品详情

正常情况:
10万QPS → 缓存 → 返回 ✅

某一时刻:
缓存key过期!❌
         ↓
10万QPS → 数据库
         ↓
数据库崩溃!💥

解决方案

方案1:互斥锁 ⭐⭐⭐⭐

@Service
public class ProductService {
    
    @Autowired
    private RedisTemplate<String, Product> redisTemplate;
    
    @Autowired
    private ProductMapper productMapper;
    
    /**
     * 使用互斥锁防止缓存击穿
     */
    public Product getProduct(Long productId) {
        String cacheKey = "product:" + productId;
        
        // 1. 查询缓存
        Product product = redisTemplate.opsForValue().get(cacheKey);
        if (product != null) {
            return product;
        }
        
        // 2. 缓存未命中,尝试获取锁
        String lockKey = "lock:product:" + productId;
        
        try {
            // 尝试获取分布式锁
            Boolean locked = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
            
            if (Boolean.TRUE.equals(locked)) {
                // 获取锁成功,查询数据库
                log.info("获取锁成功,查询数据库: {}", productId);
                
                // 双重检查:再次查询缓存
                product = redisTemplate.opsForValue().get(cacheKey);
                if (product != null) {
                    return product;
                }
                
                // 查询数据库
                product = productMapper.selectById(productId);
                
                if (product != null) {
                    // 写入缓存
                    redisTemplate.opsForValue().set(
                        cacheKey, 
                        product, 
                        30, 
                        TimeUnit.MINUTES
                    );
                }
                
                return product;
                
            } else {
                // 获取锁失败,等待后重试
                log.info("获取锁失败,等待重试: {}", productId);
                Thread.sleep(50);
                return getProduct(productId);  // 递归重试
            }
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("查询商品失败", e);
            
        } finally {
            // 释放锁
            redisTemplate.delete(lockKey);
        }
    }
}

更优雅的实现(Redisson)

@Service
public class ProductServiceWithRedisson {
    
    @Autowired
    private RedissonClient redissonClient;
    
    /**
     * 使用Redisson分布式锁
     */
    public Product getProduct(Long productId) {
        String cacheKey = "product:" + productId;
        
        // 1. 查询缓存
        Product product = getFromCache(cacheKey);
        if (product != null) {
            return product;
        }
        
        // 2. 获取锁
        String lockKey = "lock:product:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试加锁,最多等待10秒,锁超时时间30秒
            boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
            
            if (locked) {
                // 双重检查
                product = getFromCache(cacheKey);
                if (product != null) {
                    return product;
                }
                
                // 查询数据库
                product = productMapper.selectById(productId);
                
                if (product != null) {
                    setToCache(cacheKey, product, 30, TimeUnit.MINUTES);
                }
                
                return product;
            } else {
                // 获取锁超时
                throw new RuntimeException("系统繁忙,请稍后重试");
            }
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("查询失败", e);
            
        } finally {
            // 释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

流程图

并发10000个请求查询同一个商品:

线程1 → 获取锁成功 ✅
        ↓
        查询数据库
        ↓
        写入缓存
        ↓
        释放锁

线程2-10000 → 获取锁失败 ❌
              ↓
              等待50ms
              ↓
              重试查询
              ↓
              命中缓存 ✅

结果:只有1次数据库查询!

优缺点

优点 ✅:
- 完全避免击穿
- 只有一个线程查询数据库
- 保护数据库

缺点 ❌:
- 加锁影响性能
- 等待时间长
- 分布式锁实现复杂

适用场景:
- 超热点数据
- 数据库压力大

方案2:逻辑过期 ⭐⭐⭐⭐⭐

/**
 * 带逻辑过期时间的缓存对象
 */
@Data
public class CacheData<T> {
    private T data;           // 实际数据
    private Long expireTime;  // 逻辑过期时间(时间戳)
    
    public boolean isExpired() {
        return System.currentTimeMillis() > expireTime;
    }
}

@Service
public class ProductServiceWithLogicalExpire {
    
    @Autowired
    private RedisTemplate<String, CacheData<Product>> redisTemplate;
    
    // 重建缓存的线程池
    private final ExecutorService rebuildExecutor = Executors.newFixedThreadPool(10);
    
    /**
     * 逻辑过期方案
     */
    public Product getProduct(Long productId) {
        String cacheKey = "product:" + productId;
        
        // 1. 查询缓存
        CacheData<Product> cacheData = redisTemplate.opsForValue().get(cacheKey);
        
        if (cacheData == null) {
            // 缓存未命中(这种情况很少,因为永不过期)
            return loadAndCache(productId);
        }
        
        // 2. 检查逻辑过期时间
        if (!cacheData.isExpired()) {
            // 未过期,直接返回
            return cacheData.getData();
        }
        
        // 3. 已过期,异步重建缓存
        String lockKey = "lock:rebuild:" + productId;
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
        
        if (Boolean.TRUE.equals(locked)) {
            // 获取锁成功,异步重建缓存
            rebuildExecutor.submit(() -> {
                try {
                    log.info("异步重建缓存: {}", productId);
                    rebuildCache(productId);
                } finally {
                    redisTemplate.delete(lockKey);
                }
            });
        }
        
        // 4. 返回旧数据(虽然过期,但还能用)
        log.info("返回过期数据: {}", productId);
        return cacheData.getData();
    }
    
    /**
     * 重建缓存
     */
    private void rebuildCache(Long productId) {
        String cacheKey = "product:" + productId;
        
        // 查询数据库
        Product product = productMapper.selectById(productId);
        
        if (product != null) {
            // 封装为CacheData
            CacheData<Product> cacheData = new CacheData<>();
            cacheData.setData(product);
            cacheData.setExpireTime(
                System.currentTimeMillis() + 30 * 60 * 1000  // 30分钟后过期
            );
            
            // 写入缓存(不设置Redis过期时间,永不过期)
            redisTemplate.opsForValue().set(cacheKey, cacheData);
            
            log.info("缓存重建完成: {}", productId);
        }
    }
    
    /**
     * 初次加载并缓存
     */
    private Product loadAndCache(Long productId) {
        Product product = productMapper.selectById(productId);
        
        if (product != null) {
            String cacheKey = "product:" + productId;
            CacheData<Product> cacheData = new CacheData<>();
            cacheData.setData(product);
            cacheData.setExpireTime(
                System.currentTimeMillis() + 30 * 60 * 1000
            );
            
            redisTemplate.opsForValue().set(cacheKey, cacheData);
        }
        
        return product;
    }
}

流程图

请求到来:

查询缓存 → 命中 → 检查逻辑过期时间
                    ↓
                  未过期
                    ↓
                  直接返回 ✅

查询缓存 → 命中 → 检查逻辑过期时间
                    ↓
                  已过期
                    ↓
                  尝试获取锁
                    ↓
          ┌─────────┴─────────┐
       获取成功             获取失败
          ↓                     ↓
       异步重建缓存          直接返回旧数据 ✅
          ↓
       返回旧数据 ✅

特点:
- 永远不会阻塞
- 最多返回略微过期的数据
- 异步更新,不影响性能

优缺点

优点 ✅:
- 性能最好(不阻塞)
- 永远有数据返回
- 缓存永不过期

缺点 ❌:
- 数据可能略微不一致
- 实现复杂
- 占用额外内存(存储过期时间)

适用场景:
- 对一致性要求不高
- 性能要求极高
- 热点数据

方案3:热点数据永不过期 ⭐⭐⭐

@Service
public class HotDataService {
    
    /**
     * 热点数据永不过期
     */
    @PostConstruct
    public void initHotData() {
        // 预热热点数据
        List<Long> hotProductIds = getHotProductIds();
        
        for (Long productId : hotProductIds) {
            Product product = productMapper.selectById(productId);
            
            if (product != null) {
                String cacheKey = "product:hot:" + productId;
                // 不设置过期时间
                redisTemplate.opsForValue().set(cacheKey, product);
            }
        }
        
        log.info("热点数据预热完成,共{}个", hotProductIds.size());
    }
    
    /**
     * 定时刷新热点数据
     */
    @Scheduled(fixedDelay = 600000)  // 每10分钟
    public void refreshHotData() {
        List<Long> hotProductIds = getHotProductIds();
        
        for (Long productId : hotProductIds) {
            Product product = productMapper.selectById(productId);
            
            if (product != null) {
                String cacheKey = "product:hot:" + productId;
                redisTemplate.opsForValue().set(cacheKey, product);
            }
        }
        
        log.info("热点数据刷新完成");
    }
}

❄️ 问题3:缓存雪崩

什么是缓存雪崩?

场景:大促活动准备

20:00  运营人员批量导入10万个商品
       所有商品设置1小时缓存
       过期时间:21:00

21:00  10万个key同时过期!❌
       ↓
       大量请求打到数据库
       ↓
       数据库CPU 100%
       ↓
       响应超时
       ↓
       系统雪崩!💥

解决方案

方案1:随机过期时间 ⭐⭐⭐⭐⭐

@Service
public class ProductServiceWithRandomExpire {
    
    /**
     * 设置随机过期时间
     */
    public void cacheProduct(Product product) {
        String cacheKey = "product:" + product.getId();
        
        // 基础过期时间:30分钟
        long baseExpire = 30 * 60;
        
        // 随机增加 0-10分钟
        long randomExpire = ThreadLocalRandom.current().nextLong(0, 10 * 60);
        
        // 最终过期时间:30-40分钟之间
        long expireSeconds = baseExpire + randomExpire;
        
        redisTemplate.opsForValue().set(
            cacheKey,
            product,
            expireSeconds,
            TimeUnit.SECONDS
        );
        
        log.info("商品缓存,过期时间: {}秒", expireSeconds);
    }
    
    /**
     * 更通用的方法
     */
    public <T> void cacheWithRandomExpire(String key, T value, 
                                          long baseMinutes, double randomPercent) {
        // baseMinutes: 基础分钟数
        // randomPercent: 随机百分比,例如0.2表示增加0-20%的时间
        
        long baseSeconds = baseMinutes * 60;
        long randomSeconds = (long) (baseSeconds * Math.random() * randomPercent);
        long expireSeconds = baseSeconds + randomSeconds;
        
        redisTemplate.opsForValue().set(
            key,
            value,
            expireSeconds,
            TimeUnit.SECONDS
        );
    }
}

// 使用示例
public void example() {
    // 过期时间:30分钟 + 随机(0-6分钟)
    cacheWithRandomExpire("key1", value1, 30, 0.2);
    
    // 过期时间:60分钟 + 随机(0-12分钟)
    cacheWithRandomExpire("key2", value2, 60, 0.2);
}

效果对比

固定过期时间:
21:00:00  10万个key同时过期 💥

随机过期时间:
21:00:00  1000个key过期
21:00:01  980个key过期
21:00:02  1020个key过期
...
21:10:00  最后一批key过期

结果:压力分散到10分钟内!✅

方案2:多级缓存 ⭐⭐⭐⭐

/**
 * 多级缓存架构
 */
@Service
public class MultiLevelCacheService {
    
    @Autowired
    private RedisTemplate<String, Product> redisTemplate;
    
    // 本地缓存(Caffeine)
    private final LoadingCache<Long, Product> localCache;
    
    public MultiLevelCacheService() {
        this.localCache = Caffeine.newBuilder()
            .maximumSize(10000)  // 最多1万个
            .expireAfterWrite(5, TimeUnit.MINUTES)  // 5分钟过期
            .build(this::loadFromRedis);
    }
    
    /**
     * 三级缓存查询
     */
    public Product getProduct(Long productId) {
        // Level 1: 本地缓存
        try {
            Product product = localCache.get(productId);
            if (product != null) {
                log.info("L1缓存命中: {}", productId);
                return product;
            }
        } catch (Exception e) {
            log.warn("L1缓存查询失败", e);
        }
        
        // Level 2: Redis缓存
        Product product = loadFromRedis(productId);
        if (product != null) {
            log.info("L2缓存命中: {}", productId);
            return product;
        }
        
        // Level 3: 数据库
        product = loadFromDB(productId);
        if (product != null) {
            log.info("L3数据库命中: {}", productId);
            // 回写缓存
            saveToRedis(productId, product);
        }
        
        return product;
    }
    
    private Product loadFromRedis(Long productId) {
        String cacheKey = "product:" + productId;
        return redisTemplate.opsForValue().get(cacheKey);
    }
    
    private Product loadFromDB(Long productId) {
        return productMapper.selectById(productId);
    }
    
    private void saveToRedis(Long productId, Product product) {
        String cacheKey = "product:" + productId;
        long randomExpire = 30 + ThreadLocalRandom.current().nextInt(10);
        redisTemplate.opsForValue().set(
            cacheKey,
            product,
            randomExpire,
            TimeUnit.MINUTES
        );
    }
}

缓存架构图

┌─────────────┐
│   客户端    │
└──────┬──────┘
       │
   ① 请求
       │
┌──────▼─────────────┐
│  L1: 本地缓存      │  容量:1万
│  (Caffeine)        │  过期:5分钟
│  命中率:60%       │
└──────┬─────────────┘
       │ 未命中
   ② 查询L2
       │
┌──────▼─────────────┐
│  L2: Redis缓存     │  容量:100万
│  命中率:35%       │  过期:30分钟
└──────┬─────────────┘
       │ 未命中
   ③ 查询L3
       │
┌──────▼─────────────┐
│  L3: MySQL数据库   │  命中率:5%
└────────────────────┘

总命中率:95%
数据库查询:只有5%

方案3:缓存预热 ⭐⭐⭐⭐

@Component
public class CacheWarmUp {
    
    @Autowired
    private ProductMapper productMapper;
    
    @Autowired
    private RedisTemplate<String, Product> redisTemplate;
    
    /**
     * 启动时预热缓存
     */
    @PostConstruct
    public void warmUp() {
        log.info("开始缓存预热...");
        
        // 加载热点数据
        List<Product> hotProducts = productMapper.selectHotProducts(1000);
        
        int count = 0;
        for (Product product : hotProducts) {
            String cacheKey = "product:" + product.getId();
            
            // 设置随机过期时间
            long expireMinutes = 30 + ThreadLocalRandom.current().nextInt(30);
            
            redisTemplate.opsForValue().set(
                cacheKey,
                product,
                expireMinutes,
                TimeUnit.MINUTES
            );
            
            count++;
        }
        
        log.info("缓存预热完成,共预热{}个商品", count);
    }
    
    /**
     * 大促前预热
     */
    public void warmUpBeforePromotion() {
        log.info("大促预热开始...");
        
        // 分批加载,避免数据库压力
        int batchSize = 1000;
        int offset = 0;
        
        while (true) {
            List<Product> products = productMapper.selectByPage(offset, batchSize);
            
            if (products.isEmpty()) {
                break;
            }
            
            // 批量写入Redis
            for (Product product : products) {
                cacheProduct(product);
            }
            
            offset += batchSize;
            
            // 休息一下,避免数据库压力
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        
        log.info("大促预热完成");
    }
}

方案4:降级熔断 ⭐⭐⭐⭐⭐

/**
 * 降级熔断保护
 */
@Service
public class ProductServiceWithCircuitBreaker {
    
    @Autowired
    private ProductMapper productMapper;
    
    /**
     * 使用Hystrix/Resilience4j熔断
     */
    @HystrixCommand(
        fallbackMethod = "getProductFallback",
        commandProperties = {
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000")
        }
    )
    public Product getProduct(Long productId) {
        // 查询数据库
        return productMapper.selectById(productId);
    }
    
    /**
     * 降级方法
     */
    public Product getProductFallback(Long productId, Throwable throwable) {
        log.warn("触发降级,返回默认数据: {}", productId, throwable);
        
        // 返回降级数据
        Product product = new Product();
        product.setId(productId);
        product.setName("商品暂时无法查看");
        product.setPrice(BigDecimal.ZERO);
        
        return product;
    }
}

🎯 综合防御方案

实际项目架构

/**
 * 生产级缓存服务
 */
@Service
public class ProductionCacheService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    @Autowired
    private ProductMapper productMapper;
    
    // 本地缓存
    private final Cache<Long, Product> localCache = Caffeine.newBuilder()
        .maximumSize(10000)
        .expireAfterWrite(5, TimeUnit.MINUTES)
        .build();
    
    // 布隆过滤器
    private RBloomFilter<Long> bloomFilter;
    
    @PostConstruct
    public void init() {
        // 初始化布隆过滤器
        bloomFilter = redissonClient.getBloomFilter("product:bloom");
        bloomFilter.tryInit(10000000L, 0.01);
        
        // 加载所有商品ID
        List<Long> allIds = productMapper.selectAllIds();
        allIds.forEach(bloomFilter::add);
    }
    
    /**
     * 综合方案查询商品
     */
    public Product getProduct(Long productId) {
        // 1. 参数校验
        if (productId == null || productId <= 0) {
            throw new IllegalArgumentException("商品ID非法");
        }
        
        // 2. 布隆过滤器(防穿透)
        if (!bloomFilter.contains(productId)) {
            log.info("布隆过滤器拦截: {}", productId);
            return null;
        }
        
        // 3. 本地缓存
        Product product = localCache.getIfPresent(productId);
        if (product != null) {
            return product;
        }
        
        // 4. Redis缓存(带逻辑过期,防击穿)
        product = getFromRedisWithLogicalExpire(productId);
        if (product != null) {
            localCache.put(productId, product);
            return product;
        }
        
        // 5. 数据库(加互斥锁)
        product = loadFromDBWithLock(productId);
        
        return product;
    }
    
    /**
     * 从Redis获取(逻辑过期)
     */
    private Product getFromRedisWithLogicalExpire(Long productId) {
        // 实现逻辑过期方案...
        return null;
    }
    
    /**
     * 从数据库加载(加锁)
     */
    private Product loadFromDBWithLock(Long productId) {
        String lockKey = "lock:product:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
                Product product = productMapper.selectById(productId);
                
                if (product != null) {
                    // 缓存(随机过期时间,防雪崩)
                    cacheWithRandomExpire(productId, product);
                } else {
                    // 缓存空对象(防穿透)
                    cacheNullValue(productId);
                }
                
                return product;
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
        
        return null;
    }
    
    /**
     * 缓存(随机过期时间)
     */
    private void cacheWithRandomExpire(Long productId, Product product) {
        long baseSeconds = 30 * 60;  // 30分钟
        long randomSeconds = ThreadLocalRandom.current().nextLong(0, 10 * 60);  // 0-10分钟
        
        RBucket<Product> bucket = redissonClient.getBucket("product:" + productId);
        bucket.set(product, baseSeconds + randomSeconds, TimeUnit.SECONDS);
    }
    
    /**
     * 缓存空对象
     */
    private void cacheNullValue(Long productId) {
        RBucket<String> bucket = redissonClient.getBucket("product:null:" + productId);
        bucket.set("NULL", 5, TimeUnit.MINUTES);  // 5分钟过期
    }
}

🎉 总结

对比表

问题解决方案推荐度适用场景
缓存穿透 🕳️布隆过滤器⭐⭐⭐⭐⭐海量数据
缓存空对象⭐⭐⭐小规模数据
参数校验⭐⭐⭐⭐所有场景
缓存击穿 💥互斥锁⭐⭐⭐⭐强一致性
逻辑过期⭐⭐⭐⭐⭐高性能
永不过期⭐⭐⭐超热点数据
缓存雪崩 ❄️随机过期⭐⭐⭐⭐⭐所有场景
多级缓存⭐⭐⭐⭐高并发
降级熔断⭐⭐⭐⭐⭐保护系统

记忆口诀 📝

缓存三灾要记牢,
穿透击穿和雪崩。

穿透攻击不存在,
布隆过滤来防范。
空对象也可缓存,
参数校验第一关。

击穿热点突然过期,
互斥锁来保护数据库。
逻辑过期更优雅,
返回旧数异步更新。

雪崩批量同时过期,
随机时间来分散。
多级缓存增可用,
降级熔断保系统!

愿你的缓存固若金汤,系统永不崩溃! 🛡️✨