高并发必看!缓存击穿 / 穿透 / 雪崩实战解析

分布式系统缓存三大问题解析与实战方案

在高并发分布式系统中,缓存是提升性能的关键组件,但不当使用可能引发严重问题。本文将深入解析缓存击穿、穿透、雪崩的原理,并结合电商、社交等真实场景给出解决方案和优化代码。

一、缓存击穿:热点数据失效引发的数据库风暴

场景描述
某电商平台 "双 11" 促销,爆款 iPhone 14 的库存查询接口 QPS 瞬间达到 10 万。当该商品缓存过期时,海量请求直接压垮数据库。

问题本质

  • 单个热点 Key 失效
  • 大量并发请求同时查询该 Key
  • 数据库瞬时压力剧增

解决方案

  1. 永不过期策略
// 热点数据管理类
public class HotDataManager {
    // 使用LoadingCache实现自动刷新
    private LoadingCache<String, ProductInfo> cache = Caffeine.newBuilder()
        .maximumSize(1000)
        .refreshAfterWrite(1, TimeUnit.HOURS) // 1小时后刷新
        .build(key -> loadProductFromDb(key));

    // 从数据库加载商品信息
    private ProductInfo loadProductFromDb(String productId) {
        // 模拟数据库查询
        return dbService.queryProduct(productId);
    }

    // 获取商品信息
    public ProductInfo getProduct(String productId) {
        try {
            // 自动刷新,保证热点数据永不过期
            return cache.get(productId);
        } catch (ExecutionException e) {
            log.error("获取商品信息失败", e);
            return null;
        }
    }
}
  1. 互斥锁机制
// 带互斥锁的缓存服务
public class CacheService {
    private final Cache<String, Object> cache = RedisCache.getInstance();
    private final ReentrantLock lock = new ReentrantLock();

    public Object getData(String key) {
        // 先查缓存
        Object value = cache.get(key);
        if (value != null) {
            return value;
        }

        // 未命中,尝试加锁查询数据库
        try {
            if (lock.tryLock(100, TimeUnit.MILLISECONDS)) { // 尝试获取锁
                try {
                    // 双重检查
                    value = cache.get(key);
                    if (value != null) {
                        return value;
                    }
                    
                    // 查询数据库
                    value = queryDatabase(key);
                    if (value != null) {
                        cache.put(key, value); // 更新缓存
                    }
                } finally {
                    lock.unlock();
                }
            } else {
                // 获取锁失败,等待片刻后重试
                Thread.sleep(50);
                return getData(key);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        }
        
        return value;
    }
}

最佳实践

  • 热点数据使用多级缓存(本地缓存 + 分布式缓存)
  • 结合自动刷新和手动刷新机制
  • 监控热点 Key 访问频率,动态调整过期策略

二、缓存穿透:恶意请求引发的无底洞

场景描述
某社交平台用户 ID 为自增序列,攻击者发送大量不存在的用户 ID 请求(如 - 1、9999999),导致数据库 CPU 飙升至 100%。

问题本质

  • 请求的数据在缓存和数据库中均不存在
  • 每次请求都要访问数据库
  • 恶意攻击可能利用此漏洞耗尽数据库资源

解决方案

  1. 缓存空对象
// 带空值缓存的服务
public class UserService {
    private final Cache<String, User> cache = RedisCache.getInstance();
    private static final long EMPTY_TTL = 60; // 空值缓存60秒

    public User getUser(String userId) {
        // 先查缓存
        User user = cache.get(userId);
        
        if (user != null) {
            return user == NULL_USER ? null : user; // 区分空对象和真实数据
        }
        
        // 查询数据库
        user = dbService.getUser(userId);
        
        if (user != null) {
            cache.put(userId, user);
        } else {
            // 缓存空对象,避免穿透
            cache.put(userId, NULL_USER, EMPTY_TTL);
        }
        
        return user;
    }
    
    // 空对象单例
    private static final User NULL_USER = new User();
}
  1. 布隆过滤器
// 带布隆过滤器的缓存服务
public class BloomFilterCacheService {
    // 创建布隆过滤器,预计插入100万数据,误判率0.01%
    private final BloomFilter<String> bloomFilter = BloomFilter.create(
        Funnels.stringFunnel(StandardCharsets.UTF_8),
        1_000_000,
        0.0001);
    
    private final Cache<String, Object> cache = RedisCache.getInstance();
    
    // 初始化布隆过滤器
    public void initBloomFilter() {
        // 从数据库加载所有有效Key到布隆过滤器
        List<String> allKeys = dbService.getAllKeys();
        allKeys.forEach(bloomFilter::put);
    }
    
    public Object getData(String key) {
        // 先通过布隆过滤器判断
        if (!bloomFilter.mightContain(key)) {
            return null; // 一定不存在,直接返回
        }
        
        // 布隆过滤器判断可能存在,再查缓存
        Object value = cache.get(key);
        if (value != null) {
            return value;
        }
        
        // 查询数据库
        value = dbService.getData(key);
        if (value != null) {
            cache.put(key, value);
        }
        
        return value;
    }
}

最佳实践

  • 布隆过滤器适用于数据相对固定的场景
  • 空值缓存时间不宜过长,避免影响业务
  • 对恶意请求 IP 进行限流和封禁

三、缓存雪崩:大规模缓存失效的多米诺效应

场景描述
某视频平台设置所有缓存 Key 的过期时间为凌晨 2 点。当缓存集群在此时重启后,海量请求直接打向数据库,导致数据库崩溃,服务不可用。

问题本质

  • 大量缓存 Key 同时过期
  • 缓存集群整体故障
  • 瞬时高并发请求压垮数据库

解决方案

  1. 随机过期时间
// 带随机过期的缓存服务
public class RandomExpireCacheService {
    private final Cache<String, Object> cache = RedisCache.getInstance();
    private final Random random = new Random();
    private static final int BASE_EXPIRE = 3600; // 基础过期时间1小时
    private static final int RANDOM_RANGE = 1800; // 随机范围30分钟

    public void setData(String key, Object value) {
        // 计算随机过期时间:基础时间 + 随机偏移
        long expireTime = BASE_EXPIRE + random.nextInt(RANDOM_RANGE);
        cache.put(key, value, expireTime);
    }

    public Object getData(String key) {
        return cache.get(key);
    }
}
  1. 多级缓存架构
// 多级缓存服务
public class MultiLevelCacheService {
    // 一级缓存:本地缓存,使用Caffeine
    private final Cache<String, Object> localCache = Caffeine.newBuilder()
        .maximumSize(10000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build();
    
    // 二级缓存:分布式缓存
    private final Cache<String, Object> distributedCache = RedisCache.getInstance();

    public Object getData(String key) {
        // 先查本地缓存
        Object value = localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        
        // 本地缓存未命中,查分布式缓存
        value = distributedCache.get(key);
        if (value != null) {
            // 回写本地缓存
            localCache.put(key, value);
            return value;
        }
        
        // 分布式缓存未命中,查数据库
        value = dbService.getData(key);
        if (value != null) {
            // 同时更新两级缓存
            localCache.put(key, value);
            distributedCache.put(key, value);
        }
        
        return value;
    }
}
  1. 熔断降级策略
// 带熔断降级的缓存服务
public class CircuitBreakerCacheService {
    private final Cache<String, Object> cache = RedisCache.getInstance();
    private final CircuitBreaker circuitBreaker = new CircuitBreaker(
        500, // 滑动窗口大小
        100, // 错误阈值
        5000); // 恢复时间窗口
    
    public Object getData(String key) {
        // 检查熔断器状态
        if (circuitBreaker.isOpen()) {
            // 熔断器打开,返回降级数据
            return getFallbackData(key);
        }
        
        try {
            // 执行正常逻辑
            return cache.get(key);
        } catch (Exception e) {
            // 记录失败,可能触发熔断
            circuitBreaker.recordFailure();
            return getFallbackData(key);
        }
    }
    
    private Object getFallbackData(String key) {
        // 返回降级数据,如默认值或缓存的历史数据
        return fallbackCache.get(key);
    }
}

最佳实践

  • 缓存集群采用高可用架构(主从 + 哨兵 + 集群)
  • 核心业务使用多级缓存,降低单点风险
  • 实现熔断降级机制,保证服务可用性

四、对比与总结

问题类型

核心差异

解决方案

适用场景

缓存击穿

单个热点 Key 失效

互斥锁 / 永不过期

爆款商品、明星动态

缓存穿透

请求不存在的数据

布隆过滤器 / 缓存空对象

恶意攻击、无效查询

缓存雪崩

大规模缓存失效

随机过期 / 多级缓存 / 熔断降级

全量数据刷新、集群故障

五、生产环境建议

  1. 监控与告警
  2. 实时监控缓存命中率、QPS、内存使用情况
  3. 设置缓存雪崩预警阈值(如集群节点失联比例)
  4. 弹性伸缩
  5. 高峰期自动扩容缓存集群
  6. 实现读写分离架构
  7. 灰度发布
  8. 新缓存策略先在测试环境验证
  9. 线上环境小流量试点后再全量推广

通过合理设计缓存架构、采用多级缓存策略、实现智能熔断降级,可有效解决缓存三大问题,保障分布式系统在高并发场景下的稳定性与可用性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值