Guava——缓存

缓存

1、概述

Google Guava 的 Cache 模块是 Java 中非常流行且功能强大的本地缓存实现。它不仅提供了简单的键值对存储,还支持自动加载、缓存过期、基于容量或权重的淘汰策略、软引用/弱引用以及缓存统计等高级特性。

Guava Cache 适用于以下场景:

  • 愿意消耗一部分内存空间来提升访问速度。
  • 预期某些键会被多次查询。
  • 缓存中的数据总量不会超过可用内存。

引入依赖

xml 复制代码
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.3-jre</version> <!-- 请检查并使用最新版本 -->
</dependency>

核心接口介绍

Guava Cache 的构建主要依靠 CacheBuilder,它采用建造者模式来配置缓存的各项参数。最终可以构建出两种类型的缓存:

  • LoadingCache:在缓存未命中时,会自动调用配置好的 CacheLoader 来加载数据并放入缓存。
  • Cache:需要手动插入数据,或者在获取时传入 Callable 来动态计算。

2、核心API

2.1、CacheBuilder 配置 API

方法名 说明
newBuilder() 工厂方法,初始化 CacheBuilder 实例
initialCapacity(int) 设置底层哈希表的初始容量
maximumSize(long) 设置缓存最大条目数(基于 LRU 策略淘汰)
maximumWeight(long) 设置缓存最大总权重(需配合 weigher 使用)
weigher(Weigher) 自定义缓存项的权重计算规则
expireAfterWrite(long, TimeUnit) 写入或更新后指定时间过期
expireAfterAccess(long, TimeUnit) 读写后指定时间过期(类似于 Session 过期)
refreshAfterWrite(long, TimeUnit) 写入指定时间后,后续请求触发异步/同步刷出旧数据
weakKeys() / weakValues() 使用弱引用存储 Key / Value
softValues() 使用软引用存储 Value(内存不足时垃圾回收)
removalListener(RemovalListener) 注册缓存移除事件的监听器
recordStats() 开启指标统计(未调用则统计数据均为 0)
build() / build(CacheLoader) 构建 Cache 或 LoadingCache 实例

2.2、Cache 读写 API

方法名 说明
getIfPresent(Object key) 获取 Key 对应的缓存;未命中返回 null
get(K key, Callable loader) 获取缓存;未命中时执行 loader 计算、写入缓存并返回
put(K key, V value) 手动将键值对存入缓存
putAll(Map map) 批量存入键值对
invalidate(Object key) 手动清除指定 key 的缓存
invalidateAll(Iterable keys) 批量清除指定 keys 的缓存
invalidateAll() 清空缓存中的所有数据
size() 返回当前缓存的大致条目数量
asMap() 将 Cache 转为 ConcurrentMap 视图(操作会联动原缓存)
stats() 返回当前缓存的 CacheStats 统计信息

2.3、LoadingCache 自动加载 API(继承自 Cache)

方法名 说明
get(K key) 获取缓存;未命中时自动调用 CacheLoader.load 加载(抛受检异常)
getUnchecked(K key) 与 get 功能一致,但将受检异常包装为 UncheckedExecutionException
getAll(Iterable keys) 批量获取;未命中时调用 CacheLoader.loadAll 进行批量加载
refresh(K key) 显式触发指定 key 的数据刷新

2.4、CacheStats 监控统计 API

方法名 说明
hitRate() 缓存命中率(hitCount / requestCount)
hitCount() 缓存命中总次数
missCount() 未命中总次数
missRate() 未命中率(missCount / requestCount)
loadSuccessCount() 成功加载新值的次数
loadExceptionCount() 加载新值时抛出异常的次数
totalLoadTime() 加载新值累计耗时(单位:纳秒)
averageLoadPenalty() 平均单次加载耗时(单位:纳秒)
evictionCount() 因容量限制或过期等被驱逐/淘汰的条目总数

3、使用示例

3.1、LoadingCache 自动加载示例

这是最常用的模式。当调用 get(key) 时,如果缓存中存在,则直接返回;如果不存在,则会自动调用 CacheLoader.load(key) 方法获取数据,存入缓存后再返回。

java 复制代码
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class LoadingCacheExample {
    public static void main(String[] args) throws ExecutionException {
        // 创建一个 LoadingCache
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                // 设置最大并发级别
                .concurrencyLevel(8)
                // 设置缓存最大容量
                .maximumSize(100)
                // 设置写入后过期时间
                .expireAfterWrite(10, TimeUnit.MINUTES)
                // 构建缓存并指定 CacheLoader
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) throws Exception {
                        // 这里模拟从数据库或外部接口加载数据
                        System.out.println("缓存未命中,正在加载数据: " + key);
                        return "Value_" + key;
                    }
                });

        // 第一次获取,会触发 load 方法
        System.out.println(cache.get("Key1")); // 输出: 缓存未命中... -> Value_Key1
        
        // 第二次获取,直接从缓存中取
        System.out.println(cache.get("Key1")); // 输出: Value_Key1
    }
}
java 复制代码
缓存未命中,正在加载数据: Key1
Value_Key1
Value_Key1

3.2、Cache 手动加载示例

如果在某些场景下,你不想全局指定一种加载方式,可以使用普通的 Cache。你可以直接 put 数据,或者使用带有 Callable 的 get 方法实现"如果缓存中有则返回,没有则计算、存入缓存并返回"的逻辑。

java 复制代码
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

public class CacheExample {
    public static void main(String[] args) throws ExecutionException {
        Cache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(100)
                .build();

        // 1. 手动存入缓存
        cache.put("Key1", "Value_Key1");
        System.out.println(cache.getIfPresent("Key1")); // 输出: Value_Key1

        // 2. 动态加载 (Get-if-Absent-Compute)
        String value = cache.get("Key2", new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println("缓存未命中,执行 Callable 计算数据...");
                return "Value_Key2";
            }
        });
        
        System.out.println(value); // 输出: Value_Key2
    }
}
java 复制代码
Value_Key1
缓存未命中,执行 Callable 计算数据...
Value_Key2

4、淘汰策略

4.1、基于容量淘汰

  • maximumSize(long):限制缓存的最大项数。当即将达到限制时,会根据 LRU(最近最少使用)算法淘汰条目。
  • maximumWeight(long):配合 Weigher 接口使用,限制缓存的总权重(例如按字节大小计算缓存容量)。
java 复制代码
CacheBuilder.newBuilder()
    .maximumWeight(100000)
    .weigher((String key, String value) -> value.length())
    .build();

4.2、基于时间淘汰

  • expireAfterWrite(long, TimeUnit):缓存项在创建或最后一次更新后的指定时间后过期。
  • expireAfterAccess(long, TimeUnit):缓存项在最后一次读取或写入后的指定时间后过期(常用于 Session 类似场景)。

4.3、基于引用淘汰(结合垃圾回收)

  • weakKeys():使用弱引用存储键。如果没有其他强引用指向该键,该条目可能会被垃圾回收。
  • weakValues():使用弱引用存储值。
  • softValues():使用软引用存储值。当内存不足时,软引用指向的对象会被垃圾回收。

5、移除监听器(Removal Listener)

你可以配置一个监听器,在缓存项被移除(不论是由于过期、手动清除还是容量满了被淘汰)时执行特定操作(例如释放资源、记录日志)。

java 复制代码
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;

CacheBuilder.newBuilder()
    .maximumSize(10)
    .removalListener(new RemovalListener<String, String>() {
        @Override
        public void onRemoval(RemovalNotification<String, String> notification) {
            System.out.println("缓存被移除 - 键: " + notification.getKey() 
                    + ", 值: " + notification.getValue() 
                    + ", 原因: " + notification.getCause());
        }
    })
    .build();

6、手动清除缓存

你也可以随时手动干预缓存的数据:

  • cache.invalidate(key):清除指定 key 的缓存。
  • cache.invalidateAll(keys):批量清除指定 keys 的缓存。
  • cache.invalidateAll():清空所有缓存。

7、缓存统计(Statistics)

Guava 提供了强大的统计功能,便于监控缓存的命中率和性能。在构建缓存时需要调用 recordStats()开启。

java 复制代码
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheStats;

public class CacheStatsExample {
    public static void main(String[] args) {
        Cache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(100)
                .recordStats() // 开启统计功能
                .build();

        cache.put("A", "1");
        cache.getIfPresent("A"); // 命中
        cache.getIfPresent("B"); // 未命中

        // 获取统计信息
        CacheStats stats = cache.stats();
        System.out.println("缓存命中率: " + stats.hitRate());
        System.out.println("未命中次数: " + stats.missCount());
        System.out.println("加载新值花费的平均时间: " + stats.averageLoadPenalty());
    }
}
java 复制代码
缓存命中率: 0.5
未命中次数: 1
加载新值花费的平均时间: 0.0
相关推荐
接着奏乐接着舞。14 小时前
【2026】73道Redis 常见面试题与参考答案
数据库·redis·后端·缓存
Seraphina361 天前
记一次实验:利用缓存服务器规范化技术进行网页缓存欺骗
经验分享·笔记·网络安全·缓存
吴声子夜歌1 天前
Guava——概述
guava
草莓熊Lotso2 天前
【Redis 初阶】Set 类型深度解析:去重集合的运算能力与实战场景
linux·网络·数据库·windows·redis·tcp/ip·缓存
云存储小天使3 天前
训练效率提升50%以上:GooseFS 写缓存及其在具身智能数据处理中的应用
缓存·腾讯云·对象存储·goosefs
程序员夏洛3 天前
Redis 数据过期后的删除策略是什么?
数据库·redis·缓存
鱼鳞_3 天前
热门八股-Redis
数据库·redis·缓存
hweiyu003 天前
Redis命令:MIGRATE
redis·缓存
A.说学逗唱的Coke3 天前
【人工智能专题】Redis 杀进 AI 数据层:向量搜索、语义缓存与 Agent 记忆工程从入门到踩坑
人工智能·redis·缓存