1. 简介
Caffeine是一个用于Java应用程序的高性能缓存框架。它提供了一个强大且易于使用的缓存库,可以在应用程序中使用,以提高数据访问的速度和效率。
下面是一些Caffeine缓存框架的主要特点:
-
高性能:Caffeine的设计目标之一是提供卓越的性能。它通过使用高效的数据结构和优化的算法来实现快速的缓存访问。与其他一些常见的缓存框架相比,Caffeine在缓存访问的速度和响应时间上表现出色。
-
内存管理:Caffeine提供了灵活的内存管理选项。它支持基于大小、基于数量和基于权重的缓存大小限制。你可以根据应用程序的需求来选择合适的缓存大小策略,并且可以通过配置参数进行进一步的调整。
-
强大的功能:Caffeine提供了许多强大的功能来满足各种需求。它支持异步加载和刷新缓存项,可以设置过期时间和定时刷新策略,支持缓存项的自动删除和手动失效等。此外,Caffeine还提供了统计信息和监听器机制,可以方便地监控和管理缓存的状态和变化。
-
线程安全:Caffeine是线程安全的,可以在多线程环境中安全地使用。它使用了细粒度的锁定机制来保护共享资源,确保并发访问的正确性和一致性。
-
易于集成:Caffeine是一个独立的Java库,可以很容易地与现有的应用程序集成。它与标准的Java并发库和其他第三方库兼容,并且可以与各种框架和技术(如Spring、Hibernate等)无缝集成。
官方文档:https://github.com/ben-manes/caffeine/wiki/Home-zh-CN
2. Quick Start
写几个单元测试 熟悉一下 caffeine的基本用法
2.1 添加maven依赖
java8 最高只能使用2.x的版本
<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><version>2.9.3</version>
</dependency>
2.2 添加缓存
数据准备
private final List<User> users = Lists.newArrayList(new User(1, "zhangsan"),new User(2, "lisi"),new User(3, "wangwu"));private final int userKey = 1;private final List<Integer> userKeys = Lists.newArrayList(1, 2);@SneakyThrows
private User getUserById(Integer id) {TimeUnit.SECONDS.sleep(1);return users.stream().filter(u -> Objects.equals(u.getId(), id)).findFirst().get();
}
2.2.1 手动加载
@Test
public void manual() {Cache<Integer, User> cache = Caffeine.newBuilder()// 元素写入10分钟后过期.expireAfterWrite(10, TimeUnit.MINUTES)// 最大能放1w个元素.maximumSize(10_000).build();// 查找一个缓存元素, 没有查找到的时候返回nullUser user = cache.getIfPresent(userKey);// 如果缓存不存在则执行 mappingFunction 生成缓存元素返回, 并将元素put进cache// 类似于map的 computeIfAbsent方法user = cache.get(userKey, k -> getUserById(userKey));// 添加或者更新一个缓存元素cache.put(userKey, getUserById(userKey));// 移除一个缓存元素cache.invalidate(userKey);
}
推荐使用
cache.get(key, k -> value)
操作来在缓存中不存在该key对应的缓存元素的时候进行计算生成并直接写入至缓存内,而当该key对应的缓存元素存在的时候将会直接返回存在的缓存值。
2.2.2 自动加载
@Test
public void loading() {LoadingCache<Integer, User> cache = Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(10, TimeUnit.MINUTES)// 设置自动加载的function.build(this::getUserById);// 查找缓存,如果缓存不存在则自动调用getUserById生成缓存元素, 如果无法生成则返回nullUser user = cache.get(userKey);// 批量查找缓存,如果缓存不存在则生成缓存元素Map<Integer, User> users = cache.getAll(userKeys);
}
LoadingCache
是一个Cache
附加上CacheLoader
能力之后的缓存实现。
通过
getAll
可以达到批量查找缓存的目的。 默认情况下,在getAll
方法中,将会对每个不存在对应缓存的key调用一次CacheLoader.load
来生成缓存元素。 在批量检索比单个查找更有效率的场景下,你可以覆盖并开发CacheLoader.loadAll
方法来使你的缓存更有效率。
2.2.3 异步手动加载
@Test
@SneakyThrows
public void asyncManual() {AsyncCache<Integer, User> cache = Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).maximumSize(10_000)// 构建异步对象.buildAsync();// 查找一个缓存元素, 没有查找到的时候返回nullCompletableFuture<User> user = cache.getIfPresent(userKey);// 查找缓存元素,如果不存在,则异步生成user = cache.get(userKey, k -> getUserById(userKey));// 添加或者更新一个缓存元素cache.put(userKey, user);// 移除一个缓存元素cache.synchronous().invalidate(userKey);
}
AsyncCache
是Cache
的一个变体,AsyncCache
提供了在Executor
上生成缓存元素并返回CompletableFuture
的能力。这给出了在当前流行的响应式编程模型中利用缓存的能力。
synchronous()
方法给Cache
提供了阻塞直到异步缓存生成完毕的能力。当然,也可以使用
AsyncCache.asMap()
所暴露出来的ConcurrentMap
的方法对缓存进行操作。默认的线程池实现是
ForkJoinPool.commonPool()
,当然你也可以通过覆盖并实现Caffeine.executor(Executor)
方法来自定义你的线程池选择。
2.2.4 异步自动加载
@Test
@SneakyThrows
public void asyncLoading() {AsyncLoadingCache<Integer, User> cache = Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(10, TimeUnit.MINUTES)// 设置自动加载的function.buildAsync(key -> getUserById(key));// 也可以指定加载时使用缓存对象的executor//.buildAsync((key, executor) -> getUserById(key, executor));// 查找缓存元素,如果其不存在,将会异步进行生成CompletableFuture<User> user = cache.get(userKey);// 批量查找缓存元素,如果其不存在,将会异步进行生成CompletableFuture<Map<Integer, User>> users = cache.getAll(userKeys);
}
AsyncLoadingCache
是一个AsyncCache
加上AsyncCacheLoader
能力的实现。
2.3 缓存驱逐
Caffeine 提供了三种驱逐策略,分别是基于容量,基于时间和基于引用三种类型。
在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而它将会在写操作之后进行少量的维护工作,在写操作较少的情况下,也偶尔会在读操作之后进行。如果你的缓存吞吐量较高,那么你不用去担心你的缓存的过期维护问题。
2.3.1 基于容量
@Test
@SneakyThrows
public void evictionWithSize() {// 基于缓存内的元素个数进行驱逐LoadingCache<Integer, User> cacheWithSize = Caffeine.newBuilder().maximumSize(1).build(key -> getUserById(key));// 基于缓存内元素权重进行驱逐LoadingCache<Integer, User> cacheWitWeight = Caffeine.newBuilder().maximumWeight(50)// 权重必须大于0.weigher((Integer key, User user) -> Math.abs(user.hashCode() % 100)).build(key -> getUserById(key));for (User user : users) {cacheWithSize.put(user.getId(), user);cacheWitWeight.put(user.getId(), user);}//因为是异步驱逐的 所以需要睡眠一下TimeUnit.SECONDS.sleep(1);log.info("cacheWithSize size:{}, element: {}", cacheWithSize.asMap().size(), cacheWithSize.asMap());// cacheWithSize size:1, element: {3=User(id=3, name=wangwu)}log.info("cacheWitWeight size:{} element: {}", cacheWitWeight.asMap().size(), cacheWitWeight.asMap());// cacheWitWeight size:2 element: {2=User(id=2, name=lisi), 3=User(id=3, name=wangwu)}
}
如果你的缓存容量不希望超过某个特定的大小,那么记得使用
Caffeine.maximumSize(long)
。缓存将会尝试通过基于就近度和频率的算法来驱逐掉不会再被使用到的元素。另一种情况,你的缓存可能中的元素可能存在不同的“权重”--打个比方,你的缓存中的元素可能有不同的内存占用--你也许需要借助
Caffeine.weigher(Weigher)
方法来界定每个元素的权重并通过Caffeine.maximumWeight(long)
方法来界定缓存中元素的总权重来实现上述的场景。除了“最大容量”所需要的注意事项,在基于权重驱逐的策略下,一个缓存元素的权重计算是在其创建和更新时,此后其权重值都是静态存在的,在两个元素之间进行权重的比较的时候,并不会根据进行相对权重的比较。
2.3.2 基于时间
@Test
public void evictionWithTime() {// 基于固定的过期时间驱逐策略 - 访问多久后过期LoadingCache<Integer, User> cacheWithAccessTime = Caffeine.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build(key -> getUserById(key));// 基于固定的过期时间驱逐策略 - 写入多久后过期LoadingCache<Integer, User> cacheWithWriteTime = Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build(key -> getUserById(key));// 基于不同的过期驱逐策略LoadingCache<Integer, User> cacheWithDynamicTime = Caffeine.newBuilder().expireAfter(new Expiry<Integer, User>() {// 创建多久后过期public long expireAfterCreate(Integer key, User user, long currentTime) {// 给一个60-120秒的随机时间return TimeUnit.SECONDS.toNanos(RandomUtils.nextInt(60, 120));}// 更新多久后过期public long expireAfterUpdate(Integer key, User graph,long currentTime, long currentDuration) {return currentDuration;}// 访问多久后过期public long expireAfterRead(Integer key, User graph,long currentTime, long currentDuration) {return currentDuration;}}).build(key -> getUserById(key));
}
Caffeine提供了三种方法进行基于时间的驱逐:
expireAfterAccess(long, TimeUnit):
一个元素在上一次读写操作后一段时间之后,在指定的时间后没有被再次访问将会被认定为过期项。expireAfterWrite(long, TimeUnit):
一个元素将会在其创建或者最近一次被更新之后的一段时间后被认定为过期项。expireAfter(Expiry):
一个元素将会在指定的时间后被认定为过期项。在写操作,和偶尔的读操作中将会进行周期性的过期事件的执行。过期事件的调度和触发将会在O(1)的时间复杂度内完成。
2.3.3 基于引用
@Test
public void evictionWithReference() {// 当key和缓存元素都不再存在其他强引用的时候驱逐LoadingCache<Integer, User> cacheWithWeak = Caffeine.newBuilder().weakKeys().weakValues().build(key -> getUserById(key));// 当进行GC的时候进行驱逐LoadingCache<Integer, User> cacheWithSoft = Caffeine.newBuilder().softValues().build(key -> getUserById(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之间的比较。
2.4 删除缓存
术语:
- 驱逐(eviction) 缓存元素因为策略被移除(如2.3章节)
- 失效(invalidation) 缓存元素被手动移除
- 移除(removal) 由于驱逐或者失效而最终导致的结果
@Test
@SneakyThrows
public void removeOrEvictionRecord() {Cache<Integer, User> cache = Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS).evictionListener((Integer key, User user, RemovalCause cause) ->log.info("Key {} was evicted ({})", key, cause)).removalListener((Integer key, User user, RemovalCause cause) ->log.info("Key {} was removed ({})", key, cause)).build();for (User user : users) {cache.put(user.getId(), user);}log.info("cache data put success");TimeUnit.SECONDS.sleep(10);// 失效keycache.invalidate(userKey);// 批量失效keycache.invalidateAll(userKeys);// 失效所有的keycache.invalidateAll();
}
你可以为你的缓存通过
Caffeine.removalListener(RemovalListener)
方法定义一个移除监听器在一个元素被移除的时候进行相应的操作。这些操作是使用Executor
异步执行的,其中默认的 Executor 实现是ForkJoinPool.commonPool()
并且可以通过覆盖Caffeine.executor(Executor)
方法自定义线程池的实现。当移除之后的自定义操作必须要同步执行的时候,你需要使用
Caffeine.evictionListener(RemovalListener)
。这个监听器将在RemovalCause.wasEvicted()
为 true 的时候被触发。为了移除操作能够明确生效,Cache.asMap()
提供了方法来执行原子操作。记住任何在
RemovalListener
中被抛出的异常将会被吞食。
2.5 刷新缓存
@Test
@SneakyThrows
public void refreshAfterWrite() {// 同时使用 expireAfterWrite refreshAfterWrite// 使一个元素在其被允许刷新但是没有被主动查询的时候,这个元素也会被视为过期(防止不活跃的数据常驻内存)LoadingCache<Integer, User> cache = Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(Duration.ofSeconds(10))/** 1. 写入到达指定时间后刷新* 2. 不是到达时间直接刷新 而是标记为准备刷新 数据下次访问的时候才开始刷新* 3. 在刷新的时候如果查询缓存元素,那么直接返回旧值*/.refreshAfterWrite(Duration.ofSeconds(3)).build(key -> getUserById(key));
}
刷新和驱逐并不相同。可以通过
LoadingCache.refresh(K)
方法,异步为key对应的缓存元素刷新一个新的值。与驱逐不同的是,在刷新的时候如果查询缓存元素,其旧值将仍被返回,直到该元素的刷新完毕后结束后才会返回刷新后的新值。与
expireAfterWrite
相反,refreshAfterWrite
将会使在写操作之后的一段时间后允许key对应的缓存元素进行刷新,但是只有在这个key被真正查询到的时候才会正式进行刷新操作。所以打个比方,你可以在同一个缓存中同时用到refreshAfterWrite
和expireAfterWrite
,这样缓存元素在被允许刷新的时候不会直接刷新使得过期时间被盲目重置。当一个元素在其被允许刷新但是没有被主动查询的时候,这个元素也会被视为过期。
2.6 统计
@Test
public void statistics() {Cache<Integer, User> cache = Caffeine.newBuilder().maximumSize(10_000).recordStats().build();// hitRate 查询缓存的命中率// evictionCount 被驱逐的缓存数量// averageLoadPenalty 新值被载入的平均耗时log.info("cache stats:{}", cache.stats());// cache stats:CacheStats{hitCount=0, missCount=0, loadSuccessCount=0, loadFailureCount=0, totalLoadTime=0, evictionCount=0, evictionWeight=0}
}
3. 集成SpringBoot
集成 SpringBoot
有两种方式
-
将cache对象声明称
SpringBean
然后使用的时候注入进来直接操作 -
结合
Spring
的CacheManager
将caffeine
注册到cache模块中,然后使用spring注解进行缓存操作cache 方面的注解主要有以下 5 个:
@Cacheable【创建、查询缓存】:触发缓存入口(一般放在创建和获取的方法上,@Cacheable 注解会先查询是否已经有缓存。如果有,则直接从缓存中返回;如果没有,则会执行方法并返回结果缓存【返回方法返回 NULL,则不进行缓存】)
@CachePut【更新缓存】:更新缓存且不影响方法执行(用于修改的方法上,该注解下的方法始终会被执行)
@CacheEvict【删除缓存】:触发缓存的 eviction(用于删除的方法上)
@Caching【组合缓存配置】:将多个缓存组合在一个方法上(该注解可以允许一个方法同时设置多个注解)
@CacheConfig【类级别共享配置】:在类级别设置一些缓存相关的共同配置(与其它缓存配合使用),避免在每个缓存方法上重复配置相同的缓存属性