Skip to Content

LocalCache

LocalCache(Guava Cache)是一个本地缓存,他不会进行文件存储或跨服务器共享,其主要用于提升单个服务节点的性能。

LocalCache 提供了两个核心接口 CacheLoadingCache 来构建缓存。

手动管理缓存 Cache

Cache 接口是一种需要你手动管理缓存项。

当缓存的加载逻辑不固定,或者需要在特定时机才能确定缓存值时使用 Cache。

import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; public class CacheExample { public static void main(String[] args) throws Exception { // 一个需要手动管理缓存项的缓存 Cache<String, String> cache = CacheBuilder.newBuilder() // 设置缓存的最大容量 .maximumSize(100) .build(); // 手动添加缓存数据、获取缓存k1 cache.put("k1", "v1"); String result1 = cache.get("k1", () -> "回调不会执行,因为缓存已经存在"); // 多次获不存在的缓存k2 String result2 = cache.get("k2", () -> { String v = "v2"; System.out.println("数据不存在,从数据库加载..." + v); return v; }); String result3 = cache.get("k2", () -> "回调不会执行,因为缓存已经存在"); // 移除缓存k2、重新获取缓存k2 cache.invalidate("k2"); String result4 = cache.get("k2", () -> { String v = "new-v2"; System.out.println("数据不存在,从数据库加载..." + v); return v; }); } }

自动加载缓存 LoadingCache

LoadingCache 接口扩展了自动加载缓存项的能力。

在构建 LoadingCache 时,需要提供一个缓存加载器 CacheLoader 。

当键值的计算和获取方式是固定的、可预期时使用 LoadingCache。

import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; public class LoadingCacheExample { public static void main(String[] args) throws Exception { // 一个能自动加载缓存项的缓存,构建时提供一个缓存加载器 LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder() .maximumSize(1000) .build(CacheLoader.from(k -> { System.out.println("访问数据库获取值..."); return "value:" + k; })); // 第一次调用 get,缓存未命中,自动触发 CacheLoader.load() String value1 = loadingCache.get("user-101"); System.out.println(value1); // 第二次调用 get,缓存命中,直接返回结果 String value2 = loadingCache.get("user-101"); System.out.println(value2); } }

缓存策略

LocalCache 可以组合多种策略来精细化地控制缓存的行为。

基于容量

当缓存容量达到上限时,采用LRU算法移除缓存项。

  • maximumSize:最大存储数量。
  • maximumWeight:最大存储重量,需要提供一个称重器 weigher 用于计算缓存项的重量。

maximumSizemaximumWeight是互斥的策略无法同时使用。

import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.cache.Weigher; import java.util.Random; public class CacheRecycleCapacityExample { public static void main(String[] args) throws Exception { // 缓存项数量达到最大值时执行LRU淘汰缓存项 LoadingCache<Integer, String> cacheMaxNum = CacheBuilder.newBuilder() .maximumSize(5) .build(CacheLoader.from(k -> k + "")); // cacheMaxNum 中缓存项的变化 for (int i = 1; i <= 10; i++) { cacheMaxNum.get(i); System.out.println(cacheMaxNum.asMap().values()); } // 缓存项重量达到最大值时执行LRU淘汰缓存项(称重器:按照字符串长度计算重量) LoadingCache<Integer, String> cacheMaxWeight = CacheBuilder.newBuilder() .maximumWeight(10) .weigher((Weigher<Integer, String>) (k, v) -> v.length()) .build(CacheLoader.from(k -> k + "")); // cacheMaxWeight 中缓存项的变化 Random random = new Random(); for (int i = 1; i <= 10; i++) { int ri = random.nextInt((int) Math.pow(10,i)); cacheMaxWeight.get(ri); System.out.println(cacheMaxWeight.asMap().values()); } } }

基于时间

  • expireAfterWrite:基于写入时间的过期回收缓存项,阻塞获取新缓存值。
  • expireAfterAccess:基于访问时间的过期回收缓存项,阻塞获取新缓存值(读写重置时间)。
  • refreshAfterWrite:基于写入时间的阻塞刷新,在新的缓存值返回前使用旧值。
组合使用概括缺点
rw + ea【首选】为热数据自动保鲜,为冷数据自动清理。短暂返回旧数据
ew + ea保证数据强一致性,但牺牲了性能。性能抖动、会阻塞、雪崩风险
rw + ew用于有绝对生命周期的场景,如会话和令牌。雪崩风险、内存管理较弱

rw + ea 它能以最佳性能解决 90% 的缓存问题,只有需要强制安全或一致性时,才考虑后两种组合。

import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import java.util.concurrent.TimeUnit; public class CacheRecycleTimeExample { public static void main(String[] args) throws Exception { // 1. 当容量超过上限,采用LRU回收缓存 // 2. 缓存项写入后3秒刷新 // 3. 缓存项读写后5秒过期 LoadingCache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(1000) .refreshAfterWrite(3, TimeUnit.SECONDS) .expireAfterAccess(5, TimeUnit.SECONDS) .build(CacheLoader.from(k -> { System.out.println("获取缓存项"); return System.currentTimeMillis() + ""; })); // 频繁访问,能确保数据较新 for (int i = 0; i < 10; i++) { Thread.sleep(1000); String value = cache.get("k"); System.out.println(value); } // 访问频率骤减,能确保数据有效 for (int i = 0; i < 10; i++) { Thread.sleep(i * 1000); String value = cache.get("k"); System.out.println(value); } } }

基于键值引用

  • weakKeys:使用弱引用包装键,当程序中不存在该键的强引用了,键值将在下一次GC时清理。
  • weakValues:使用弱引用包装值,当程序中不存在该值的强引用了,键值将在下一次GC时清理。
  • softValues:使用软引用包装值,当内存紧张时GC会回收软引用值。

缓存中存放的是包装实际键值的弱引用或软引用,当发现实际键值被GC清理后才会回收缓存。

非必要不要使用引用策略管理缓存

  • 使用引用策略回收缓存,会大大增加缓存复杂度、难以预测、难以调试。
  • weakKeyssoftValues 是互斥的只能二选一使用。
  • weakKeys 对键判定依赖的是 == 而非 .equals,这是该策略的一大坑
  • 当键值为字面量或者常量时,引用策略会失效(常量池中不会被GC)。
  • 当存在其他激进的策略,引用策略会失效(例如:expireAfterAccess(1, TimeUnit.MILLISECOND))
最后更新于