2、本地缓存实战:Guava Cache 基础用法、过期策略、最大容量限制、统计信息

好,咱们直接上手。上一章聊了缓存的分层和理论,这一章我带你落地第一个实战工具——Guava Cache。

为什么选它?因为它是 Google 开源的,成熟、稳定,而且 API 设计得非常优雅。我在好几个高并发项目里都用它做本地缓存,从来没出过幺蛾子。说白了,它就是 Java 世界里本地缓存的「标配」。

2.1 快速上手:三行代码搞定一个缓存

先看最基础的使用方式。你不需要引入什么复杂的框架,只要在 pom.xml 里加一个依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.3-jre</version>
</dependency>

然后,你就可以用 CacheBuilder 来构建缓存了。我个人习惯把缓存定义成一个静态字段,方便全局复用:

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.concurrent.TimeUnit;

public class UserCache {
    private static final Cache<String, User> userCache =
        CacheBuilder.newBuilder()
            .maximumSize(1000)               // 最多缓存 1000 条
            .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后 10 分钟过期
            .recordStats()                   // 开启统计信息
            .build();

    public static User getUser(String userId) {
        try {
            return userCache.get(userId, () -> loadFromDB(userId));
        } catch (Exception e) {
            // 处理异常
            return null;
        }
    }

    private static User loadFromDB(String userId) {
        // 模拟数据库查询
        System.out.println("查询数据库: " + userId);
        return new User(userId, "name_" + userId);
    }
}

看到了吗?核心就三件事:设置容量、设置过期、开启统计。然后通过 get(key, loader) 方法,如果缓存里有就直接返回,没有就自动调用 loader 加载并放入缓存。这个模式叫「Cache-Aside」,也是实际项目中最常用的。

💡 我的小技巧: 用 get(key, loader) 而不是先 getIfPresent 再手动 put。前者是原子操作,能避免并发场景下的「缓存穿透」问题。我见过太多人自己写 if-else,结果在高并发下把数据库打挂了。

2.2 过期策略:不是所有数据都该永久保留

缓存如果不设过期时间,那就是个「内存泄漏器」。Guava Cache 提供了三种过期策略,我分别说说。

2.2.1 基于写入时间过期(expireAfterWrite)

这是最常用的策略。数据写入缓存后,经过指定时间就自动失效。适合那些变化不频繁、但需要保证最终一致性的数据,比如用户基本信息、配置项。

.expireAfterWrite(30, TimeUnit.MINUTES)

2.2.2 基于访问时间过期(expireAfterAccess)

这个策略是:如果某个缓存项在指定时间内没有被读取或写入,就失效。适合做「热点数据」的淘汰——不热的数据就让它走人。

.expireAfterAccess(5, TimeUnit.MINUTES)
⚠️ 注意: 我曾经在一个 Session 缓存场景里用了 expireAfterAccess,结果发现用户明明在活跃,但 Session 还是被清掉了。为什么?因为 expireAfterAccess 只记录「最后一次访问时间」,如果用户每隔 4 分钟发一次请求,那缓存永远不会过期。但如果你设的是 5 分钟,用户在第 6 分钟发请求时,缓存已经没了。所以,一定要根据业务的实际访问间隔来设置。

2.2.3 基于引用的过期(weakKeys / weakValues / softValues)

这个用得少,但特定场景很管用。它允许 JVM 在内存不足时回收缓存项。我一般只在缓存大对象、且允许丢失时才用。

.softValues() // 使用软引用,GC 时可能被回收

2.3 最大容量限制:别让缓存吃掉你的堆内存

本地缓存最大的坑就是「内存溢出」。Guava Cache 通过 maximumSize 和 maximumWeight 来控制容量。

2.3.1 maximumSize:按条目数限制

简单粗暴,直接限制缓存能放多少个 key-value 对。当超过限制时,会基于 LRU(最近最少使用) 算法淘汰最不常用的数据。

.maximumSize(5000)

2.3.2 maximumWeight:按权重限制

如果你的缓存项大小不一(比如有的 value 是 1KB,有的是 1MB),用条目数就不太合理了。这时候可以用权重:

.maximumWeight(100_000) // 总权重不超过 100000
.weigher((String key, User value) -> {
    // 假设每个 User 对象的大小估算
    return value.getName().length() + 50;
})

嗯,这里要注意:maximumWeight 必须和 weigher 一起使用,否则会抛异常。我刚开始用的时候就踩过这个坑。

2.4 统计信息:用数据说话

你优化缓存,总得知道缓存到底有没有用吧?Guava Cache 提供了 recordStats() 方法,开启后可以获取命中率、加载次数、淘汰次数等关键指标。

CacheStats stats = userCache.stats();
System.out.println("命中率: " + stats.hitRate());
System.out.println("命中次数: " + stats.hitCount());
System.out.println("未命中次数: " + stats.missCount());
System.out.println("加载成功次数: " + stats.loadSuccessCount());
System.out.println("加载异常次数: " + stats.loadExceptionCount());
System.out.println("总加载时间: " + stats.totalLoadTime());

我个人习惯在监控面板上展示这些数据。如果命中率低于 80%,说明缓存策略有问题——要么过期时间太短,要么容量太小,要么缓存的数据本身就不适合缓存。

📊 实战经验: 我在一个订单查询接口里,刚开始命中率只有 45%。通过统计信息发现,大部分未命中是因为 key 里带了时间戳(比如 "order_2024_001"),导致每次查询都是新 key。后来我把 key 设计成只包含订单 ID,命中率直接飙升到 92%。缓存设计,key 的粒度是关键。

2.5 核心概念一览

我把 Guava Cache 的核心配置整理成了一张表,方便你对照使用:

配置项 作用 适用场景
maximumSize 按条目数限制缓存容量 缓存项大小均匀时
maximumWeight 按权重限制缓存容量 缓存项大小差异大时
expireAfterWrite 写入后固定时间过期 数据变化不频繁,需最终一致性
expireAfterAccess 最后一次访问后固定时间过期 热点数据淘汰,非活跃数据清理
recordStats 开启命中率等统计 需要监控缓存效果时
removalListener 监听缓存项被淘汰的事件 需要做清理回调时(如关闭连接)

2.6 一张图看懂 Guava Cache 工作流程

下面这张 SVG 图展示了 Guava Cache 的核心流程:从请求进来,到命中返回,再到未命中加载,最后淘汰旧数据。你一看就明白。

Guava Cache 核心工作流程 请求 get(key) 缓存命中? 返回缓存值 未命中,加载数据 Callable / 手动 put 放入缓存 返回加载结果 淘汰检查(LRU/过期) 淘汰旧数据 更新统计信息 💡 关键点 命中率高 = 缓存有效 命中率低 = 检查 key 设计

2.7 避坑指南:我踩过的三个坑

最后,分享几个我实际项目中踩过的坑,你遇到了可以少走弯路。

  • 坑一:忘记开启 recordStats。没有统计信息,你根本不知道缓存效果怎么样。我建议所有缓存都默认开启。
  • 坑二:过期时间设得太短。我曾经把 expireAfterWrite 设为 1 分钟,结果数据库压力一点没减。后来分析发现,数据其实可以缓存 10 分钟。记住:过期时间要基于业务容忍度来设,不是拍脑袋。
  • 坑三:key 设计不合理。比如把用户 ID 和请求参数拼在一起当 key,导致同一个用户的不同请求都未命中。正确的做法是:key 只包含业务唯一标识,不要带无关信息。