个人使用场景: 做三级缓存
今天主要说的是Caffeine
,中文名就是咖啡因,一种容易让人精神亢奋的物质。它可以说是Guava的重写,但是效率却非常的高,青出于蓝而胜于蓝。
1.Guava (JDK 集成了,无需导入)
2.Caffeine
<!-- https://mvnrepository.com/artifact/com.google.guava/guava --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>22.0</version> </dependency> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>3.0.5</version> </dependency>
Guava
1.本地缓存 LUR
private static Cache<String, String> cache = CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.SECONDS).build(); cache.put("1","213"); Thread.sleep(1000); cache.put("4","213"); Thread.sleep(9000); System.err.println(cache.getIfPresent("1")); //null System.err.println(cache.getIfPresent("4")); //213
2. LUR本地缓存实现,支持多种缓存过期策略(LRU)
//初始化 Guava内存 LRU算法 private static LoadingCache<String, String> localCache = CacheBuilder.newBuilder() .initialCapacity(1000) .maximumSize(10000) .expireAfterAccess(12, TimeUnit.HOURS) .build(new CacheLoader<String, String>() { //默认的数据加载实现,当调用get取值的时候,如果key没有对应的值,就调用这个方法进行加载 @Override public String load(String s) throws Exception { return "null"; } }); public static void setKey(String key, String value) { localCache.put(key, value); } public static String getKey(String key) { String value = null; try { value = localCache.get(key); if ("null".equals(value)) { return null; } } catch (ExecutionException e) { log.error("localCache get error", e); } return null; }
Caffeine: 原文:https://zhuanlan.zhihu.com/p/329684099
缓存类型
Caffeine 的缓存一共有四种使用姿势
Cache 手动加载
Cache<Key, MyObject> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.build();
// 查找一个缓存元素, 没有查找到的时候返回null
MyObject graph = cache.getIfPresent(key);
// 查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回null
graph = cache.get(key, k -> createObject(key));
// 添加或者更新一个缓存元素
cache.put(key, graph);
// 移除一个缓存元素
cache.invalidate(key);
值得注意的是,当缓存的元素无法生成或者在生成的过程中抛出异常而导致生成元素失败,cache.get
可能会返回 null
也可以使用 Cache.asMap()
所暴露出来的 ConcurrentMap 的方法直接对缓存进行操作
LoadingCache 自动加载
LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
// 查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回null
Graph graph = cache.get(key);
// 批量查找缓存,如果缓存不存在则生成缓存元素
Map<Key, Graph> graphs = cache.getAll(keys);
AsyncCache 手动异步加载
AsyncCache<Key, Graph> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.buildAsync();
// 查找缓存元素,如果不存在,则异步生成
CompletableFuture<Graph> graph = cache.get(key, k -> createExpensiveGraph(key));
AsyncLoadingCache 自动异步加载
AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
// 你可以选择: 去异步的封装一段同步操作来生成缓存元素
.buildAsync(key -> createExpensiveGraph(key));
// 你也可以选择: 构建一个异步缓存元素操作并返回一个future
.buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
// 查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<Graph> graph = cache.get(key);
// 批量查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);
可用参数
我们以最简单的 Caffeine.newBuilder()
为例:
maximumSize
使用姿势:
Cache<Key, MyObject> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.build();
缓存会通过 Window TinyLfu 算法控制整个缓存大小。关于这个算法我们在下文的原理中细讲。
maximumWeight 和 weigher
使用姿势:
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumWeight(10_000)
.weigher((Key key, Graph graph) -> graph.vertices().size())
.build(key -> createExpensiveGraph(key));
这种方式控制的是总权重。需要 weigher 提供为每个 entry 计算权重的方式。当我们的缓存大小不均匀时,我们可以通过这种方式控制总大小。权重计算是在其创建或更新时发生的,此后其权重值都是静态存在的。
expireAfterAccess、expireAfterWrite、expireAfter
// 基于固定的过期时间驱逐策略
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));
expireAfterAccess 表示上次读写超过一定时间后过期,expireAfterWrite 表示上次创建或更新超过一定时间后过期。expireAfter 允许复杂的表达式,过期时间可以通过 entry 等外部参数确定。
至于过期淘汰的发生,是在写操作以及偶尔发生在读操作中的。过期事件的调度和触发将会在 O(1)的时间复杂度内完成。如果希望过期发生的更及时,可以通过在你的 Cache 构造器中通过 Scheduler 接口和 Caffeine.scheduler(Scheduler) 方法去指定一个调度线程代替在缓存活动中去对过期事件进行调度。
具体地说,在默认情况下,当一个缓存元素过期的时候,Caffeine 不会自动立即将其清理和驱逐。而它将会在写操作之后进行少量的维护工作,在写操作较少的情况下,也偶尔会在读操作之后进行。如果你的缓存吞吐量较高,那么你不用去担心你的缓存的过期维护问题。但是如果你的缓存读写操作都很少,可以额外通过一个线程使用 Cache.cleanUp() 方法在合适的时候触发清理操作。
weakKeys、weakValues 和 softValues
// 当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));
key 支持弱引用,而 value 则支持弱引用和软引用。需要注意的是,AsyncCache 不支持软引用和弱引用。
另外需要注意的是,使用了 weakKeys 后,缓存 key 之间的比较将会通过引用相等(==) 而不是对象相等 equals() 去进行。原因是 GC 只依赖于引用相等性。(否则的话,万一 A 与 B 是 equals 的,A 回收但 B 没回收,如果是以 A 为 key 进的缓存,缓存会被回收,用户拿着 B 却无法再次 get(B))。
同理,使用了 weakValues 或 softValues 后,value 之间的比较也会通过引用相等(==) 而不是对象相等 equals()。
removalListener
Cache<Key, Graph> graphs = Caffeine.newBuilder()
.removalListener((Key key, Graph graph, RemovalCause cause) ->
System.out.printf("Key %s was removed (%s)%n", key, cause))
.build();
removal(移除)包括 eviction(驱逐:由于策略自动移除)和 invalidation(失效:手动移除)
removalListener 的操作将会异步执行在一个 Executor 上。默认的线程池实现是 ForkJoinPool.commonPool()。当然也可以通过覆盖 Caffeine.executor(Executor) 方法自定义线程池的实现。这个 Executor 同时负责 refresh 等操作。
如果希望这个操作是同步的,可以通过下文的 writer() 方法实现。
refreshAfterWrite
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
这个参数是 LoadingCache 和 AsyncLoadingCache 的才会有的。在刷新的时候如果查询缓存元素,其旧值将仍被返回,直到该元素的刷新完毕后结束后才会返回刷新后的新值。refreshAfterWrite 将会使在写操作之后的一段时间后允许 key 对应的缓存元素进行刷新,但是只有在这个 key 被真正查询到的时候才会正式进行刷新操作。
在刷新的过程中,如果抛出任何异常,会保留旧值。异常会被 logger 打印,然后被吞掉。
此外,CacheLoader 还支持通过覆盖重写 CacheLoader.reload(K, V) 方法使得在刷新中可以将旧值也参与到更新的过程中去。
refresh 的操作将会异步执行在一个 Executor 上。默认的线程池实现是 ForkJoinPool.commonPool()。当然也可以通过覆盖 Caffeine.executor(Executor) 方法自定义线程池的实现。这个 Executor 同时负责 removalListener 的操作。
writer
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.writer(new CacheWriter<Key, Graph>() {
@Override public void write(Key key, Graph graph) {
// 持久化或者次级缓存
}
@Override public void delete(Key key, Graph graph, RemovalCause cause) {
// 从持久化或者次级缓存中删除
}
})
.build(key -> createExpensiveGraph(key));
CacheWriter 给缓存提供了充当底层资源的门面的能力,当其与 CacheLoader 一起使用的时候,所有的读和写操作都可以通过缓存向下传播。Writers 提供了原子性的操作,包括从外部资源同步的场景。这意味着在缓存中,当一个 key 的写入操作在完成之前,后续对这个 key 的其他写操作都是阻塞的,同时在这段时间内,尝试获取这个 key 对应的缓存元素的时候获取到的也将都是旧值。如果写入失败那么之前的旧值将会被保留同时异常将会被传播给调用者。
CacheWriter 将会在缓存元素被创建,更新或者移除的时候被触发。但是当一个映射被加载(比如 LoadingCache.get),重载 (比如 LoadingCache.refresh),或者生成 (比如 Map.computeIfPresent) 的时候将不会被触发。
需要注意的是,CacheWriter 将不能用在 weak keys 或者 AsyncLoadingCache 的场景。
CacheWriter 可以用来实现 write-through 和 write-back 两种模式的缓存。也可以用来整合多级缓存,或是用来作为发布同步监听器使用。
recordStats
Cache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.recordStats()
.build();
通过使用 Caffeine.recordStats()方法可以打开数据收集功能。Cache.stats()方法将会返回一个 CacheStats 对象,其将会含有一些统计指标,比如:
- hitRate(): 查询缓存的命中率
- evictionCount(): 被驱逐的缓存数量
- averageLoadPenalty(): 新值被载入的平均耗时
这些缓存统计指标可以被基于 push/pull 模式的报告系统进行集成。基于 pull 模式的系统可以通过调用 Cache.stats() 方法获取当前缓存最新的统计快照。一个基于 push 的系统可以通过自定义一个 StatsCounter 对象达到在缓存操作发生时自动推送更新指标的目的。
from
CaffeineSpec spec = CaffeineSpec.parse(
"maximumWeight=1000, expireAfterWrite=10m, recordStats");
LoadingCache<Key, Graph> graphs = Caffeine.from(spec)
.weigher((Key key, Graph graph) -> graph.vertices().size())
.build(key -> createExpensiveGraph(key));
可以使用 CaffeineSpec 提供简单的字符格式配置,不是很常用