Java缓存命中案例怎么判断处理

wen java案例 23

本文目录导读:

Java缓存命中案例怎么判断处理

  1. 如何判断缓存是否命中?
  2. 命中与未命中的处理策略
  3. 典型案例:缓存命中与处理流程
  4. 常见陷阱与调优
  5. 判断与处理流程图

判断和处理 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 是否存在
    │                    │
    │ 是                │ 否
    ▼                    ▼
返回缓存值          查询数据库
    │                    │
    │                ┌───┴───┐
    │                │       │
    │            数据库有值  数据库无值
    │                │       │
    │                ▼       ▼
    │            写入缓存(带过期时间)  写入缓存空值(防穿透)
    │                │                │
    └────── 退出 ◄───┘ ←──────────────┘

核心原则

  1. 先查缓存,再查数据库
  2. 未命中时,一定要同步回填缓存(或标记空值)
  3. 高并发下,使用加锁 + 双重检查防止重复加载
  4. 根据业务容忍度,决定是否缓存空值或使用布隆过滤器

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