缓存击穿解决案例

wen java案例 1

缓存击穿实战解决案例:从线程阻塞到毫秒级响应的架构演进


目录导读

  1. 什么是缓存击穿?——热key突然失效的灾难现场
  2. 常规解法:互斥锁(Mutex)的局限与陷阱
  3. 进阶解法:逻辑过期 + 双重检测(实战案例拆解)
  4. 终极防线:热点数据永不过期 & 熔断降级
  5. 案例复盘:某电商秒杀系统的缓存击穿修复全过程
  6. 常见问题问答(FAQ)

什么是缓存击穿?

缓存击穿指某个热点key(如爆款商品ID、热搜词)在缓存过期的一瞬间,大量请求同时穿透到数据库,与缓存雪崩(大量key同时失效)不同,击穿是单点压力问题,如果不处理,数据库QPS可能从1000瞬间飙升至10万,直接导致连接池耗尽。

缓存击穿解决案例

常规解法:互斥锁的局限

最经典的做法是加分布式锁(如Redis的SETNX):

String key = "hot:product:1001";
String lockKey = "lock:" + key;
boolean locked = redis.setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);
if (locked) {
    try {
        // 查DB,回填缓存
    } finally {
        redis.delete(lockKey);
    }
} else {
    Thread.sleep(50);
    // 重试获取缓存
}

但此方案有致命缺陷

  • 若查DB耗时2秒,则所有等待线程会阻塞在sleep上,造成线程池耗尽
  • 若Redis宕机或锁过期,则可能击穿保护失效
  • 高并发下,锁竞争本身会消耗大量Redis连接。

进阶解法:逻辑过期 + 双重检测(实战案例)

核心思想:不设置物理过期时间,而是存储一个“逻辑过期时间戳”,当读取时发现逻辑过期,只让一个线程去重建缓存,其他线程直接返回旧值

案例代码(伪代码)

public String getProductInfo(String productId) {
    // 1. 从Redis获取数据
    CacheData cacheData = redis.get("product:" + productId);
    long currentTime = System.currentTimeMillis();
    // 2. 未过期,直接返回
    if (cacheData.getExpireTime() > currentTime) {
        return cacheData.getData();
    }
    // 3. 逻辑过期,尝试获取互斥锁
    String lockKey = "rebuild:" + productId;
    boolean lock = redis.setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);
    if (lock) {
        // 异步线程池去重建缓存
        threadPool.execute(() -> {
            String dbData = queryDb(productId);
            CacheData newData = new CacheData(dbData, currentTime + 3600*1000);
            redis.set("product:" + productId, newData);
            redis.delete(lockKey);
        });
    }
    // 4. 无论是否拿到锁,都直接返回旧数据(避免阻塞)
    return cacheData.getData();
}

为什么更优?

  • 无阻塞:所有请求立即返回旧值,用户体验无感知;
  • 线程安全:只有一个线程负责重建,利用异步线程池隔离DB压力;
  • 防击穿:即使重建失败,旧数据仍在,不会打到数据库。

终极防线:热点数据永不过期

对于极热门的key(如首页Banner),可以设置物理永不过期,但通过后台定时任务主动刷新,例如每10分钟由任务调度系统重新生成缓存,完全规避击穿问题,此方案需配合熔断降级(如Sentinel)——若后台刷新失败,则直接返回降级数据(如静态JSON)。

案例复盘:某电商秒杀系统

背景:某大促活动期间,限时秒杀商品ID为999的缓存(过期时间5分钟)失效瞬间,5000个并发请求涌入MySQL,导致慢查询堆积,CPU飙升。

解决方案步骤

  1. 识别热点:通过Redis的hotkey分析,发现该key访问频率是其他商品的100倍;
  2. 改造为逻辑过期:存储expireTime为当前时间+5分钟;
  3. 增加本地缓存(Caffeine):JVM内先查本地缓存,命中率高达99%,减少对Redis的依赖;
  4. 配置熔断规则:若DB查询平均耗时超过500ms,直接快速失败返回默认值。

效果:修复后,数据库QPS稳定在200,接口响应时间从平均800ms降至15ms,秒杀活动顺利完成。

常见问题问答(FAQ)

Q1:逻辑过期方案中,如果旧数据本身就是脏的怎么办? A:需在重建缓存时校验数据版本号(如DB的update_time),若发现新版本与旧值不一致,则强制覆盖。

Q2:异步重建线程池怎么设置大小? A:建议为CPU核心数 * 2,并采用有界队列,线程池满时,可选择丢弃重建任务(旧数据仍能服务),防止内存溢出。

Q3:为什么不直接使用Redis的SET EX + GET(原子操作)? A:SET NX EX无法防止并发重建,且无法返回旧值,逻辑过期方案将“过期判断”与“数据读取”分离,更灵活。

Q4:本地缓存(Caffeine)如何保证一致性? A:设置写入后30秒过期,并通过Redis Pub/Sub监听DB更新事件,主动失效本地缓存。


缓存击穿不是“一击必杀”的难题,而是需要结合业务场景做复合策略,从互斥锁到逻辑过期,再到本地缓存+后台刷新,核心思路永远是用空间换时间用异步换同步,建议开发者在设计缓存层时,提前规划热点识别和降级预案,才能在高并发下立于不败之地。

抱歉,评论功能暂时关闭!