如何用Java案例实现缓存击穿?从原理到代码,一篇讲透
目录导读
- 缓存击穿是什么?为什么你必须了解它?
- 缓存击穿与缓存雪崩、缓存穿透的区别
- Java实现缓存击穿的3种核心方案
- 终极实战:基于Redis+Java的完整代码案例
- 常见问答:企业级开发避坑指南
缓存击穿是什么?为什么你必须了解它?
缓存击穿(Cache Breakdown)指的是:一个热点Key在缓存失效的瞬间,大量并发请求同时打到数据库,导致数据库瞬间负载飙升甚至崩溃。

日常比喻:就像景区最火的网红店突然关门,所有游客一窝蜂冲进后台找老板,老板直接被挤晕。
为什么危险?
- 数据库连接池耗尽 → 服务不可用
- 热点Key往往是核心数据(如秒杀商品、热搜榜单)
- 一旦发生,恢复时间长,影响全站体验
缓存击穿与缓存雪崩、缓存穿透的区别
| 概念 | 触发场景 | 核心问题 |
|---|---|---|
| 缓存穿透 | 查询根本不存在的数据 | 恶意攻击绕过缓存 |
| 缓存击穿 | 热点Key失效瞬间 | 高并发冲击数据库 |
| 缓存雪崩 | 大量Key同时失效 | 数据库被瞬间淹没 |
一句话记法:
- 穿透是“打空气”
- 击穿是“打热点”
- 雪崩是“打一片”
Java实现缓存击穿的3种核心方案
方案A:互斥锁(Mutex Lock)
原理:当缓存失效,只允许一个线程去数据库查询,其他线程等待结果。
Java实现:使用ReentrantLock或Redisson分布式锁。
方案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客户端和锁对象的注入方式。