1. 概述
使用缓存的优点是可以减少直接访问数据库的压力。Caffeine是目前单机版缓存性能最高的,提供了最优的缓存命中率。用法和java中的map集合比较类似,底层使用一个ConcurrencyHashMap来保存所有数据,可以理解为一个增强版的map集合,增强的功能有设置缓存过期时间,缓存数据驱逐,统计缓存数据等。本文会大量使用详细的代码示例,通俗易懂地帮助大家学会使用Caffeine本地缓存。
常见QA
- Caffeine和redis的区别?共同点都是基于内存。其中,Caffeine是本地缓存,基于单个JVM,不能直接跨多台机器分布,如果程序停止,JVM停止,本地缓存数据会全部丢失,类似java中的map集合,相比Redis,Caffeine的性能更好。Redis是一个分布式缓存系统,独立部署,支持将数据持久化到磁盘上,因此可以在应用程序关闭后仍然保留数据。Redis支持分布式架构,可以配置成主从模式或者集群模式,从而提供更好的水平扩展性和高可用性。
- Ehcache和Caffeine的区别?Caffeine是一个较新的本地缓存框架,在内存管理和高并发访问方面通常比Ehcache更高效。
2. 准备
引入依赖即可
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
3.代码讲解
Caffeine 最核心的是com.github.benmanes.caffeine.cache.Cache接口,所有与缓存有关的处理方法,都是在这个接口之中定义的,接口中的方法见下图。
1. 入门代码
private static void demo01() throws InterruptedException {
/**
* .expireAfterAccess(3L, TimeUnit.SECONDS):假设用户 "Alice" 第一次登录,登录状态被存储在缓存中,并且记录了最后一次访问的时间。
* 假设用户 "Alice" 第一次登录,登录状态被存储在缓存中,并且记录了最后一次访问的时间。
* 如果在3秒内没有任何请求使用了 "Alice" 的登录状态,那么缓存中的 "Alice" 条目会在3秒后自动过期,即被移除。
* 下次有请求需要使用 "Alice" 的登录状态时,缓存会失效,需要重新加载或计算 "Alice" 的登录状态,并将新的状态存储在缓存中
* 这样设计的好处在于,如果用户在一段时间内没有活动(例如3秒内没有操作),那么缓存中的数据会自动过期,可以确保缓存中的数据不会长时间驻留,
* 从而减少缓存占用的内存空间,并且确保了数据的及时更新。
*/
/**
* 在设置了 .maximumSize(100) 之后,如果缓存中的条目数量超过了100,Caffeine 缓存库会根据一定的策略来进行缓存条目的淘汰,
* 以确保缓存的大小不会无限增长。
*/
Cache<String, String> cache = Caffeine.newBuilder() // 构建一个新的caffeine实例
.maximumSize(100) // 设置缓存之中保存的最大数据量
.expireAfterAccess(3L, TimeUnit.SECONDS) // 缓存数据在3秒内没被访问则失效
.build();
cache.put("Bob", "已登录");
cache.put("Lily","未登录");
log.info("未超时获取缓存数据,Bob= {}", cache.getIfPresent("Bob")); // 获取数据,输出:未超时获取缓存数据,Bob= 已登录
TimeUnit.SECONDS.sleep(5); // 5秒后超时
log.info("已超时获取缓存数据,Bob= {}", cache.getIfPresent("Bob")); // 获取数据,输出:已超时获取缓存数据,Bob= null
/**
* 在默认情况下,一旦缓存数据消失之后,Cache 接口可以返回的内容就是 null 数据了,于是有些人认为空数据不利于标注,
* 那么此时也可以考虑进行一些数据的控制。
* 这种数据加载操作指的是在缓存数据不存在的时候进行数据的同步加载处理操作
*/
log.info("已超时获取缓存数据,Bob= {}", cache.get("Bob", (key) -> { // 最终输出:已超时获取缓存数据,Bob= [expire]Bob
log.info("失效处理,没有发现 key = {} 的数据,要进行失效处理控制", key);
return "[expire]" + key; // 失效数据的返回
}));
}
2. 同步数据加载
数据加载是指将数据放入缓存的过程
如果发现指定的 KEY 的缓存项不存在了,Caffeine提供相关功能,实现重新进行数据的加载,例如:通过demo01方法中的操作方法可以发现,此时当缓存数据失效之后,可以自动的根据 Function 函数式接口加载所需要的数据内容(demo01中cache.get(“Bob”, (key) -> { }代码部分)
同步数据加载操作属于同步的操作范畴,加载不停,数据是不会返回的(所有操作均由主线程顺序执行)。而除了上文入门案例demo01中的加载机制之外,在缓存组件之中还提供有一个较为特殊的 CacheLoader 接口,这个接口的触发机制有些不太一样,它所采用的依然是同步的加载处理。
private static void demo02(){
LoadingCache<String, String> cache = Caffeine.newBuilder() // 第四步,修改变量类型为LoadingCache
.maximumSize(100) // 设置缓存之中保存的最大数据量
.expireAfterAccess(3L, TimeUnit.SECONDS) // 缓存数据在3秒内没被访问则失效
.build(new CacheLoader<String, String>() { // 第一步,build方法中传参
@Override
public @Nullable String load(@NonNull String s) throws Exception {
log.info("[cacheLoader]进行缓存数据的加载处理, 当前的key = {}", s);
TimeUnit.SECONDS.sleep(1); // 第三步,模拟数据的加载延迟
return "【loadingcache】" + s; // 第二步,数据加载的返回结果
}
});
cache.put("Bob", "已登录");
cache.put("Lily","未登录");
cache.put("Wang", "未登录");
log.info("未超时获取缓存数据,Bob= {}", cache.getIfPresent("Bob")); // 未超时获取缓存数据,Bob= 已登录
try {
TimeUnit.SECONDS.sleep(5); // 5秒后超时
} catch (InterruptedException e) {
e.printStackTrace();
}
cache.put("Lee", "未登录"); // 第五步,缓存失效以后添加新的数据项
List<String> list = new ArrayList<>(); // 第六步,封装一个list
list.add("Bob");
list.add("Lily");
list.add("Lee");
for (Map.Entry<String, String> entry : cache.getAll(list).entrySet()){ // 第七步
log.info("【数据加载】key={},value={}", entry.getKey(), entry.getValue());
}
/**
* 第八步,返回结果
* 未超时获取缓存数据,Bob= 已登录
* [cacheLoader]进行缓存数据的加载处理, 当前的key = Bob
* [cacheLoader]进行缓存数据的加载处理, 当前的key = Lily
* 【数据加载】key=Bob,value=【loadingcache】Bob
* 【数据加载】key=Lily,value=【loadingcache】Lily
* 【数据加载】key=Lee,value=未登录
*/
}
3. 数据的异步加载操作
多线程可以提升性能,优先,功能和上面的同步加载数据相同
数据加载进内存的过程是异步的,从缓存中读数据默认还是由主线程同步实现。
注意:缓存的value应为CompletableFuture.completedFuture(“value”)格式
public static void main(String[] args) throws ExecutionException, InterruptedException {
AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100) // 设置缓存之中保存的最大数据量
.expireAfterAccess(3L, TimeUnit.SECONDS) // 缓存数据在3秒内没被访问则失效
.buildAsync((key, executor) ->
CompletableFuture.supplyAsync(() -> {
log.info("[cacheLoader]进行缓存数据的加载处理, 当前的key = {}", key);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "【loadingcache】" + key;
})
);
cache.put("Bob", CompletableFuture.completedFuture("已登录"));
cache.put("Lily",CompletableFuture.completedFuture("未登录"));
cache.put("Wang", CompletableFuture.completedFuture("未登录"));
log.info("未超时获取缓存数据,Bob= {}", cache.getIfPresent("Bob").get()); // 未超时获取缓存数据,Bob= 已登录
try {
TimeUnit.SECONDS.sleep(5); // 5秒后超时
} catch (InterruptedException e) {
e.printStackTrace();
}
cache.put("Lee", CompletableFuture.completedFuture("已登录"));
List<String> list = new ArrayList<>();
list.add("Bob");
list.add("Lily");
list.add("Lee");
for (Map.Entry<String, String> entry : cache.getAll(list).get().entrySet()){
log.info("【数据加载】key={},value={}", entry.getKey(), entry.getValue());
}
/**
* 结果输出
* [main] INFO com.cabbage.demos.AsynJiazai - 未超时获取缓存数据,Bob= 已登录
* [ForkJoinPool.commonPool-worker-2] INFO com.cabbage.demos.AsynJiazai - [cacheLoader]进行缓存数据的加载处理, 当前的key = Bob
* [ForkJoinPool.commonPool-worker-11] INFO com.cabbage.demos.AsynJiazai - [cacheLoader]进行缓存数据的加载处理, 当前的key = Lily
* [main] INFO com.cabbage.demos.AsynJiazai - 【数据加载】key=Bob,value=【loadingcache】Bob
* [main] INFO com.cabbage.demos.AsynJiazai - 【数据加载】key=Lily,value=【loadingcache】Lily
* [main] INFO com.cabbage.demos.AsynJiazai - 【数据加载】key=Lee,value=已登录
*/
4.缓存数据驱逐
默认的缓存驱逐算法是Window-TinyLFU,提供了最优命中率,有效避免热点数据的失效。
以下代码示例都是基于同步缓存数据加载,
4.1基于缓存容量的驱逐策略
- 假设缓存容量设置为1,当你设置第二条数据时,第一条数据丢失
private static void demo01() throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder() // 构建一个新的caffeine实例
.maximumSize(1) // 设置缓存之中保存的最大数据量
.expireAfterAccess(3L, TimeUnit.SECONDS) // 缓存数据在3秒内没被访问则失效
.build();
cache.put("Bob", "已登录");
cache.put("Lily","未登录");
TimeUnit.MILLISECONDS.sleep(10);
/**
有一些延迟
* 如果不加sleep,会出现
* 现在缓存个数已经超过了,但是最早的缓存数据还在保留,没有及时清理
* 大家可以去掉sleep自己试一下
*/
log.info("获取缓存数据,Bob= {}", cache.getIfPresent("Bob")); // 输出:获取缓存数据,Bob= null
log.info("获取缓存数据,Lily= {}", cache.getIfPresent("Lily")); //输出:获取缓存数据,Lily= 未登录
}
4.2 基于缓存权重驱逐策略
- 先设置一个总的权重,再为每一条数据定义权重,例如:假设总权重为100,为每条数据设置权重50,那么在你设置第三条数据的时候,会有一条缓存数据被淘汰。
private static void demo02(){
Cache<String, String> cache = Caffeine.newBuilder()
// .maximumSize(100)
/**
* 在进行权重驱逐策略配置的时候,使用的方法为“maximumWeiaht()'
* 但是此时不要再设置保存的个数了,
* 因为个数的算法和权重的算法是两个不同的方式,二选一的关系。
*/
.maximumWeight(100) // 第一步,设置缓存之中的最大权重
.weigher((key, value) -> { // 第二步,权重计算
log.info("[weigher权重计算器] key = {}, val = {}", key, value);
// 实际开发之中的权重计算处理操作,可以通过KEY和VALUE的长度计算得来
return 50; // 第三步
})
.expireAfterAccess(3L, TimeUnit.SECONDS) // 缓存数据在3秒内没被访问则失效
.build();
cache.put("Bob", "已登录");
cache.put("Lily","未登录");
cache.put("Wang","未登录");
cache.put("Lee","已登录");
log.info("获取缓存数据,Bob= {}", cache.getIfPresent("Bob"));
log.info("获取缓存数据,Lily= {}", cache.getIfPresent("Lily"));
log.info("获取缓存数据,Wang= {}", cache.getIfPresent("Wang"));
log.info("获取缓存数据,Lee= {}", cache.getIfPresent("Lee"));
/**
* 输出
* [weigher权重计算器] key = Bob, val = 已登录
* [weigher权重计算器] key = Lily, val = 未登录
* [weigher权重计算器] key = Wang, val = 未登录
* [weigher权重计算器] key = Lee, val = 已登录
* 获取缓存数据,Bob= null
* 获取缓存数据,Lily= null
* 获取缓存数据,Wang= 未登录
* 获取缓存数据,Lee= 已登录
*/
}
4.3 基于时间的驱逐策略
- 在进行驱逐的时候,对于时间的管理有两种,一种是通过最后一次读的方式进行配置(见入门代码.expireAfterAccess(3L, TimeUnit.SECONDS) // 缓存数据在3秒内没被访问则失效),另外一种就是通过写的时间进行计数(写完以后的第几秒,缓存会失效)。
private static void demo03() throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(2L, TimeUnit.SECONDS) // 第一步,写入后两秒失效
.build();
cache.put("Bob", "已登录");
for (int i = 0;i<3;i++){
TimeUnit.MILLISECONDS.sleep(1500); // 每次休眠1.5秒
log.info("[第{}次访问] key = {}, value = {}", i, "Bob", cache.getIfPresent("Bob"));
}
/**
* 输出
* 14:34:49.972 [main] INFO com.cabbage.demos.CacheEvictionManager - [第0次访问] key = Bob, value = 已登录
* 14:34:51.478 [main] INFO com.cabbage.demos.CacheEvictionManager - [第1次访问] key = Bob, value = null
* 14:34:52.989 [main] INFO com.cabbage.demos.CacheEvictionManager - [第2次访问] key = Bob, value = null
*/
}
4.4 采用定制化的缓存驱逐策略
- 可以通过 Expiry 接口来实现,这个接口内部定义有如下的处理方法:expireAfterCreate,expireAfterUpdate,expireAfterRead,详细实现参考以下代码
private static void demo04() {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfter(new Expiry<String, String>() {
@Override
public long expireAfterCreate(@NonNull String s, @NonNull String s2, long l) {
log.info("[创建后失效计算 key = {}, value = {}]", s, s2);
// 相当于创建后多少秒就失效了
return TimeUnit.NANOSECONDS.convert(2, TimeUnit.SECONDS);
}
@Override
public long expireAfterUpdate(@NonNull String s, @NonNull String s2, long l, @NonNegative long l1) {
log.info("[更新后失效计算 key = {}, value = {}]", s, s2);
// 更新完多少秒后就失效了
return TimeUnit.NANOSECONDS.convert(3, TimeUnit.SECONDS);
}
@Override
public long expireAfterRead(@NonNull String s, @NonNull String s2, long l, @NonNegative long l1) {
log.info("[读取后失效计算 key = {}, value = {}]", s, s2);
// 读取完多少秒后就失效了
return TimeUnit.NANOSECONDS.convert(5, TimeUnit.SECONDS); // 将2秒转成纳秒
}
}) // 第一步
.build();
cache.put("Bob", "已登录");
cache.put("Lily","未登录");
cache.put("Wang","未登录");
cache.put("Lee","已登录");
log.info("获取缓存数据,Bob= {}", cache.getIfPresent("Bob"));
log.info("获取缓存数据,Lily= {}", cache.getIfPresent("Lily"));
log.info("获取缓存数据,Wang= {}", cache.getIfPresent("Wang"));
log.info("获取缓存数据,Lee= {}", cache.getIfPresent("Lee"));
/**
*
* 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - [创建后失效计算 key = Bob, value = 已登录]
* 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - [创建后失效计算 key = Lily, value = 未登录]
* 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - [创建后失效计算 key = Wang, value = 未登录]
* 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - [创建后失效计算 key = Lee, value = 已登录]
* 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - [读取后失效计算 key = Bob, value = 已登录]
* 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - 获取缓存数据,Bob= 已登录
* 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - [读取后失效计算 key = Lily, value = 未登录]
* 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - 获取缓存数据,Lily= 未登录
* 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - [读取后失效计算 key = Wang, value = 未登录]
* 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - 获取缓存数据,Wang= 未登录
* 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - [读取后失效计算 key = Lee, value = 已登录]
* 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - 获取缓存数据,Lee= 已登录
*/
}
5. 缓存数据的删除与监听
- 手动删除一条数据
private static void demo01() {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.build();
cache.put("Bob", "已登录");
cache.put("Lily","未登录");
cache.put("Wang","未登录");
cache.put("Lee","已登录");
cache.invalidate("Bob"); // 第一步,删除指定key的缓存
// cache.invalidateAll(); // 删除所有缓存
log.info("获取缓存数据,Bob= {}", cache.getIfPresent("Bob"));
log.info("获取缓存数据,Lily= {}", cache.getIfPresent("Lily"));
log.info("获取缓存数据,Wang= {}", cache.getIfPresent("Wang"));
log.info("获取缓存数据,Lee= {}", cache.getIfPresent("Lee"));
/**
* 输出结果
* 11:20:37.978 [main] INFO com.cabbage.demos.CacheDelAndListener - 获取缓存数据,Bob= null
* 11:20:37.980 [main] INFO com.cabbage.demos.CacheDelAndListener - 获取缓存数据,Lily= 未登录
* 11:20:37.980 [main] INFO com.cabbage.demos.CacheDelAndListener - 获取缓存数据,Wang= 未登录
* 11:20:37.980 [main] INFO com.cabbage.demos.CacheDelAndListener - 获取缓存数据,Lee= 已登录
*/
}
- 删除监听,删除数据之前可以通过监听进行一些操作
private static void demo02() {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.removalListener(new RemovalListener<String, String>() { // 第一步,设置监听器,删除时触发,removalCause是删除的原因
@Override
public void onRemoval(@Nullable String s, @Nullable String s2, @NonNull RemovalCause removalCause) {
log.info("【数据删除监听】key = {}, value = {}, cause = {}", s, s2, removalCause);
}
})
.build();
cache.put("Bob", "已登录");
cache.put("Lily","未登录");
cache.put("Wang","未登录");
cache.put("Lee","已登录");
cache.invalidate("Bob"); // 删除指定key的缓存
// cache.invalidateAll(); // 删除所有缓存
log.info("获取缓存数据,Bob= {}", cache.getIfPresent("Bob"));
log.info("获取缓存数据,Lily= {}", cache.getIfPresent("Lily"));
log.info("获取缓存数据,Wang= {}", cache.getIfPresent("Wang"));
log.info("获取缓存数据,Lee= {}", cache.getIfPresent("Lee"));
}
/**
* 输出结果
* 11:19:46.021 [main] INFO com.cabbage.demos.CacheDelAndListener - 获取缓存数据,Bob= null
* 11:19:46.021 [ForkJoinPool.commonPool-worker-9] INFO com.cabbage.demos.CacheDelAndListener - 【数据删除监听】key = Bob, value = 已登录, cause = EXPLICIT
* 11:19:46.024 [main] INFO com.cabbage.demos.CacheDelAndListener - 获取缓存数据,Lily= 未登录
* 11:19:46.024 [main] INFO com.cabbage.demos.CacheDelAndListener - 获取缓存数据,Wang= 未登录
* 11:19:46.024 [main] INFO com.cabbage.demos.CacheDelAndListener - 获取缓存数据,Lee= 已登录
*/
6. CacheStats 缓存数据统计
/**
* 获取缓存的 统计数据
* 在使用数据统计的时候,Caffeine 内部使用了一个 StatsCounter 接口类型,
* 最终如果要想实现数据的统计的处理操作,那么肯定是需要通过 StatsCounter 接口实现的,而这个接口提供有一个内置的并
* 发数据统计的操作实现子类。
* Cafeine 缓存组件除了提供有强大的缓存处理性能之外,也额外提供了一些缓存数据的统计功能,每当用户进行缓存数据操下时,
* 都可以对这些操作记录的结果进行记录,这样就可以准确的知道缓存命中数、失效数、驱逐数等统计结果。
*/
private static void demo01() throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterAccess(1L, TimeUnit.MILLISECONDS) // 设置1毫秒未读过期
.recordStats()
.build();
cache.put("Bob", "已登录");
cache.put("Lily","未登录");
cache.put("Wang","未登录");
cache.put("Lee","已登录");
// 此时设置的候选的KEY数据是有些不存在的,通过这些不存在的数据进行最终的非命中统计操作
String[] keys = new String[]{"Bob", "Lily", "Wang", "Lee", "No1", "no2"};
// 定义随机数
Random random = new Random();
for (int i=0;i<1000;i++){
new Thread(() -> {
String key = keys[random.nextInt(keys.length)]; // 随机选取一个key
log.info("key = {}, value = {}", key,cache.getIfPresent(key));
},"查询线程 - "+i).start();
}
TimeUnit.SECONDS.sleep(1); // 让多线程执行完
CacheStats stats = cache.stats();
log.info("【CacheStats】缓存操作请求次数: {}", stats.requestCount());
log.info("【CacheStats】缓存命中次数: {}", stats.hitCount());
log.info("【Cachestats】缓存未命中次数: {}", stats.missCount());
//所有的缓存组件里面,最为重要的一项性能指标就是命中率的处理问题了
log.info("【CacheStats】缓存命中率: {}", stats.hitRate());
log.info("【CacheStats】缓存驱逐次数: {}", stats.evictionCount());
/**
* 输出结果
16:20:02.911 [main] INFO com.cabbage.demos.CacheStatsDemo - 【CacheStats】缓存操作请求次数: 1000
16:20:02.911 [main] INFO com.cabbage.demos.CacheStatsDemo - 【CacheStats】缓存命中次数: 13
16:20:02.911 [main] INFO com.cabbage.demos.CacheStatsDemo - 【Cachestats】缓存未命中次数: 987
16:20:02.911 [main] INFO com.cabbage.demos.CacheStatsDemo - 【CacheStats】缓存命中率: 0.013
16:20:02.911 [main] INFO com.cabbage.demos.CacheStatsDemo - 【CacheStats】缓存驱逐次数: 4
*/
}