一、介绍
1、概述
使用缓存的优点是可以减少直接访问数据库的压力。Caffeine是目前单机版缓存性能最高的,提供了最优的缓存命中率。用法和java中的map集合比较类似,底层使用一个ConcurrentHashMap来保存所有数据,可以理解为一个增强版的map集合,增强的功能有设置缓存过期时间,缓存数据驱逐,统计缓存数据等。
CaffeineCache是一个基于Java的高性能缓存库,它提供了内存缓存的功能,可以用于加速应用程序的数据访问。CaffeineCache在性能和灵活性方面都有很高的表现,并且易于使用。基于CaffeineCache的优秀特性,它也成为了Spring的默认缓存技术。
Spring Boot 1.x版本中的默认本地缓存是Guava Cache。在 Spring5 (SpringBoot 2.x)后,Spring 官方放弃了 Guava Cache 作为缓存机制,而是使用性能更优秀的 Caffeine 作为默认缓存组件,这对于Caffeine来说是一个很大的肯定。
2、比较
(1)Caffeine和redis:
共同点都是基于内存。其中,Caffeine是本地缓存,基于单个JVM,不能直接跨多台机器分布,如果程序停止,JVM停止,本地缓存数据会全部丢失,类似java中的map集合,相比Redis,Caffeine的性能更好。Redis是一个分布式缓存系统,独立部署,支持将数据持久化到磁盘上,因此可以在应用程序关闭后仍然保留数据。Redis支持分布式架构,可以配置成主从模式或者集群模式,从而提供更好的水平扩展性和高可用性。
简单的说,Caffeine只在当前应用程序生效,如果是多个节点,多个节点是隔离的,分布式也是隔离的;而redis分布式多个节点数据是共享的。
(2)Ehcache和Caffeine:Caffeine是一个较新的本地缓存框架,在内存管理和高并发访问方面通常比Ehcache更高效。
3、优点
1. 高性能:CaffeineCache使用了一些高效的数据结构和算法,以提供快速的缓存访问速度。它使用了堆外内存和快速的哈希表实现,以最大限度地减少内存和CPU的使用。
2. 内存管理:CaffeineCache提供了灵活的内存管理功能,可以根据不同的需求进行配置。它支持最大缓存大小、最大缓存条目数、缓存过期策略等选项,以便根据应用程序的具体需求进行调整。
3. 缓存策略:CaffeineCache支持多种缓存策略,包括基于时间的过期、基于大小的淘汰和基于引用的淘汰等。它还支持自定义的缓存策略,以便根据应用程序的特定需求进行优化。
4. 异步加载:CaffeineCache允许异步加载缓存项,以避免在缓存未命中时造成的延迟。它提供了异步加载的接口和回调机制,使得应用程序能够并发地加载和使用缓存项。
5. 监听器:CaffeineCache提供了缓存监听器,可以在缓存项被创建、更新或删除时触发相应的事件。这可以用于实现缓存的一致性和通知机制,以便应用程序能够及时响应缓存的变化。
4、使用场景
频繁访问,但是变动不频繁且对实时要求不高的数据,建议使用 localcache,例如字典类数据。
二、原理
1、淘汰策略
CaffeineCache提供了多种淘汰策略,可以根据应用程序的需求选择适合的策略。以下是一些常用的缓存策略:
1. 基于时间的过期(Time-based Expiration):根据缓存项的存活时间来进行过期策略。可以设置缓存项的过期时间,当缓存项超过指定的时间后,将会被自动移除。
2. 基于大小的淘汰(Size-based Eviction):根据缓存项的大小来进行淘汰策略。可以设置缓存的最大大小,当缓存的大小超过指定的阈值时,将会按照一定的算法淘汰一部分缓存项。
3. 基于引用的淘汰(Reference-based Eviction):根据缓存项的引用情况来进行淘汰策略。可以选择强引用、软引用或弱引用作为缓存项的引用类型,当JVM对缓存项的引用不再存在时,将会自动移除缓存项。
4. 基于权重的淘汰(Weight-based Eviction):根据缓存项的权重来进行淘汰策略。可以为每个缓存项设置权重,当缓存的总权重超过指定的阈值时,将会按照一定的算法淘汰一部分缓存项。
除了以上常用的淘汰策略外,CaffeineCache还支持自定义的淘汰策略。通过实现`Weigher`接口或者自定义过期策略,应用程序可以根据自己的需求来定制缓存的淘汰策略。
需要注意的是,CaffeineCache并不保证缓存项的准确性和一致性。它只提供了一些基本的缓存策略,应用程序在使用时需要根据自身的业务逻辑进行适当的控制和处理。
三、一些策略
依赖:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.2</version>
</dependency>
1、缓存填充策略
Caffeine Cache提供了三种缓存填充策略:手动、同步加载和异步加载。
1.1、手动
在每次get key的时候指定一个同步的函数,如果key不存在就调用这个函数生成一个值。
/**
* 手动加载
* @param key
* @return
*/
public Object manulOperator(String key) {
Cache<String, Object> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS)
.expireAfterAccess(1, TimeUnit.SECONDS)
.maximumSize(10)
.build();
//如果一个key不存在,那么会进入指定的函数生成value
Object value = cache.get(key, t -> setValue(key).apply(key));
cache.put("hello",value);
//判断是否存在如果不存返回null
Object ifPresent = cache.getIfPresent(key);
//移除一个key
cache.invalidate(key);
return value;
}
public Function<String, Object> setValue(String key){
return t -> key + "value";
}
1.2、同步加载
构造Cache时候,build方法传入一个CacheLoader实现类。实现load方法,通过key加载value。
/**
* 同步加载
* @param key
* @return
*/
public Object syncOperator(String key){
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(k -> setValue(key).apply(key));
return cache.get(key);
}
public Function<String, Object> setValue(String key){
return t -> key + "value";
}
1.3、异步加载
AsyncLoadingCache是继承自LoadingCache类的,异步加载使用Executor去调用方法并返回一个CompletableFuture。异步加载缓存使用了响应式编程模型。
如果要以同步方式调用时,应提供CacheLoader。要以异步表示时,应该提供一个AsyncCacheLoader,并返回一个CompletableFuture。
/**
* 异步加载
*
* @param key
* @return
*/
public Object asyncOperator(String key){
AsyncLoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.buildAsync(k -> setAsyncValue(key).get());
return cache.get(key);
}
public CompletableFuture<Object> setAsyncValue(String key){
return CompletableFuture.supplyAsync(() -> {
return key + "value";
});
}
2、回收策略(缓存清理)
Caffeine提供了3种回收策略:基于大小回收,基于时间回收,基于引用回收。
2.1、基于容量
LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10) // 基于容量,超过10个会基于最近最常使用算法 进行缓存清理
.build(key -> createData(key));
2.2、基于时间
Caffeine提供了三种方法进行基于时间的驱逐:
(1)expireAfterAccess(long, TimeUnit): 一个元素在上一次读写操作后一段时间之后,在指定的时间后没有被再次访问将会被认定为过期项。在当被缓存的元素时被绑定在一个session上时,当session因为不活跃而使元素过期的情况下,这是理想的选择。
(2)expireAfterWrite(long, TimeUnit): 一个元素将会在其创建或者最近一次被更新之后的一段时间后被认定为过期项。在对被缓存的元素的时效性存在要求的场景下,这是理想的选择。
(3)expireAfter(Expiry): 一个元素将会在指定的时间后被认定为过期项。当被缓存的元素过期时间收到外部资源影响的时候,这是理想的选择。
// 基于固定的过期时间驱逐策略
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));
2.3、基于引用
Caffeine 允许配置你的缓存去让GC去帮助清理缓存当中的元素,其中key支持弱引用,而value则支持弱引用和软引用。记住 AsyncCache不支持软引用和弱引用。
(1)Caffeine.weakKeys() 在保存key的时候将会进行弱引用。这允许在GC的过程中,当key没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行key之间的比较。
(2)Caffeine.weakValues()在保存value的时候将会使用弱引用。这允许在GC的过程中,当value没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行value之间的比较。
(3)Caffeine.softValues()在保存value的时候将会使用软引用。为了相应内存的需要,在GC过程中被软引用的对象将会被通过LRU算法回收。由于使用软引用可能会影响整体性能,我们还是建议通过使用基于缓存容量的驱逐策略代替软引用的使用。同样的,使用 softValues() 将会通过引用相等(==)而不是对象相等 equals()去进行value之间的比较。
// 当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));
3、缓存移出
3.1、手动移出
// 失效key
cache.invalidate(key)
// 批量失效key
cache.invalidateAll(keys)
// 失效所有的key
cache.invalidateAll()
3.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();
4、刷新缓存
异步刷新缓存,重新加载。
LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10)
.expireAfterWrite(10, TimeUnit.SECONDS)
.build(key -> createData(key));
// 刷新
cache.refresh("key");