本文目录导读:

判断和处理 Java 缓存命中案例,本质上就是识别热点数据、确认缓存是否有效以及制定缓存失效后的处理策略。
以下从判断是否命中、处理命中/未命中的逻辑、典型案例分析以及常见陷阱与优化四个维度进行拆解。
如何判断缓存是否命中?
在 Java 中,缓存通常以 Map 结构(如 ConcurrentHashMap)或专门的缓存库(如 Caffeine、Redis)实现,判断命中的核心逻辑是:根据 Key 查询缓存,检查返回结果是否为 null 或空。
标准判断逻辑(最常用)
public Object queryData(String key) {
// 1. 先从缓存中获取
Object cachedValue = cache.get(key);
// 2. 判断是否命中
if (cachedValue != null) {
// 命中:直接返回
return cachedValue;
}
// 3. 未命中:从数据源加载并回填缓存
Object dbValue = queryFromDatabase(key);
if (dbValue != null) {
cache.put(key, dbValue); // 回填缓存
}
return dbValue;
}
关键判断点:cache.get(key) != null 即为命中,null 即为未命中。
缓存库的显式命中判断(如 Caffeine)
// Caffeine 提供了 getIfPresent 方法,显式判断
Cache<String, Object> cache = Caffeine.newBuilder().build();
Object value = cache.getIfPresent(key);
if (value != null) {
// 命中
return value;
}
// 未命中:同步加载
return cache.get(key, k -> loadFromDB(k));
针对“空值”的防穿透处理
如果数据库中本身就不存在该数据(查询结果为 null),普通判断会导致每次查询都穿透到数据库。需要区分“缓存未命中”和“数据不存在”:
public Object queryDataSafe(String key) {
// 1. 查缓存(缓存中可能存了一个占位 null 值)
Object cachedValue = cache.get(key);
// 2. 判断“缓存中存在”而不是“值为 null”
if (cache.containsKey(key)) {
// 如果缓存中存储的是 null,表示“数据库无此数据”
return cachedValue; // 直接返回 null,无需查库
}
// 3. 缓存中不存在:查数据库
Object dbValue = queryFromDatabase(key);
// 不管数据库返回什么,都放入缓存(哪怕是 null)
cache.put(key, dbValue);
return dbValue;
}
最佳实践:在缓存中针对不存在的 Key,显式放入一个
null值并设置较短的过期时间(如 5 分钟),防止缓存穿透。
命中与未命中的处理策略
命中时的处理
- 直接返回:将缓存中的数据反序列化后返回。
- 更新缓存时效:如果使用 Redis,可以
expire重置过期时间,避免热点数据突然失效。
未命中时的处理(常见 3 种模式)
| 模式 | 适用场景 | 核心代码 |
|---|---|---|
| 同步回填 | 数据更新不频繁,并发低 | 查询 DB → 放入缓存 → 返回 |
| 异步回填 | 数据量大但可以容忍短暂不一致 | 先返回空/旧数据,后台线程更新缓存 |
| 分布式锁回填 | 高并发下防止缓存击穿 | 获取锁 → 查库 → 释放锁,避免同时大量请求穿透 |
高并发缓存击穿示例(使用分布式锁):
public Object queryWithLock(String key) {
Object value = cache.get(key);
if (value != null) return value;
// 未命中,加锁防止大量请求同时穿透
String lockKey = "lock:" + key;
boolean locked = redisLock.tryLock(lockKey, 500);
if (locked) {
try {
// 双重检查:是否已被其他线程回填
value = cache.get(key);
if (value == null) {
value = loadFromDB(key);
cache.put(key, value);
}
} finally {
redisLock.unlock(lockKey);
}
} else {
// 未获取到锁的线程:等待或返回空
Thread.sleep(10);
value = cache.get(key); // 重试
}
return value;
}
典型案例:缓存命中与处理流程
案例 1:用户信息查询(简单命中判断)
public User getUserById(Long userId) {
String key = "user:" + userId;
// 判断是否命中
String json = redisUtil.get(key);
if (json != null) {
// 命中:反序列化返回
return JSON.parseObject(json, User.class);
}
// 未命中:查数据库
User user = userMapper.selectById(userId);
if (user != null) {
// 回填缓存,设置过期时间 1 小时
redisUtil.set(key, JSON.toJSONString(user), 3600);
} else {
// 数据库不存在:防止穿透,缓存空值 5 分钟
redisUtil.set(key, "NULL", 300);
}
return user;
}
案例 2:商品库存缓存(带锁防击穿)
public int getStock(Long productId) {
String key = "stock:" + productId;
Integer stock = (Integer) cache.get(key);
if (stock != null) return stock;
// 未命中:加锁
synchronized (this) { // 生产环境建议用分布式锁
stock = (Integer) cache.get(key);
if (stock == null) {
stock = stockMapper.getStock(productId);
cache.put(key, stock, 30); // 缓存30秒
}
}
return stock;
}
常见陷阱与调优
“缓存雪崩” —— 大量 Key 同时失效
- 现象:大量请求同时未命中,瞬间压垮数据库。
- 处理:设置过期时间时添加随机值(如
基础时间 + Random(0, 300)秒)。
“缓存穿透” —— 查询不存在的 Key
- 现象:每次请求都直接查库,因为缓存中没有该 Key。
- 处理:缓存空值(如
"NULL")或使用布隆过滤器提前拦截。
“缓存击穿” —— 热点 Key 突然失效
- 现象:某个热点 Key 失效,大量并发请求同时未命中。
- 处理:使用互斥锁(如
ReentrantLock或 Redis 分布式锁)让单个线程去加载数据。
数据一致性判断
- 业务要求强一致性:缓存配合数据库事务,写操作时先删除缓存再更新数据库(延迟双删)。
- 业务允许最终一致:设置合理的过期时间,让缓存自动失效后重新加载。
判断与处理流程图
请求到来
│
▼
查询缓存 → 判断 Key 是否存在
│ │
│ 是 │ 否
▼ ▼
返回缓存值 查询数据库
│ │
│ ┌───┴───┐
│ │ │
│ 数据库有值 数据库无值
│ │ │
│ ▼ ▼
│ 写入缓存(带过期时间) 写入缓存空值(防穿透)
│ │ │
└────── 退出 ◄───┘ ←──────────────┘
核心原则:
- 先查缓存,再查数据库。
- 未命中时,一定要同步回填缓存(或标记空值)。
- 高并发下,使用加锁 + 双重检查防止重复加载。
- 根据业务容忍度,决定是否缓存空值或使用布隆过滤器。