Java缓存击穿案例如何规避解决

wen java案例 23

本文目录导读:

Java缓存击穿案例如何规避解决

  1. 什么是缓存击穿?先看一个案例
  2. 核心解决思路
  3. 方案一:加互斥锁(Mutex Lock) ⭐️⭐️⭐️⭐️⭐️
  4. 方案二:逻辑过期 + 异步重建 ⭐️⭐️⭐️⭐️
  5. 方案三:热点 key 永不过期(物理层面) ⭐️⭐️⭐️
  6. 方案四:缓存预热 + 打散过期时间 ⭐️⭐️
  7. 实战建议(按场景选择)
  8. 避坑指南(经验之谈)
  9. 面试话术总结

这是一个非常经典的面试题,也是线上高并发场景中容易踩的坑,我会从一个真实的业务案例入手,再给出从简单到复杂的完整解决方案


什么是缓存击穿?先看一个案例

场景假设

假设你有一个电商系统,商品详情页的缓存策略是:

  1. 用户请求商品ID为 9527 的详情。
  2. Redis 中查询 key = product:9527
  3. 如果缓存中有,直接返回;如果没有,去 MySQL 查询,然后回写 Redis,并设置过期时间(1 小时)。

问题爆发

某天,运营在后台手动删除了 product:9527 的缓存(或者该缓存恰好到了过期时间)。

  • 突然涌入 10 万个用户 同时请求该商品详情。
  • 10万个请求全部发现缓存中没有 product:9527
  • 于是这 10万个请求同时涌入 MySQL 去查询数据库。
  • MySQL 瞬间连接打满、CPU 飙升,导致数据库崩溃

这就是缓存击穿

定义:一个热点 key 在缓存失效的瞬间,大量并发请求直接打到数据库,导致数据库压力暴增甚至宕机。


核心解决思路

核心就一句话:不让大量请求同时通过缓存缺口直达数据库

具体有以下几种主流方案:


方案一:加互斥锁(Mutex Lock) ⭐️⭐️⭐️⭐️⭐️

这是最常用、最有效的方法,保证同一时刻,只有一个线程去数据库查询并重建缓存,其他线程等待。

实现方式(基于 Redis SETNX 或 Redisson)

public String getProductDetail(String productId) {
    String cacheKey = "product:" + productId;
    String value = redis.get(cacheKey);
    if (value != null) {
        return value; // 缓存命中
    }
    // 开始:缓存未命中,尝试加锁
    String lockKey = "lock:product:" + productId;
    boolean locked = redis.setnx(lockKey, "1", 3, TimeUnit.SECONDS); // 加锁并设置过期时间
    if (locked) {
        try {
            // 二次检查:可能别的线程已经重建了缓存
            value = redis.get(cacheKey);
            if (value != null) {
                return value;
            }
            // 去数据库查询
            value = queryDB(productId);
            redis.set(cacheKey, value, 3600, TimeUnit.SECONDS);
            return value;
        } finally {
            redis.del(lockKey); // 释放锁
        }
    } else {
        // 未获得锁的线程,等待后重试(或者自旋)
        Thread.sleep(100);
        return getProductDetail(productId); // 递归重试
    }
}

优缺点

优点 缺点
简单可靠,保证数据库安全 牺牲了一点可用性(少量等待)
适用于绝大多数场景 如果锁实现不好,可能死锁

逻辑过期 + 异步重建 ⭐️⭐️⭐️⭐️

不设置物理过期时间,而是缓存一个逻辑过期时间,当发现逻辑过期时,只让一个线程去重建,其他线程继续返回旧数据。

实现方式

public String getProductDetail(String productId) {
    String cacheKey = "product:" + productId;
    String cacheData = redis.get(cacheKey);
    if (cacheData == null) {
        return null; // 或者直接查DB返回(理论上不会发生)
    }
    // 解析出业务数据和过期时间
    ProductCache cache = parseCache(cacheData);
    if (cache.getExpireTime() > System.currentTimeMillis()) {
        return cache.getData(); // 未过期,直接返回
    }
    // 逻辑过期:尝试获取锁,异步重建
    String lockKey = "rebuild:product:" + productId;
    boolean locked = redis.setnx(lockKey, "1", 3, TimeUnit.SECONDS);
    if (locked) {
        // 异步线程池去重建缓存
        executorService.submit(() -> {
            String newData = queryDB(productId);
            ProductCache newCache = new ProductCache(newData, System.currentTimeMillis() + 3600*1000);
            redis.set(cacheKey, JSON.toJSONString(newCache));
            redis.del(lockKey);
        });
    }
    // 无论是否获得锁,都返回旧数据
    return cache.getData();
}

优缺点

优点 缺点
极高性能,不阻塞任何请求 数据短暂不一致(旧数据)
抗高并发 实现稍复杂

热点 key 永不过期(物理层面) ⭐️⭐️⭐️

对于明确知道的热点 key,不给它设置过期时间,但为了避免数据永久不更新,可以后台定时更新

实现方式

// 1. 初始化时,永久缓存热点数据
redis.set("product:9527", queryDB("9527")); // 不设过期时间
// 2. 后台定时任务,每隔一段时间更新缓存
@Scheduled(fixedDelay = 10 * 60 * 1000) // 每10分钟
public void refreshHotProduct() {
    String productId = "9527";
    String newData = queryDB(productId);
    redis.set("product:" + productId, newData);
}

优缺点

优点 缺点
绝对不会击穿 占用内存
实现极简单 热点 key 需要人工识别
如果定时任务挂了,数据会陈旧

缓存预热 + 打散过期时间 ⭐️⭐️

在系统启动或活动开始前,手动将热点数据提前加载到缓存,并给过期时间增加随机偏移

// 不设置统一的3600秒,而是随机波动
int baseExpire = 3600;
int randomDelta = new Random().nextInt(300); // 0~300秒随机
redis.set(key, value, baseExpire + randomDelta, TimeUnit.SECONDS);

目的

避免大量热点 key 同时过期,从而降低击穿概率。


实战建议(按场景选择)

业务场景 推荐方案 理由
高一致性要求(如余额、库存) 互斥锁 保证数据最新、最准确
并发极高,可接受短暂脏读(如首页、商品列表) 逻辑过期+异步重建 性能最好,不阻塞
已知的极少热点 key 永不过期+定时更新 最简单,最可靠
一般业务系统,不确定热点 随机过期时间 低成本防范

避坑指南(经验之谈)

  1. 别用 Java 内置锁(synchronized) → 分布式环境下无效,需要用 Redis 分布式锁。
  2. 加锁一定要设置过期时间 → 防止死锁(SETNX + EXPIRE要原子操作或用 Redisson)。
  3. DB 查询要加熔断/限流 → 即使有缓存保护,也要防止查询把 DB 打满(比如使用 Hystrix / Sentinel)。
  4. 监控热点 key → 提前识别哪些 key 访问量最高,可以主动做缓存预热或者手动设置永不过期

面试话术总结

“针对缓存击穿,我一般首选互斥锁方案,当缓存失效时,只会有一个线程去数据库查询并重建缓存,其他线程等待或自旋重试。
如果业务对少量数据不一致不敏感且并发极高,我会用逻辑过期+异步重建方案,性能更好。
同时我会配合缓存预热过期时间随机化来降低击穿概率。
会用 Redisson 实现分布式锁,避免死锁,并配合 Sentinel 做 DB 限流保护。”

这样的回答既有深度又有实战经验,面试官通常会很满意。

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