> 文章列表 > 本地缓存解决方案GuavaCache | Spring Cloud 37

本地缓存解决方案GuavaCache | Spring Cloud 37

本地缓存解决方案GuavaCache | Spring Cloud 37

一、前言

Guava是一个谷歌开源Java工具库,提供了一些非常实用的工具。当业务实现上需要用到本地缓存,来解决一些数据量相对较小但是频繁访问数据的场景,可以采用GuavaCacheBuilder解决方案。

Guava Cache是一个全内存本地缓存。拥有以下优点:

  • 线程安全的缓存,与ConcurrentMap相似(前者更"好"),在高并发情况下,能够正常缓存更新以及返回
  • 提供了三种基本的缓存回收方式: 基于容量回收、定时回收和基于引用回收策略
  • 提供了两种定时回收:按照写入时间,最早写入的最先回收;按照访问时间,最早访问的最早回收
  • 支持缓存更新及缓存移除监听
  • 可以监控缓存加载/命中情况
  • 使用方便、简单

二、使用示例

2.1 最基础的例子[CacheBuilder]

// 新建CacheBuilder
Cache<Integer, String> cache = CacheBuilder.newBuilder().build();
cache.put(1, "a");
cache.put(2, "b");
System.out.println(cache.getIfPresent(1));  // 输出: a
System.out.println(cache.getIfPresent(3));  // 输出: null
System.out.println(cache.getAllPresent(new ArrayList<Integer>(){{add(1);add(2);
}}));  // 输出: {1=a, 2=b}
  • getIfPresent(key):从现有的缓存中获取,如果缓存中有key,则返回value,如果没有则返回null
  • getAll(Iterable<? extends K>):用来执行批量查询。默认情况下,对每个不在缓存中的keygetAll方法会单独调用CacheLoader.load来加载缓存项。

2.2 若无缓存时,自定义缓存值[CacheLoader、get()]

// 遇到不存在的key,定义默认缓存值
// 1. 在cache定义时设置通用缓存模版
LoadingCache<Integer, String> cache1 = CacheBuilder.newBuilder().build(new CacheLoader<Integer, String>() {@Overridepublic String load(Integer key) throws Exception {return "hellokey" + key;}}
);
cache1.put(1, "a");
System.out.println(cache1.getIfPresent(1));  // 输出: a
try {System.out.println(cache1.getAll(new ArrayList<Integer>(){{  // getAll()将没有命中的key调用load()方法去加载数据add(1);add(2);}}));  // 输出: {1=a, 2=hellokey2}System.out.println(cache1.get(3));  // 输出: hellokey3
} catch (ExecutionException e) {e.printStackTrace();
}// 2. 在获取缓存值时设置缓存
Cache<Integer, String> cache2 = CacheBuilder.newBuilder().build();
cache2.put(1, "a");
System.out.println(cache2.getIfPresent(1));  // 输出: a
try {String value = cache2.get(2, () -> "hellokey2");System.out.println(value);  // 输出: hellokey2
} catch (ExecutionException e) {e.printStackTrace();
}

2.3 设置缓存的并发级别

CacheBuilder.newBuilder()// 设置并发级别为cpu核心数,默认为4.concurrencyLevel(Runtime.getRuntime().availableProcessors()) .build();

在通常情况下,推荐将并发级别设置为服务器CPU核心数。

2.4 设置缓存的初始容量

在构建缓存时可以为缓存设置一个合理大小初始容量,由于Guava的缓存使用了分离锁的机制,扩容的代价非常昂贵。所以合理的初始容量能够减少缓存容器的扩容次数。

CacheBuilder.newBuilder()// 设置初始容量为100.initialCapacity(100).build();

2.5 设置缓存的最大存储[.maximumSize()、.maximumWeight()]

// ps. .maximumSize(long),.maximumWeight(long)互斥,build()只可以二选一
// 1. 基于缓存多少
Cache<Integer, String> cache1 = CacheBuilder.newBuilder().maximumSize(2L)  // 设置缓存上限,最多两个.build();
cache1.put(1, "a");
cache1.put(2, "b");
cache1.put(3, "c");
System.out.println(cache1.asMap());  // 输出: {3=c, 2=b}
System.out.println(cache1.getIfPresent(2));  // 输出: b
cache1.put(4, "d");
System.out.println(cache1.asMap());  // 输出: {2=b, 4=d}// 2. 基于缓存大小
Cache<Integer, Integer> cache2 = CacheBuilder.newBuilder().maximumWeight(100L)  // 指定最大总重.weigher((Weigher<Integer, Integer>) (key, value) -> {if (value % 2 == 0) {return 20;  // 偶数,则权重为20} else {return 5;  // 非偶数,则权重为5}})  // 设置权重函数.build();
for (int i = 0; i <= 20; i += 2) {cache2.put(i, i);
}
System.out.println(cache2.asMap());  // 输出: {20=20, 18=18, 16=16, 14=14}
cache2.invalidateAll();  // 清除所有的缓存
for (int i = 0; i <= 20; i += 1) {cache2.put(i, i);
}
System.out.println(cache2.asMap());  // 输出: {20=20, 19=19, 18=18, 17=17, 16=16, 14=14, 11=11}

2.5.1 基于容量的清除策略

通过CacheBuilder.maximumSize(long)方法可以设置Cache的最大容量数,当缓存数量达到或接近该最大值时,Cache将清除掉那些最近最少使用的缓存。

2.5.2 基于权重的清除策略

使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重。

如每一项缓存所占据的内存空间大小都不一样,可以看作它们有不同的“权重”(weights),作为执行清除策略时优化回收的对象

2.6 控制缓存回收的时间[.expireAfterWrite()、.expireAfterAccess()]

  • expireAfterAccess 读写缓存后多久过期

  • expireAfterWrite 写缓存后多久过期

    并发时的特性:(无论缓存中是否存在数据)如果当其他线程在加载数据,当前线程会一直阻塞等待其它线程加载完成数据。

// 1. 设置缓存写入后多久过期
Cache<Integer, Integer> cache1 = CacheBuilder.newBuilder().expireAfterWrite(2, TimeUnit.SECONDS)  // 缓存写入2s后过期.build();
cache1.put(1,1);
System.out.println(cache1.asMap());  // 输出: {1=1}
try {Thread.sleep(3000);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println(cache1.asMap());  // 输出: {}// 2. 设置缓存读取后多久过期
Cache<Integer, Integer> cache2 = CacheBuilder.newBuilder().expireAfterAccess(2, TimeUnit.SECONDS)  // 缓存读取2s后过期.build();
cache2.put(1,1);
try {Thread.sleep(3000);
} catch (InterruptedException e) {e.printStackTrace();
}
cache2.getIfPresent(1);
System.out.println(cache2.asMap());  // 输出: {1=1}
try {Thread.sleep(3000);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println(cache2.asMap());  // 输出: {}

2.7 缓存更新[.refreshAfterWrite()]

使用refreshAfterWrites后,需要实现CacheLoaderreload方法。需要在方法中创建一个ListenableFutureTask,然后将这个task提交给线程池去异步执行。

这样的话,缓存失效后重新加载就变成了异步,加载期间尝试获取取缓存的线程也不会被阻塞。而是获取到加载之前的值。加载完毕之后,各个线程就能取到最新的值。

ExecutorService executorService = Executors.newFixedThreadPool(5);
// 设置更新时间, 定时去更新缓存中的数据
LoadingCache<String, String> cache = CacheBuilder.newBuilder().refreshAfterWrite(2, TimeUnit.SECONDS)  // 缓存写入2s后更新.build(new CacheLoader<String, String>() {@Overridepublic String load(String key) throws Exception {return UUID.randomUUID().toString();  // 假设是个查库操作之类的..}// 异步加载缓存@Overridepublic ListenableFuture<String> reload(String key, String oldValue) throws Exception {//定义任务。ListenableFutureTask<String> futureTask = ListenableFutureTask.create(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}return "曹操";});//异步执行任务executorService.execute(futureTask);return futureTask;}});
cache.put("1", "1");
System.out.println(cache.getIfPresent("1"));  // 输出: 1//睡眠3s后,再次获取,此时缓存失效,异步的加载缓存。但是线程是立即返回“旧结果”。
try {Thread.sleep(3000);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println(cache.getIfPresent("1"));  // 输出: 1try {Thread.sleep(2000);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println(cache.getIfPresent("1"));  // 输出: 曹操

refreshAfterWrites异步去刷新缓存的方法,使用过期的旧值快速响应。
expireAfterWrites缓存失效后线程需要同步等待加载结果,可能会造成请求大量堆积的问题。

2.8 手动清除缓存[.invalidate()、.invalidateAll()]

// 清除缓存中的数据
Cache<Integer, String> cache = CacheBuilder.newBuilder().build();
cache.put(1, "a");
System.out.println(cache.asMap());  // 输出: {1=a}
cache.invalidateAll();  // 清除所有缓存
System.out.println(cache.asMap());  // 输出: {}
cache.put(2, "b");
System.out.println(cache.asMap());  // 输出: {2=b}
cache.invalidate(2);  // 清除指定缓存
System.out.println(cache.asMap());  // 输出: {}
cache.put(1, "a");
cache.put(2, "b");
cache.put(3, "c");
System.out.println(cache.asMap());  // 输出: {2=b, 1=a, 3=c}
cache.invalidateAll(new ArrayList<Integer>() {{  // 批量清除缓存add(1);add(2);
}});
System.out.println(cache.asMap());  // 输出: {3=c}

2.9 设置监听器[.removalListener()]

// 设置移除监听器(ps. 当移除缓存时,会打印出被移除缓存的信息(基于模版格式))
LoadingCache<Integer, Integer> cache = CacheBuilder.newBuilder().expireAfterWrite(2, TimeUnit.SECONDS)  // 设置2s后过期时间.removalListener(notification -> System.out.println("remove key[" + notification.getKey()+ "],value[" + notification.getValue()+ "],remove reason[" + notification.getCause() + "]"))  // 设置移除监听器,并设置输出模版.build(new CacheLoader<Integer, Integer>() {@Overridepublic Integer load(Integer key) throws Exception {return 2;  // 当无值时, 设置默认值}});
cache.put(1, 1);
cache.put(2, 2);
System.out.println(cache.asMap());  // 输出: {2=2, 1=1}
cache.invalidateAll();
System.out.println(cache.asMap());  // 输出: {}
cache.put(3, 3);
try {// ps. 如果定义的CacheLoader没有声明任何检查型异常,则可以通过getUnchecked()取值System.out.println(cache.getUnchecked(3));  // 输出: 3Thread.sleep(3000);System.out.println(cache.getUnchecked(3));  // 输出: 2
} catch (InterruptedException e) {e.printStackTrace();
}

2.10 自带的统计功能[.recordStats()]

// 开启统计,并查看统计信息
LoadingCache<String, String> cache = CacheBuilder.newBuilder().recordStats()  // 开启统计功能.refreshAfterWrite(2, TimeUnit.SECONDS)  // 缓存写入2s后更新.build(new CacheLoader<String, String>() {@Overridepublic String load(String key) throws Exception {return UUID.randomUUID().toString();  // 假设是个查库操作之类的..}});
cache.put("1", "a");
System.out.println(cache.asMap());  // 输出: {1=a}
System.out.println(cache.stats());  // 输出: CacheStats{hitCount=0, missCount=0, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=0}
cache.getIfPresent("2");
System.out.println(cache.asMap());  // 输出: {1=a}
System.out.println(cache.stats());  // 输出: CacheStats{hitCount=0, missCount=1, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=0}
try {Thread.sleep(3000);
} catch (InterruptedException e) {e.printStackTrace();
}
cache.getIfPresent("1");
System.out.println(cache.asMap());  // 输出: {1=0207bb01-7b3c-4b66-b575-9fb2c5511a96}
System.out.println(cache.stats());  // 输出: CacheStats{hitCount=1, missCount=1, loadSuccessCount=1, loadExceptionCount=0, totalLoadTime=21118733, evictionCount=0}
/* hitCount;  // 缓存命中数* missCount; // 缓存未命中数* loadSuccessCount; // load成功数* loadExceptionCount; // load异常数* totalLoadTime; // load的总共耗时* evictionCount; // 缓存项被回收的总数,不包括显式清除*/

2.11 显示缓存中的数据[.asMap()]

// asMap视图
Cache<Integer, String> cache = CacheBuilder.newBuilder().build();
cache.put(1, "a");
cache.put(2, "b");
cache.put(3, "c");
cache.asMap();  // 返回的是个ConcurrentMap
System.out.println(cache.asMap().containsKey(1));  // 输出: true
System.out.println(cache.asMap().containsValue("b"));  // 输出: true
System.out.println(cache.asMap().get(1));  // 输出: a
System.out.println(cache.asMap().put(5, "e"));  // 输出: null
System.out.println(cache.asMap().entrySet());  // 输出: [5=e, 2=b, 1=a, 3=c]
System.out.println(cache.asMap());  // 输出: {5=e, 2=b, 1=a, 3=c}

2.12 基于引用的回收策略

在构建Cache实例过程中,通过设置使用弱引用的键、或弱引用的值、或软引用的值,从而使JVMGC时顺带实现缓存的清除。

  • CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收
  • CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收
  • CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定

垃圾回收仅依赖==恒等式,使用弱引用键的缓存用而不是equals(),即同一对象引用。

// 最大3个值的弱引用
Cache<Object, Object> cache = CacheBuilder.newBuilder().maximumSize(3).weakValues().build();
Object value = new Object();
cache.put("1", value);
System.out.println(cache.getIfPresent("1"));//java.lang.Object@6438a396
value = new Object();// 原对象不再有强引用
// 强制垃圾回收
System.gc();
System.out.println(cache.getIfPresent("1"));//null