Redis三大问题:穿透、击穿、雪崩(实战解析)

一、缓存穿透(Cache Penetration)- 查不存在的数据

1. 什么是缓存穿透?

复制代码
// 场景:黑客恶意攻击
// 请求数据库中根本不存在的key

// 正常流程:
public User getUser(Long userId) {
    // 1. 先查缓存
    User user = redis.get("user:" + userId);
    if (user != null) {
        return user;  // 缓存命中
    }
    
    // 2. 缓存没有,查数据库
    user = userDao.findById(userId);
    if (user != null) {
        // 3. 写入缓存
        redis.set("user:" + userId, user);
    }
    
    return user;
}

// 攻击:黑客一直请求不存在的userId
// 结果:每次请求都穿透到数据库,数据库压力巨大!

2. 什么时候会出现?

复制代码
出现时机:
1. 恶意攻击:黑客故意请求不存在的数据
2. 业务逻辑错误:程序bug导致生成不存在的ID
3. 爬虫抓取:爬虫遍历ID,遇到不存在的

常见场景:
- 商品详情页:请求不存在的商品ID
- 用户详情:请求不存在的用户ID
- 订单查询:查询不存在的订单号

3. 解决方案

方案1:缓存空对象
复制代码
// 将不存在的key也缓存起来,但设置较短的过期时间
public User getUserSafe(Long userId) {
    String key = "user:" + userId;
    
    // 1. 从缓存读取
    User user = redis.get(key);
    
    // 如果是空对象标记
    if (user != null && user.getId() == null) {
        return null;  // 明确知道不存在
    }
    
    if (user != null) {
        return user;  // 正常用户
    }
    
    // 2. 查数据库
    user = userDao.findById(userId);
    
    if (user == null) {
        // 缓存空对象,防止穿透
        User emptyUser = new User();  // 空对象
        emptyUser.setId(null);  // 标记为空
        redis.setex(key, 60, emptyUser);  // 只缓存60秒
        
        return null;
    }
    
    // 3. 缓存正常数据
    redis.setex(key, 300, user);  // 缓存5分钟
    
    return user;
}
方案2:布隆过滤器(Bloom Filter)- 推荐
复制代码
// 布隆过滤器:判断元素"可能存在"或"肯定不存在"
@Component
public class BloomFilterService {
    
    // 使用Redisson的布隆过滤器
    @Autowired
    private RedissonClient redisson;
    
    private RBloomFilter<Long> userBloomFilter;
    
    @PostConstruct
    public void init() {
        // 初始化布隆过滤器
        userBloomFilter = redisson.getBloomFilter("user:bloomfilter");
        // 预期元素数量100万,误判率0.1%
        userBloomFilter.tryInit(1000000L, 0.001);
        
        // 预热:加载所有存在的用户ID
        List<Long> allUserIds = userDao.findAllIds();
        for (Long id : allUserIds) {
            userBloomFilter.add(id);
        }
    }
    
    public User getUserWithBloom(Long userId) {
        String key = "user:" + userId;
        
        // 1. 先用布隆过滤器判断
        if (!userBloomFilter.contains(userId)) {
            // 肯定不存在,直接返回
            System.out.println("布隆过滤器说:用户" + userId + "不存在");
            return null;
        }
        
        // 2. 走正常缓存流程
        User user = redis.get(key);
        if (user != null) {
            return user;
        }
        
        // 3. 查数据库
        user = userDao.findById(userId);
        if (user != null) {
            redis.setex(key, 300, user);
        } else {
            // 数据库也没有,可能是误判
            // 记录日志,可能需要调整布隆过滤器
            log.warn("布隆过滤器误判,用户ID: {}", userId);
        }
        
        return user;
    }
    
    // 新增用户时添加到布隆过滤器
    public void addUser(User user) {
        userDao.save(user);
        userBloomFilter.add(user.getId());
    }
}
方案3:接口层校验
复制代码
// 在请求进入业务逻辑前就过滤
@RestController
@RequestMapping("/user")
public class UserController {
    
    @GetMapping("/{id}")
    public Result getUser(@PathVariable Long id) {
        // 1. 参数校验
        if (id == null || id <= 0) {
            return Result.error("参数错误");
        }
        
        // 2. ID范围校验(如果业务有范围)
        if (id > 1000000L) {  // 假设用户ID最大100万
            return Result.error("用户不存在");
        }
        
        // 3. 模式校验(如果是特殊格式)
        if (!isValidUserId(id)) {
            return Result.error("用户ID格式错误");
        }
        
        return Result.success(userService.getUser(id));
    }
    
    private boolean isValidUserId(Long id) {
        // 可以根据业务规则校验
        // 比如:必须是数字、长度限制、不能是特殊字符等
        return id.toString().matches("\\d{1,8}");
    }
}

二、缓存击穿(Cache Breakdown)- 热点key失效

1. 什么是缓存击穿?

复制代码
// 场景:一个热点key在缓存过期瞬间
// 大量并发请求同时来查这个key

// 问题代码:
public Product getProduct(Long productId) {
    String key = "product:" + productId;
    
    // 缓存刚好过期
    Product product = redis.get(key);
    if (product == null) {
        // 大量请求同时到达这里!
        // 都会去查数据库
        product = productDao.findById(productId);
        redis.setex(key, 300, product);  // 重新缓存
    }
    
    return product;
}

// 结果:瞬间大量请求打到数据库,可能压垮数据库

2. 什么时候会出现?

复制代码
出现时机:
1. 热点数据过期:秒杀商品、热门文章
2. 定时缓存刷新:缓存统一过期
3. 缓存主动删除:管理员操作

常见场景:
- 秒杀商品详情
- 首页推荐商品
- 热搜排行榜
- 热门文章

3. 解决方案

方案1:互斥锁(Mutex Lock)- 推荐
复制代码
@Service
public class ProductService {
    
    @Autowired
    private RedissonClient redisson;
    
    public Product getProductWithLock(Long productId) {
        String key = "product:" + productId;
        
        // 1. 先查缓存
        Product product = redis.get(key);
        if (product != null) {
            return product;
        }
        
        // 2. 获取分布式锁
        String lockKey = "product:lock:" + productId;
        RLock lock = redisson.getLock(lockKey);
        
        try {
            // 尝试获取锁,最多等待100ms
            if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    // 双重检查(Double Check)
                    product = redis.get(key);
                    if (product != null) {
                        return product;
                    }
                    
                    // 3. 查数据库
                    product = productDao.findById(productId);
                    if (product == null) {
                        return null;
                    }
                    
                    // 4. 写入缓存
                    redis.setex(key, 300, product);
                    
                    return product;
                    
                } finally {
                    lock.unlock();  // 释放锁
                }
            } else {
                // 没获取到锁,说明其他线程在重建缓存
                // 等待一会再重试
                Thread.sleep(50);
                return redis.get(key);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }
}
方案2:逻辑过期(不设置过期时间)
复制代码
// 缓存不设置过期时间,由程序控制更新
public class ProductServiceV2 {
    
    public Product getProductLogicalExpire(Long productId) {
        String key = "product:" + productId;
        
        // 1. 查缓存
        ProductCacheWrapper wrapper = redis.get(key);
        
        if (wrapper != null) {
            // 检查是否逻辑过期
            if (wrapper.getExpireTime() > System.currentTimeMillis()) {
                // 未过期,直接返回
                return wrapper.getProduct();
            } else {
                // 已过期,异步更新缓存
                asyncUpdateCache(productId);
                return wrapper.getProduct();  // 先返回旧数据
            }
        }
        
        // 2. 缓存没有,查数据库
        Product product = productDao.findById(productId);
        if (product == null) {
            return null;
        }
        
        // 3. 写入缓存(设置逻辑过期时间)
        ProductCacheWrapper newWrapper = new ProductCacheWrapper(
            product, 
            System.currentTimeMillis() + 300000  // 5分钟后逻辑过期
        );
        redis.set(key, newWrapper);  // 不设置Redis过期时间
        
        return product;
    }
    
    // 异步更新缓存
    private void asyncUpdateCache(Long productId) {
        CompletableFuture.runAsync(() -> {
            try {
                Product product = productDao.findById(productId);
                if (product != null) {
                    ProductCacheWrapper wrapper = new ProductCacheWrapper(
                        product,
                        System.currentTimeMillis() + 300000
                    );
                    redis.set("product:" + productId, wrapper);
                }
            } catch (Exception e) {
                log.error("异步更新缓存失败", e);
            }
        });
    }
    
    // 包装类
    @Data
    @AllArgsConstructor
    static class ProductCacheWrapper {
        private Product product;
        private Long expireTime;  // 逻辑过期时间戳
    }
}
方案3:永不过期 + 后台刷新
复制代码
// 热点数据永不过期,后台定时刷新
@Service
@Slf4j
public class HotDataService {
    
    // 热点数据列表
    private Set<Long> hotProductIds = new ConcurrentHashSet<>();
    
    // 初始化热点数据
    @PostConstruct
    public void initHotData() {
        // 从数据库或配置文件加载热点数据
        List<Product> hotProducts = productDao.findHotProducts();
        for (Product product : hotProducts) {
            hotProductIds.add(product.getId());
        }
        
        // 启动后台刷新线程
        startRefreshThread();
    }
    
    // 获取商品
    public Product getHotProduct(Long productId) {
        String key = "product:" + productId;
        
        // 1. 查缓存
        Product product = redis.get(key);
        if (product != null) {
            return product;
        }
        
        // 2. 如果是热点商品,用锁重建
        if (hotProductIds.contains(productId)) {
            return getProductWithLock(productId);
        }
        
        // 3. 普通商品,正常流程
        return getNormalProduct(productId);
    }
    
    // 后台刷新线程
    private void startRefreshThread() {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        
        // 每30秒刷新一次热点数据
        scheduler.scheduleAtFixedRate(() -> {
            for (Long productId : hotProductIds) {
                try {
                    Product product = productDao.findById(productId);
                    if (product != null) {
                        redis.set("product:" + productId, product);
                        log.info("刷新热点商品缓存:{}", productId);
                    }
                } catch (Exception e) {
                    log.error("刷新热点商品失败:{}", productId, e);
                }
            }
        }, 0, 30, TimeUnit.SECONDS);
    }
}

三、缓存雪崩(Cache Avalanche)- 大量key同时失效

1. 什么是缓存雪崩?

复制代码
// 场景:大量缓存key在同一时间过期
// 导致所有请求都打到数据库

// 问题代码:
public class CacheService {
    public void initCache() {
        // 初始化缓存,设置相同过期时间
        for (int i = 1; i <= 10000; i++) {
            String key = "product:" + i;
            Product product = productDao.findById((long)i);
            redis.setex(key, 3600, product);  // 都1小时后过期
        }
    }
}

// 1小时后,所有缓存同时过期
// 瞬间大量请求打到数据库,数据库可能崩溃

2. 什么时候会出现?

复制代码
出现时机:
1. 缓存预热:批量设置相同过期时间
2. 缓存刷新:定时任务同时刷新
3. Redis宕机:缓存全部失效
4. 网络分区:集群脑裂

常见场景:
- 系统启动时缓存预热
- 定时刷新全量缓存
- Redis集群故障
- 大促期间缓存集中过期

3. 解决方案

方案1:随机过期时间
复制代码
@Service
public class CacheService {
    
    private Random random = new Random();
    
    // 设置缓存,使用随机过期时间
    public void setCacheWithRandomExpire(String key, Object value) {
        // 基础过期时间:1小时
        int baseExpireSeconds = 3600;
        
        // 随机增加0-300秒(5分钟)
        int randomAddSeconds = random.nextInt(300);
        
        int totalExpireSeconds = baseExpireSeconds + randomAddSeconds;
        
        redis.setex(key, totalExpireSeconds, value);
        
        log.info("设置缓存 {} 过期时间:{}秒", 
                 key, totalExpireSeconds);
    }
    
    // 批量设置缓存
    public void batchSetCache(Map<String, Object> data) {
        for (Map.Entry<String, Object> entry : data.entrySet()) {
            setCacheWithRandomExpire(entry.getKey(), entry.getValue());
        }
    }
    
    // 更高级的随机策略
    public void setCacheSmart(String key, Object value) {
        int baseExpire;
        
        // 根据key的重要性设置不同过期时间
        if (key.startsWith("hot:")) {
            // 热点数据:过期时间短,但会续期
            baseExpire = 600;  // 10分钟
        } else if (key.startsWith("important:")) {
            // 重要数据:中等过期时间
            baseExpire = 3600;  // 1小时
        } else {
            // 普通数据:长时间
            baseExpire = 7200;  // 2小时
        }
        
        // 加上随机时间
        int randomAdd = random.nextInt(600);  // 0-10分钟随机
        int totalExpire = baseExpire + randomAdd;
        
        redis.setex(key, totalExpire, value);
    }
}
方案2:二级缓存
复制代码
// 使用本地缓存 + Redis
@Component
public class TwoLevelCacheService {
    
    // 本地缓存(Guava Cache)
    private Cache<String, Object> localCache = CacheBuilder.newBuilder()
        .maximumSize(10000)  // 最多缓存10000个
        .expireAfterWrite(5, TimeUnit.MINUTES)  // 5分钟过期
        .build();
    
    public Object getWithTwoLevelCache(String key) {
        // 1. 先查本地缓存
        Object value = localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        
        // 2. 再查Redis
        value = redis.get(key);
        if (value != null) {
            // 写入本地缓存
            localCache.put(key, value);
            return value;
        }
        
        // 3. 查数据库
        value = loadFromDB(key);
        if (value != null) {
            // 写入Redis(随机过期时间)
            int expire = 300 + new Random().nextInt(60);  // 5-6分钟
            redis.setex(key, expire, value);
            
            // 写入本地缓存
            localCache.put(key, value);
        }
        
        return value;
    }
    
    // 更新缓存
    public void updateCache(String key, Object value) {
        // 1. 更新数据库
        updateDB(key, value);
        
        // 2. 删除Redis缓存
        redis.delete(key);
        
        // 3. 删除本地缓存
        localCache.invalidate(key);
        
        // 4. 异步重新加载
        CompletableFuture.runAsync(() -> {
            Object newValue = loadFromDB(key);
            if (newValue != null) {
                redis.setex(key, 300, newValue);
                localCache.put(key, newValue);
            }
        });
    }
}
方案3:缓存永不过期 + 异步更新
复制代码
// 缓存不设置过期时间,由程序控制更新
@Service
@Slf4j
public class NeverExpireCacheService {
    
    // 记录key的最后更新时间
    private Map<String, Long> keyUpdateTime = new ConcurrentHashMap<>();
    
    // 获取数据
    public Object getData(String key) {
        // 1. 从Redis获取
        Object value = redis.get(key);
        
        if (value != null) {
            // 检查是否需要异步更新
            checkAndUpdateAsync(key);
            return value;
        }
        
        // 2. 缓存没有,加载数据
        return loadAndCache(key);
    }
    
    // 检查并异步更新
    private void checkAndUpdateAsync(String key) {
        Long lastUpdate = keyUpdateTime.get(key);
        long now = System.currentTimeMillis();
        
        // 如果超过30分钟没更新,异步更新
        if (lastUpdate == null || (now - lastUpdate) > 30 * 60 * 1000) {
            CompletableFuture.runAsync(() -> {
                try {
                    Object newValue = loadFromDB(key);
                    if (newValue != null) {
                        redis.set(key, newValue);  // 永不过期
                        keyUpdateTime.put(key, now);
                        log.info("异步更新缓存:{}", key);
                    }
                } catch (Exception e) {
                    log.error("异步更新缓存失败:{}", key, e);
                }
            });
        }
    }
    
    // 热点数据续期
    @Scheduled(fixedRate = 60000)  // 每分钟执行
    public void renewHotKeys() {
        // 获取热点key列表
        Set<String> hotKeys = getHotKeys();
        
        for (String key : hotKeys) {
            // 如果key存在,就续期
            if (redis.exists(key)) {
                // 可以重新设置,或者什么都不做(永不过期)
                log.debug("热点key续期:{}", key);
            }
        }
    }
}
方案4:服务降级和熔断
复制代码
// 当缓存雪崩发生时,启用降级策略
@Component
public class FallbackService {
    
    @Autowired
    private CircuitBreakerFactory circuitBreakerFactory;
    
    public Product getProductWithFallback(Long productId) {
        CircuitBreaker circuitBreaker = circuitBreakerFactory.create("product-service");
        
        return circuitBreaker.run(
            () -> {
                // 正常业务逻辑
                return getProductFromCache(productId);
            },
            throwable -> {
                // 降级逻辑
                return getProductFromFallback(productId);
            }
        );
    }
    
    // 正常逻辑
    private Product getProductFromCache(Long productId) {
        String key = "product:" + productId;
        
        // 1. 查缓存
        Product product = redis.get(key);
        if (product != null) {
            return product;
        }
        
        // 2. 查数据库(这里可能压力大)
        product = productDao.findById(productId);
        if (product != null) {
            redis.setex(key, 300, product);
        }
        
        return product;
    }
    
    // 降级逻辑
    private Product getProductFromFallback(Long productId) {
        // 1. 返回静态数据
        Product fallbackProduct = new Product();
        fallbackProduct.setId(productId);
        fallbackProduct.setName("商品加载中...");
        fallbackProduct.setPrice(BigDecimal.ZERO);
        
        // 2. 记录日志
        log.warn("服务降级,返回兜底数据,productId: {}", productId);
        
        // 3. 异步尝试恢复
        recoverAsync(productId);
        
        return fallbackProduct;
    }
    
    // 异步恢复
    private void recoverAsync(Long productId) {
        CompletableFuture.runAsync(() -> {
            try {
                Product product = productDao.findById(productId);
                if (product != null) {
                    redis.setex("product:" + productId, 300, product);
                    log.info("异步恢复缓存成功:{}", productId);
                }
            } catch (Exception e) {
                log.error("异步恢复缓存失败:{}", productId, e);
            }
        });
    }
}

四、实战:综合解决方案

1. 完整的缓存服务

复制代码
@Component
@Slf4j
public class ComprehensiveCacheService {
    
    @Autowired
    private RedissonClient redisson;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 布隆过滤器
    private RBloomFilter<Long> bloomFilter;
    
    // 热点key集合
    private Set<String> hotKeys = new ConcurrentHashSet<>();
    
    @PostConstruct
    public void init() {
        // 初始化布隆过滤器
        bloomFilter = redisson.getBloomFilter("product:bloom");
        bloomFilter.tryInit(1000000L, 0.001);
        
        // 加载热点key
        loadHotKeys();
        
        // 启动监控线程
        startMonitor();
    }
    
    /**
     * 综合解决方案:获取商品
     */
    public Product getProductComprehensive(Long productId) {
        String key = "product:" + productId;
        
        // 1. 参数校验
        if (productId == null || productId <= 0) {
            return null;
        }
        
        // 2. 布隆过滤器校验
        if (!bloomFilter.contains(productId)) {
            log.info("布隆过滤器拦截,productId: {}", productId);
            return null;
        }
        
        // 3. 一级缓存:本地缓存(如果有)
        Product product = getFromLocalCache(key);
        if (product != null) {
            return product;
        }
        
        // 4. 二级缓存:Redis
        product = getFromRedis(key);
        if (product != null) {
            // 如果是热点key,续期
            if (hotKeys.contains(key)) {
                renewHotKey(key);
            }
            return product;
        }
        
        // 5. 缓存没有,用互斥锁保护数据库
        return getFromDBWithLock(key, productId);
    }
    
    /**
     * 从Redis获取
     */
    private Product getFromRedis(String key) {
        try {
            return (Product) redisTemplate.opsForValue().get(key);
        } catch (Exception e) {
            log.error("Redis获取失败,key: {}", key, e);
            // Redis异常时,直接查数据库
            return null;
        }
    }
    
    /**
     * 用锁保护数据库查询
     */
    private Product getFromDBWithLock(String key, Long productId) {
        String lockKey = "lock:" + key;
        RLock lock = redisson.getLock(lockKey);
        
        try {
            // 尝试获取锁
            if (lock.tryLock(100, 10000, TimeUnit.MILLISECONDS)) {
                try {
                    // 双重检查
                    Product product = getFromRedis(key);
                    if (product != null) {
                        return product;
                    }
                    
                    // 查询数据库
                    product = productDao.findById(productId);
                    if (product == null) {
                        // 数据库也没有,记录到布隆过滤器
                        // 注意:布隆过滤器不能删除,这里不记录
                        return null;
                    }
                    
                    // 写入Redis(随机过期时间)
                    int expire = generateExpireTime(key);
                    redisTemplate.opsForValue().set(key, product, expire, TimeUnit.SECONDS);
                    
                    // 如果是热点key,加入集合
                    if (isHotProduct(product)) {
                        hotKeys.add(key);
                    }
                    
                    return product;
                    
                } finally {
                    lock.unlock();
                }
            } else {
                // 没获取到锁,等待后重试
                Thread.sleep(50);
                return getFromRedis(key);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }
    
    /**
     * 生成随机过期时间
     */
    private int generateExpireTime(String key) {
        int baseExpire;
        
        if (key.startsWith("product:hot:")) {
            baseExpire = 300;  // 热点商品5分钟
        } else if (key.startsWith("product:important:")) {
            baseExpire = 1800;  // 重要商品30分钟
        } else {
            baseExpire = 3600;  // 普通商品1小时
        }
        
        // 加上随机时间(0-300秒)
        Random random = new Random();
        int randomAdd = random.nextInt(300);
        
        return baseExpire + randomAdd;
    }
    
    /**
     * 热点key续期
     */
    private void renewHotKey(String key) {
        // 如果剩余时间小于60秒,就续期
        Long expire = redisTemplate.getExpire(key);
        if (expire != null && expire < 60) {
            redisTemplate.expire(key, 300, TimeUnit.SECONDS);
            log.debug("热点key续期:{}", key);
        }
    }
    
    /**
     * 启动监控
     */
    private void startMonitor() {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        
        // 每分钟监控一次
        scheduler.scheduleAtFixedRate(() -> {
            try {
                monitorCacheHealth();
            } catch (Exception e) {
                log.error("监控任务异常", e);
            }
        }, 1, 1, TimeUnit.MINUTES);
    }
    
    /**
     * 监控缓存健康度
     */
    private void monitorCacheHealth() {
        // 监控命中率
        // 监控热点key
        // 监控Redis连接
        // 发送到监控系统
        
        log.info("缓存监控:热点key数量={}", hotKeys.size());
    }
}

2. Spring Boot配置

复制代码
# application.yml
spring:
  redis:
    host: localhost
    port: 6379
    lettuce:
      pool:
        max-active: 20
        max-wait: -1ms
        max-idle: 10
        min-idle: 5
    timeout: 2000ms

# 缓存配置
cache:
  # 默认过期时间(秒)
  default-ttl: 3600
  # 热点数据过期时间
  hot-ttl: 300
  # 是否启用布隆过滤器
  bloom-filter:
    enabled: true
    expected-insertions: 1000000
    false-probability: 0.001
  # 是否启用二级缓存
  two-level:
    enabled: true
    local-size: 10000
    local-ttl: 300

五、不同场景的解决方案选择

决策树

复制代码
问题类型 → 解决方案
─────────────────────────────────────────────
缓存穿透 → 布隆过滤器 + 缓存空值
缓存击穿 → 互斥锁 + 逻辑过期
缓存雪崩 → 随机过期 + 二级缓存 + 永不过期

场景匹配表

业务场景 主要问题 推荐方案 注意事项
用户查询 穿透(查不存在用户) 布隆过滤器 注意用户ID范围
商品详情 击穿(热点商品) 互斥锁 设置合理的锁超时
商品列表 雪崩(批量过期) 随机过期 避免同时加载大量数据
秒杀库存 击穿+雪崩 本地缓存+Redis 考虑使用Redis原子操作
配置信息 雪崩 永不过期+主动刷新 更新时注意双写一致性

六、监控和告警

1. 关键监控指标

复制代码
@Component
@Slf4j
public class CacheMonitor {
    
    // 命中率统计
    private AtomicLong hitCount = new AtomicLong(0);
    private AtomicLong missCount = new AtomicLong(0);
    
    // 穿透统计
    private AtomicLong penetrationCount = new AtomicLong(0);
    
    public <T> T getWithMonitor(String key, Supplier<T> loader) {
        T value = getFromCache(key);
        
        if (value != null) {
            hitCount.incrementAndGet();
            return value;
        } else {
            missCount.incrementAndGet();
            
            // 布隆过滤器拦截
            if (bloomFilter != null && !bloomFilter.contains(key)) {
                penetrationCount.incrementAndGet();
                return null;
            }
            
            value = loader.get();
            if (value == null) {
                penetrationCount.incrementAndGet();
            }
            return value;
        }
    }
    
    // 计算命中率
    public double getHitRate() {
        long total = hitCount.get() + missCount.get();
        if (total == 0) return 0.0;
        return (double) hitCount.get() / total;
    }
    
    // 每分钟上报指标
    @Scheduled(fixedRate = 60000)
    public void reportMetrics() {
        double hitRate = getHitRate();
        long penetration = penetrationCount.get();
        
        // 上报到监控系统
        Metrics.gauge("cache.hit.rate", hitRate);
        Metrics.counter("cache.penetration.count", penetration);
        
        // 告警
        if (hitRate < 0.8) {
            alert("缓存命中率过低: " + hitRate);
        }
        if (penetration > 1000) {
            alert("缓存穿透严重: " + penetration);
        }
    }
}

七、最佳实践总结

1. 防御策略组合

复制代码
// 完整的防御策略
public class CacheDefense {
    
    // 1. 防穿透:布隆过滤器 + 空值缓存
    private RBloomFilter<String> bloomFilter;
    
    // 2. 防击穿:分布式锁 + 双重检查
    private RLock getLock(String key) { ... }
    
    // 3. 防雪崩:随机过期 + 热点续期
    private int randomExpire() { ... }
    
    // 4. 降级:熔断 + 兜底数据
    public Object getWithFallback(String key) { ... }
    
    // 5. 监控:命中率 + 告警
    public void monitor() { ... }
}

2. 一句话记住解决方案

缓存穿透 → 布隆过滤器拦无效请求

缓存击穿 → 分布式锁保单点重建

缓存雪崩 → 随机过期防集体失效

3. 选型建议

复制代码
初创公司 → 缓存空值 + 互斥锁(简单有效)
中型公司 → 布隆过滤器 + 二级缓存(平衡方案)
大型公司 → 完整监控体系 + 智能调度(全面防御)

记住没有银弹,根据业务特点选择合适的组合方案,加上完善的监控,才是王道。

相关推荐
桦说编程2 小时前
并发编程高级技巧:运行时检测死锁,告别死锁焦虑
java·后端·性能优化
jiayong232 小时前
Spring AI Alibaba 深度解析(三):实战示例与最佳实践
java·人工智能·spring
梁同学与Android2 小时前
Android ---【经验篇】ArrayList vs CopyOnWriteArrayList 核心区别,怎么选择?
android·java·开发语言
ss2732 小时前
从零实现线程池:自定义线程池的工作线程设计与实现
java·开发语言·jvm
苗壮.2 小时前
CommandLineRunner 是什么?
java
石工记2 小时前
windows 10直接安装多个JDK
java·开发语言
菜鸟233号2 小时前
力扣669 修剪二叉搜索树 java实现
java·数据结构·算法·leetcode
健康平安的活着3 小时前
springboot+sse的实现案例
java·spring boot·后端
05大叔3 小时前
多线程的学习
java·开发语言·学习