如何用Java案例实现缓存击穿?

wen java案例 3

如何用Java案例实现缓存击穿?从原理到代码,一篇讲透

目录导读

  1. 缓存击穿是什么?为什么你必须了解它?
  2. 缓存击穿与缓存雪崩、缓存穿透的区别
  3. Java实现缓存击穿的3种核心方案
  4. 终极实战:基于Redis+Java的完整代码案例
  5. 常见问答:企业级开发避坑指南

缓存击穿是什么?为什么你必须了解它?

缓存击穿(Cache Breakdown)指的是:一个热点Key在缓存失效的瞬间,大量并发请求同时打到数据库,导致数据库瞬间负载飙升甚至崩溃。

如何用Java案例实现缓存击穿?

日常比喻:就像景区最火的网红店突然关门,所有游客一窝蜂冲进后台找老板,老板直接被挤晕。

为什么危险?

  • 数据库连接池耗尽 → 服务不可用
  • 热点Key往往是核心数据(如秒杀商品、热搜榜单)
  • 一旦发生,恢复时间长,影响全站体验

缓存击穿与缓存雪崩、缓存穿透的区别

概念 触发场景 核心问题
缓存穿透 查询根本不存在的数据 恶意攻击绕过缓存
缓存击穿 热点Key失效瞬间 高并发冲击数据库
缓存雪崩 大量Key同时失效 数据库被瞬间淹没

一句话记法

  • 穿透是“打空气”
  • 击穿是“打热点”
  • 雪崩是“打一片”

Java实现缓存击穿的3种核心方案

方案A:互斥锁(Mutex Lock)

原理:当缓存失效,只允许一个线程去数据库查询,其他线程等待结果。
Java实现:使用ReentrantLockRedisson分布式锁。

方案B:逻辑过期(提前预加载+异步刷新)

原理:缓存不设物理过期时间,而是存储一个“逻辑过期时间”,通过后台线程异步刷新。
适用场景:对实时性要求不高的热点数据。

方案C:永不过期(布隆过滤器+手动失效)

原理:热点Key永不过期,数据更新时手动删除或更新缓存。
关键点:结合消息队列确保数据一致性。


终极实战:基于Redis+Java的完整代码案例

我们以互斥锁方案为例,写一个可运行的Java案例。

Step 1:引入Maven依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.3.1</version>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.20.0</version>
</dependency>

Step 2:核心代码:带缓存击穿防护的查询方法

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
public class CacheService {
    private Jedis jedis;
    private RedissonClient redisson;
    // 模拟数据库查询
    public String queryFromDB(String key) {
        System.out.println(Thread.currentThread().getName() + " 查询数据库...");
        // 实际场景:返回数据库结果
        return "真实数据_" + System.currentTimeMillis();
    }
    public String getDataWithBreakdownProtection(String key) {
        // 1. 先从缓存获取
        String cacheValue = jedis.get(key);
        if (cacheValue != null) {
            return cacheValue;
        }
        // 2. 缓存失效,尝试获取分布式锁
        String lockKey = "lock:" + key;
        RLock lock = redisson.getLock(lockKey);
        try {
            // 尝试获取锁,等待3秒,租期10秒
            boolean isLocked = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (isLocked) {
                // 双重检查:防止锁竞争导致重复查询
                cacheValue = jedis.get(key);
                if (cacheValue != null) {
                    return cacheValue;
                }
                // 3. 查询数据库并写入缓存
                String dbValue = queryFromDB(key);
                jedis.setex(key, 3600, dbValue); // 设置1小时过期
                return dbValue;
            } else {
                // 没拿到锁,等待并重试
                Thread.sleep(100);
                return getDataWithBreakdownProtection(key);
            }
        } catch (Exception e) {
            e.printStackTrace();
            // 降级:允许少量请求直接查询数据库
            return queryFromDB(key);
        } finally {
            lock.unlock();
        }
    }
}

Step 3:模拟高并发测试

public class ConcurrentTest {
    public static void main(String[] args) {
        // 假设100个线程同时请求同一个热点Key
        ExecutorService executor = Executors.newFixedThreadPool(100);
        for (int i = 0; i < 100; i++) {
            executor.submit(() -> {
                String result = cacheService.getDataWithBreakdownProtection("hot_key");
                System.out.println(result);
            });
        }
        executor.shutdown();
    }
}

运行结果:只有1~2个线程会真正查询数据库,其他99个线程从缓存或锁等待中拿到数据。


常见问答:企业级开发避坑指南

Q1:互斥锁方案在高并发下性能会下降吗?

A:会有一点延迟,但远低于数据库崩溃的代价,建议将锁的等待时间控制在200ms内,并结合“熔断降级”机制。

Q2:如果热点Key是用户维度的(如“user_id:100”),这种方案适用吗?

A:不适用,用户级Key通常不是热点,缓存击穿主要针对全局热点,比如首页推荐、排行榜,用户级数据建议直接用缓存穿透防护。

Q3:逻辑过期方案如何保证数据一致性?

A:通过MQ异步更新缓存,并设置“最终一致性”容忍窗口,如果对一致性要求极高(如金融交易),请使用数据库乐观锁+redis事务。

Q4:布隆过滤器能解决缓存击穿吗?

A:不能,布隆过滤器只解决“缓存穿透”,不解决热点Key失效后的冲击,两者是不同问题。


总结建议

对于多数Java后端项目,推荐组合方案

  • 热点Key < 10个 → 使用互斥锁方案,代码简单,可控性强。
  • 热点Key > 10个 → 使用逻辑过期方案,并搭配监听Binlog或MQ来自动刷新缓存。

无论选择哪种,一定要做压力测试,并设置合理的降级策略(如返回旧缓存数据、节流)。


本文案例代码可直接复用到SpringBoot项目中,唯一需要替换的就是Redis客户端和锁对象的注入方式。

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