Java内存缓存神器:Caffeine(咖啡因)
文章目录
一、Caffeine简介
官网:https://github.com/ben-manes/caffeine/wiki/Home-zh-CN
Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库。
缓存和ConcurrentMap有点相似,但还是有所区别。最根本的区别是ConcurrentMap将会持有所有加入到缓存当中的元素,直到它们被从缓存当中手动移除。但是,Caffeine的缓存Cache 通常会被配置成自动驱逐缓存中元素,以限制其内存占用。在某些场景下,LoadingCache和AsyncLoadingCache 因为其自动加载缓存的能力将会变得非常实用。
Caffeine提供了灵活的构造器去创建一个拥有下列特性的缓存:
自动加载元素到缓存当中,异步加载的方式也可供选择
当达到最大容量的时候可以使用基于就近度和频率的算法进行基于容量的驱逐
将根据缓存中的元素上一次访问或者被修改的时间进行基于过期时间的驱逐
当向缓存中一个已经过时的元素进行访问的时候将会进行异步刷新
key将自动被弱引用所封装
value将自动被弱引用或者软引用所封装
驱逐(或移除)缓存中的元素时将会进行通知
写入传播到一个外部数据源当中
持续计算缓存的访问统计指标
为了提高集成度,扩展模块提供了JSR-107 JCache和Guava适配器。JSR-107规范了基于Java 6的API,在牺牲了功能和性能的代价下使代码更加规范。Guava的Cache是Caffeine的原型库并且Caffeine提供了适配器以供简单的迁移策略。
要使用Caffeine,首先要引入maven坐标:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.2</version>
</dependency>
二、缓存加载
1、手动加载
Cache
接口提供了显式搜索查找、更新和移除缓存元素的能力。
推荐使用 cache.get(key, k -> value)
操作来在缓存中不存在该key对应的缓存元素的时候进行计算生成并直接写入至缓存内,而当该key对应的缓存元素存在的时候将会直接返回存在的缓存值。
一次 cache.put(key, value)
操作将会直接写入或者更新缓存里的缓存元素,在缓存中已经存在的该key对应缓存值都会直接被覆盖。
// 定义一个缓存, 过期时间10分钟,最大缓存数1w
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.build();
// 查找一个缓存元素, 没有查找到的时候返回null
String value = cache.getIfPresent("key");
System.out.println(value); // null
// 查找缓存,如果缓存不存在则生成缓存元素 并且塞入缓存, 如果无法生成则返回null
value = cache.get("key", k -> "new value");
System.out.println(value); // new value
System.out.println(cache.getIfPresent("key")); // new value
// 添加或者更新一个缓存元素
cache.put("key", "new value2");
System.out.println(cache.getIfPresent("key")); // new value2
// 移除一个缓存元素
cache.invalidate("key");
System.out.println(cache.getIfPresent("key")); // null
2、自动加载
一个LoadingCache
是一个Cache
附加上 CacheLoader
能力之后的缓存实现。
在build中,传入一个方法,该方法的参数就是key。如果调用get方法,key不存在时,会调用该方法生成该key的数据。
public static void main(String[] args) throws InterruptedException {
LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createData(key));
// 查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回null
String value = cache.get("key");
System.out.println(value);
// 批量查找缓存,如果缓存不存在则生成缓存元素
Map<String, String> values = cache.getAll(Stream.of("key1", "key2").collect(Collectors.toList()));
System.out.println(values);
}
private static String createData(String key) {
System.out.println("createData + " + key);
return key + "value";
}
3、手动异步加载(需要额外的包)
一个AsyncCache 是 Cache 的一个变体,AsyncCache提供了在 Executor上生成缓存元素并返回 CompletableFuture的能力。这给出了在当前流行的响应式编程模型中利用缓存的能力。
synchronous()方法给 Cache提供了阻塞直到异步缓存生成完毕的能力。
当然,也可以使用 AsyncCache.asMap()所暴露出来的ConcurrentMap的方法对缓存进行操作。
默认的线程池实现是 ForkJoinPool.commonPool() ,当然你也可以通过覆盖并实现 Caffeine.executor(Executor)方法来自定义你的线程池选择。
AsyncCache<Key, Graph> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.buildAsync();
// 查找一个缓存元素, 没有查找到的时候返回null
CompletableFuture<Graph> graph = cache.getIfPresent(key);
// 查找缓存元素,如果不存在,则异步生成
graph = cache.get(key, k -> createExpensiveGraph(key));
// 添加或者更新一个缓存元素
cache.put(key, graph);
// 移除一个缓存元素
cache.synchronous().invalidate(key);
4、自动异步加载
一个 AsyncLoadingCache是一个 AsyncCache 加上 AsyncCacheLoader能力的实现。
在需要同步的方式去生成缓存元素的时候,CacheLoader是合适的选择。而在异步生成缓存的场景下, AsyncCacheLoader则是更合适的选择并且它会返回一个 CompletableFuture
。
AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
// 你可以选择: 去异步的封装一段同步操作来生成缓存元素
.buildAsync(key -> createData(key));
// 你也可以选择: 构建一个异步缓存元素操作并返回一个future
//.buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
// 查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<String> value = cache.get("key");
// 批量查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<Map<String, String>> graphs = cache.getAll(keys);
三、缓存清理
1、基于容量
LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10) // 基于容量,超过10个会基于最近最常使用算法 进行缓存清理
.build(key -> createData(key));
2、基于时间
// 基于固定的过期时间驱逐策略
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
// 基于不同的过期驱逐策略
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfter(new Expiry<Key, Graph>() {
public long expireAfterCreate(Key key, Graph graph, long currentTime) {
// Use wall clock time, rather than nanotime, if from an external resource
long seconds = graph.creationDate().plusHours(5)
.minus(System.currentTimeMillis(), MILLIS)
.toEpochSecond();
return TimeUnit.SECONDS.toNanos(seconds);
}
public long expireAfterUpdate(Key key, Graph graph,
long currentTime, long currentDuration) {
return currentDuration;
}
public long expireAfterRead(Key key, Graph graph,
long currentTime, long currentDuration) {
return currentDuration;
}
})
.build(key -> createExpensiveGraph(key));
Caffeine提供了三种方法进行基于时间的驱逐:
expireAfterAccess(long, TimeUnit)
: 一个元素在上一次读写操作后一段时间之后,在指定的时间后没有被再次访问将会被认定为过期项。在当被缓存的元素时被绑定在一个session上时,当session因为不活跃而使元素过期的情况下,这是理想的选择。expireAfterWrite(long, TimeUnit)
: 一个元素将会在其创建或者最近一次被更新之后的一段时间后被认定为过期项。在对被缓存的元素的时效性存在要求的场景下,这是理想的选择。expireAfter(Expiry)
: 一个元素将会在指定的时间后被认定为过期项。当被缓存的元素过期时间收到外部资源影响的时候,这是理想的选择。
3、基于引用
// 当key和缓存元素都不再存在其他强引用的时候驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> createExpensiveGraph(key));
// 当进行GC的时候进行驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.softValues()
.build(key -> createExpensiveGraph(key));
Caffeine 允许你配置你的缓存去让GC去帮助清理缓存当中的元素,其中key支持弱引用,而value则支持弱引用和软引用。记住 AsyncCache不支持软引用和弱引用。
Caffeine.weakKeys()
在保存key的时候将会进行弱引用。这允许在GC的过程中,当key没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行key之间的比较。
Caffeine.weakValues()
在保存value的时候将会使用弱引用。这允许在GC的过程中,当value没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行value之间的比较。
Caffeine.softValues()
在保存value的时候将会使用软引用。为了相应内存的需要,在GC过程中被软引用的对象将会被通过LRU算法回收。由于使用软引用可能会影响整体性能,我们还是建议通过使用基于缓存容量的驱逐策略代替软引用的使用。同样的,使用 softValues() 将会通过引用相等(==)而不是对象相等 equals()去进行value之间的比较。
四、缓存移出
1、手动移出
// 失效key
cache.invalidate(key)
// 批量失效key
cache.invalidateAll(keys)
// 失效所有的key
cache.invalidateAll()
2、移出监听器
可以为你的缓存通过Caffeine.removalListener(RemovalListener)
方法定义一个移除监听器在一个元素被移除的时候进行相应的操作。这些操作是使用 Executor 异步执行的,其中默认的 Executor 实现是 ForkJoinPool.commonPool()
并且可以通过覆盖Caffeine.executor(Executor)
方法自定义线程池的实现。
当移除之后的自定义操作必须要同步执行的时候,你需要使用 Caffeine.evictionListener(RemovalListener)
。这个监听器将在 RemovalCause.wasEvicted()
为 true 的时候被触发。为了移除操作能够明确生效, Cache.asMap()
提供了方法来执行原子操作。
Cache<Key, Graph> graphs = Caffeine.newBuilder()
.evictionListener((Key key, Graph graph, RemovalCause cause) ->
System.out.printf("Key %s was evicted (%s)%n", key, cause))
.removalListener((Key key, Graph graph, RemovalCause cause) ->
System.out.printf("Key %s was removed (%s)%n", key, cause))
.build();
五、刷新缓存
异步刷新缓存,重新加载。
LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10)
.expireAfterWrite(10, TimeUnit.SECONDS)
.build(key -> createData(key));
// 刷新
cache.refresh("key");