Guava Cache 本地缓存

FreeGuideOnline 最新 2026-07-12

xml com.google.guava guava 33.0.0-jre


Gradle:

```groovy
implementation 'com.google.guava:guava:33.0.0-jre'

第一个缓存示例

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

Cache<String, String> cache = CacheBuilder.newBuilder()
        .maximumSize(100)    // 最多100个条目
        .build();

// 存入数据
cache.put("key1", "value1");

// 取出数据,若不存在返回null
String value = cache.getIfPresent("key1");

// 自动加载式获取(如果不存在,通过Callable加载并存入缓存)
String loaded = cache.get("key2", () -> "computedValue");

核心构建器:CacheBuilder

所有缓存实例都通过 CacheBuilder 创建。它采用链式调用的方式配置缓存行为,最终通过 build()build(CacheLoader) 得到缓存对象。

CacheBuilder.newBuilder()
    // 各种配置...
    .build();                // 返回 Cache<K, V>
    // 或
    .build(loader);          // 返回 LoadingCache<K, V>

LoadingCache 继承了 Cache,并额外提供了自动加载的能力,是项目中最常用的缓存形态。

缓存加载策略

按需加载:Cache.get(key, Callable)

适用于加载逻辑不固定的场景。

Cache<String, User> cache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .build();

User user = cache.get("user_123", () -> userService.getUserFromDB(123));

如果 key 已缓存则直接返回,否则执行 Callable 加载、存入缓存并返回。

自动加载:LoadingCache + CacheLoader

当所有key都遵循相同的加载逻辑时,CacheLoader 是更好的选择。

LoadingCache<String, User> cache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .build(new CacheLoader<String, User>() {
            @Override
            public User load(String key) throws Exception {
                return userService.getUserFromDB(extractId(key));
            }
        });

User user = cache.get("user_123");

get(key) 会检查缓存,若缺失则自动调用 load() 方法加载。该方法会缓存加载结果,并保证对同一key的并发加载只执行一次。

批量加载:loadAll

当你需要一次加载多个key时,实现 CacheLoader.loadAll() 可以大幅降低数据库/远程调用的次数。

LoadingCache<String, User> cache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .build(new CacheLoader<String, User>() {
            @Override
            public User load(String key) { /* 单key加载 */ }

            @Override
            public Map<String, User> loadAll(Iterable<? extends String> keys) {
                return userService.batchGetUsers(keys);
            }
        });

// 批量获取,优先使用loadAll
ImmutableMap<String, User> users = cache.getAll(keys);

回收(淘汰)策略

合理的回收策略能防止缓存无限增长导致内存溢出。Guava Cache 支持以下维度:

基于数量

CacheBuilder.maximumSize(10_000)

当缓存条目数接近限制时,将按照**LRU(最近最少使用)**算法淘汰旧条目。也可用 maximumWeight 结合 weigher 实现按权重回收。

基于写入时间

CacheBuilder.expireAfterWrite(10, TimeUnit.MINUTES)

条目在写入后经过一定时间自动过期。适用于“一段时间后数据必然变脏”的场景。

基于访问时间

CacheBuilder.expireAfterAccess(5, TimeUnit.MINUTES)

条目在最后一次读或写后,经过指定时间未被访问则过期。适用于维护热点数据。

基于引用

CacheBuilder.weakKeys()
            .weakValues()
            .softValues()
  • weakKeys() / weakValues():用弱引用存储键或值,当对象没有强引用时会被GC回收。
  • softValues():用软引用存储值,仅在内存不足时被回收。

⚠️ 使用引用相关的回收时,注意 == 比较和 equals 的区别,通常需要用 weakKeys() 配合 CacheBuilder 时要格外小心键的相等性。

主动失效

除自动回收外,你还可以显式移除条目:

  • cache.invalidate(key) – 单个失效
  • cache.invalidateAll(keys) – 批量失效
  • cache.invalidateAll() – 清空整个缓存

刷新机制

与“过期后需重新加载”不同,刷新允许异步地重新加载新值,同时旧值仍然可用(直到新值加载完成)。这可以避免缓存过期瞬间的加载风暴。

LoadingCache<String, User> cache = CacheBuilder.newBuilder()
        .refreshAfterWrite(5, TimeUnit.MINUTES)
        .build(loader);

// 手动触发刷新(异步)
cache.refresh(key);

refreshAfterWrite 指定写入多久后应当刷新。当某个key在过期时间后第一次被访问时,会触发异步刷新,返回旧值;刷新完成后新值替换旧值。

注意:刷新不是自动后台线程执行的,而是由访问请求触发的。若需定期刷新,可结合定时任务主动调用 cache.refresh() 或使用 ScheduledExecutorService

统计信息

Guava Cache 可以记录缓存的命中率等指标,方便监控和调优:

Cache<String, User> cache = CacheBuilder.newBuilder()
        .recordStats()
        .maximumSize(1000)
        .build();

// 使用一段时间后获取统计信息
CacheStats stats = cache.stats();
System.out.println("命中率: " + stats.hitRate());
System.out.println("平均加载时间: " + stats.averageLoadPenalty() / 1_000_000 + "ms");

主要指标包括:命中次数、未命中次数、加载成功次数、加载异常次数、加载耗时等。

监听器

通过 removalListener 可以在条目被移除时执行自定义逻辑,比如记录日志或释放资源。

Cache<String, User> cache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .removalListener(notification -> {
            System.out.println("移除原因: " + notification.getCause());
            System.out.println("Key: " + notification.getKey());
            System.out.println("Value: " + notification.getValue());
        })
        .build();