文章目录
一、Caffeine介绍
1. 什么是Caffeine
Caffeine是一个基于Java1. 8的高性能本地缓存库。它提供了灵活的缓存配置选项,如缓存大小限制、过期策略、驱逐策略等。Caffeine是由guava演变而来的,性能和内存占用都优于guava,它的底层数据存储采用ConcurrentHashMap,从功能上可以将Caffeine视为带有淘汰策略的ConcurrentHashMap。
官方介绍及源码详见:Caffeine的Github网址
2. 为什么要用Caffeine
1.自动加载和异步刷新:
Caffeine支持缓存项的自动加载和刷新。当缓存项过期或不存在时,Caffeine 可以配置为异步的刷新该缓存项,从而提高应用程序的响应性。通过注解的方式我们很容易就能实现存量代码的缓存,对代码侵入性低,而且简单方便。
2.灵活的缓存策略:
Caffeine提供了基于大小和频率/最近使用情况的多种逐出策略(后面会提到),从而提高缓存的命中率。
3.高性能、线程安全:
Caffeine使用了ConcurrentHashMap等数据结构来提供高性能的缓存操作,并且支持多线程场景。通过减少锁竞争和上下文切换,Caffeine能够实现高吞吐量的缓存访问。
4.内存友好:
①Caffeine使用弱引用自动包装键:这样当没有其他强引用指向这些键时,它们可以被垃圾回收器回收。这有助于减少内存占用,并允许缓存自动释放不再需要的条目。
②Caffeine使用弱引用或软引用自动包装值:与键类似,缓存值也可以自动包装在弱引用或软引用中。弱引用值在没有其他强引用时会被回收,而软引用值则会在内存不足时被回收。这为缓存提供了额外的灵活性,允许它在内存压力下自动释放部分数据。
5.访问速度快:
相比分布式缓存,本地缓存访问速度更快,能够减少网络I/O开销。
3. Caffeine使用的缓存淘汰策略
1.默认使用W-TinyLFU:
W-TinyLFU(Weighted Tiny Least Frequently Used)结合了LRU(Least Recently Used,最近最少使用)和LFU(Least FrequentlyUsed,最不经常使用)的优点。它通过记录访问频率并使用对数计数器来避免对LFU的偏置,从而提供更加准确的缓存淘汰策略。这种策略能够在多种场景下提供接近理论最优的缓存性能。
2.基于时间的过期策略:
①TTL(Time to Live):根据缓存项的存活时间来进行过期策略。可以设置缓存项的过期时间,当缓存项超过指定的时间后,将会被自动移除。
②TTR(Time to Refresh):在缓存项最后一次访问一段时间后进行自动清理。
3.基于大小的淘汰策略:
根据缓存项的大小来进行淘汰策略。可以设置缓存的最大大小,当缓存的大小超过指定的阈值时,将会按照一定的算法淘汰一部分缓存项。
4.基于引用的淘汰策略:
根据缓存项的引用情况来进行淘汰策略。可以选择强引用、软引用或弱引用作为缓存项的引用类型,当JVM对缓存项的引用不再存在时,将会自动移除缓存项。
5.基于权重的淘汰策略:
根据缓存项的权重来进行淘汰策略。可以为每个缓存项设置权重,当缓存的总权重超过指定的阈值时,将会按照一定的算法淘汰一部分缓存项。
6.自定义淘汰策略:
Caffeine还允许用户通过实现特定的接口来定义自己的淘汰策略,以满足特定应用场景的需求。
4. Caffeine的应用场景
1.常用数据缓存:
对于需要频繁访问但又不会频繁变更的数据,如配置信息、枚举值(如类目)等,可以使用Caffeine进行缓存,以减少对数据库或其他存储系统的访问,提高数据读取速度。
2.实时性要求不高的数据缓存:
对于实时性要求不高的场景,如某些统计信息、历史数据,可以使用Caffeine进行缓存。这些数据的变更频率相对较低,即使缓存中的数据不是最新的,也不会对业务产生太大的影响。
3.多级缓存:
可以结合Redis等远程缓存作为多级缓存使用,在数据量较大的场景使用,本地缓存未命中再查Redis,Redis未命中再查DB。这样能大大减小Redis的缓存压力。
5. Caffeine的局限性
1.数据共享与一致性
数据共享限制:本地缓存Caffeine是与应用程序在同一个进程内的内存空间中存储数据,因此它只能被该应用程序进程访问,而不能被其他应用程序进程或节点共享。这意味着在集群部署的环境中,每个应用节点都需要维护自己的本地缓存,难以实现数据的高效共享。
数据一致性难题:当数据库中的数据发生更新时,需要同步更新所有服务器节点上的本地缓存,以保证数据的一致性。然而,这一过程操作复杂且容易出错,特别是在集群部署的环境中,数据同步的难度和成本显著增加。
2.存储能力与扩展性
存储能力有限:本地缓存占用了应用进程的内存空间,因此其存储能力受到应用程序进程内存大小的限制。当需要缓存的数据量较大时,本地缓存可能无法满足需求。
扩展性不足:本地缓存的存储能力无法随着应用程序的扩展而扩展。在应用程序需要处理更多数据或更多用户请求时,本地缓存可能成为性能瓶颈。
3.持久化与可靠性
数据易丢失:本地缓存的数据存储在应用程序进程的内存空间中,因此当应用程序进程重启或崩溃时,本地缓存的数据会丢失。这可能导致数据不一致或丢失重要的缓存数据。
缺乏持久化机制:与分布式缓存相比,本地缓存通常没有内置的持久化机制来将数据持久化到磁盘或其他存储介质上。这意味着在应用程序重启或崩溃后,无法恢复之前缓存的数据。
二、Caffeine使用教程
1.基础版
①引入依赖
首先,需要在项目中引入Caffeine的依赖。如果使用Maven作为构建工具,可以在pom.xml文件中添加以下依赖:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>适配的版本号</version>
</dependency>
注意:jdk11以上的用3.x版本,如果是1.8请用2.x版本。
②创建缓存实例
使用Caffeine库提供的API,可以创建一个缓存实例。在创建缓存实例时,可以指定缓存的最大容量、过期策略等参数。例如:
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100) // 设置缓存的最大条目数
.expireAfterWrite(10, TimeUnit.MINUTES) // 设置缓存项在写入后10分钟过期
.build();
③向缓存中添加数据
使用缓存实例的put方法,可以将数据添加到缓存中。例如:
cache.put("key1", "value1");
④获取缓存中的数据
使用缓存实例的get或getIfPresent方法,可以根据键获取缓存中的数据。get方法会在缓存未命中时尝试通过提供的函数加载数据,而getIfPresent方法则只会返回缓存中已存在的数据。例如:
// 获取缓存中已存在的数据
String value1 = cache.getIfPresent("key1");
// 缓存未命中时尝试加载数据
String value2 = cache.get("key2", key -> "defaultValue");
⑤缓存驱逐与过期
Caffeine会根据设置的过期策略或驱逐策略自动移除缓存中的数据。例如,当缓存项达到过期时间或缓存容量超过最大限制时,缓存项将被自动移除。
⑥统计与监听
Caffeine提供了缓存统计和监听功能,可以帮助开发者监控缓存的性能和状态。例如,可以通过recordStats方法开启缓存统计功能,并通过Cache.stats()方法获取当前缓存的统计指标。
LoadingCache<String, String> loadingCache = Caffeine.newBuilder()
.maximumSize(100)
.recordStats() // 开启缓存统计功能
.build(key -> "value");
// 获取缓存统计指标
CacheStats stats = loadingCache.stats();
System.out.println("缓存命中率: " + stats.hitRate());
⑦异步缓存
Caffeine还支持异步缓存,可以使用AsyncCache或AsyncLoadingCache来创建异步缓存实例。异步缓存的响应结果是一个CompletableFuture对象,可以在未来某个时间点获取缓存的结果。
⑧其他配置
Caffeine还提供了其他多种配置选项,如设置初始容量、弱引用、软引用等。这些配置可以根据实际需求进行选择和设置。
示例代码
以下是一个完整的示例代码,演示了如何使用Caffeine缓存管理器进行内存缓存的基本操作:
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
public class CaffeineCacheExample {
public static void main(String[] args) {
// 创建缓存实例
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
// 向缓存中添加数据
cache.put("key1", "value1");
// 获取缓存中的数据
String value1 = cache.getIfPresent("key1");
System.out.println("key1对应的值: " + value1);
// 缓存未命中时尝试加载数据
String value2 = cache.get("key2", key -> "defaultValue");
System.out.println("key2对应的值: " + value2);
// 缓存驱逐(手动移除缓存项)
cache.invalidate("key1");
value1 = cache.getIfPresent("key1");
System.out.println("key1对应的值(驱逐后): " + value1);
}
}
2.SpringBoot 集成 Caffeine(推荐)
①引入依赖
首先,需要在Spring Boot项目的pom.xml文件中添加Caffeine和Spring Boot缓存启动器的依赖:
<dependencies>
<!-- Caffeine依赖 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>适配的版本号</version>
</dependency>
<!-- Spring Boot缓存启动器依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
</dependencies>
注意:jdk11以上的用3.x版本,如果是1.8请用2.x版本。
②配置Caffeine缓存管理器
可以通过Java配置类来配置Caffeine缓存管理器。在配置类中,使用@Bean注解定义一个CacheManager,并将其配置为Caffeine缓存管理器,配置类上添加@EnableCaching注解,以启用Spring的缓存功能:
java
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("cache1", "cache2");
// 自定义缓存配置,例如:
cacheManager.getCaffeineCache("cache1").getCaffeine().maximumSize(100).expireAfterWrite(10, TimeUnit.MINUTES);
cacheManager.getCaffeineCache("cache2").getCaffeine().maximumSize(200).expireAfterAccess(15, TimeUnit.MINUTES);
// 或者直接设置CacheManager的默认配置
// Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
// .maximumSize(100)
// .expireAfterWrite(10, TimeUnit.MINUTES)
// .build();
// cacheManager.setCaffeine(caffeine);
return cacheManager;
}
}
注意:在上面的配置中,我们直接为CaffeineCacheManager指定了缓存名称,并分别设置了它们的Caffeine配置。另外,也可以通过设置CaffeineCacheManager的默认Caffeine配置来统一所有缓存的行为。
③使用缓存注解
在需要缓存的方法上使用Spring提供的缓存注解,如@Cacheable、@CachePut和@CacheEvict等。这些注解会告诉Spring框架在方法调用时如何进行缓存的读写操作。
例如:
java
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class MyService {
@Cacheable(value = "cache1", key = "#id")
public MyObject findById(Long id) {
// 方法实现,这里会进行数据库查询等操作
// 返回查询结果
}
@CachePut(value = "cache1", key = "#myObject.id")
public MyObject updateMyObject(MyObject myObject) {
// 方法实现,这里会更新数据库中的对象
// 返回更新后的对象
}
@CacheEvict(value = "cache1", key = "#id")
public void deleteById(Long id) {
// 方法实现,这里会从数据库中删除对象
// 删除缓存中的对象
}
}
在上面的示例中,findById方法被标记为@Cacheable,表示其返回值将被缓存。当同一个方法再次被调用时,如果缓存中存在对应的值,则直接返回缓存中的值,而不再执行方法体中的代码。updateMyObject方法被标记为@CachePut,表示在更新数据库中的对象后,将更新后的对象放入缓存中。deleteById方法被标记为@CacheEvict,表示在删除数据库中的对象后,从缓存中移除对应的值。
④配置属性(可选)
除了通过Java配置类来配置Caffeine缓存外,还可以通过Spring Boot的配置文件(如application.properties或application.yml)来配置Caffeine缓存的属性。例如:
(1)properties文件
# 缓存名称
spring.cache.cache-names=cache1,cache2
# cache1的配置
spring.cache.caffeine.cache1.maximum-size=100
spring.cache.caffeine.cache1.expire-after-write=10m
# cache2的配置
spring.cache.caffeine.cache2.maximum-size=200
spring.cache.caffeine.cache2.expire-after-access=15m
(2)yaml文件
spring:
cache:
cache-names: cache1, cache2
caffeine:
cache1:
maximum-size: 100
expire-after-write: 10m
cache2:
maximum-size: 200
expire-after-access: 15m
3.实战案例
1.场景如下:
某大屏统计功能,不需要实时统计,需要在第一次打开的时候查询速度很快。
2.针对这个场景,由于数据的实时性要求不高,可以使用Caffeine来做缓存,并通过定时任务在半夜将每日的缓存刷新。使用Cacheable可以实现有缓存时查询缓存,没缓存时进行插入。如果对数据持久性有要求,可以在系统重启的时候也调用这个定时任务。
3.代码实例
①引入Caffeine和SpringBoot Cache依赖,我这里是jdk1.8,所以用的2.x的版本
<!-- Spring Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>2.7.0</version>
</dependency>
<!-- Caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
②创建配置类
package org.jxzx.phjr.api.boot.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class CacheConfig {
/**
* Caffeine配置说明:
* initialCapacity=[integer]: 初始的缓存空间大小
* maximumSize=[long]: 缓存的最大条数
* maximumWeight=[long]: 缓存的最大权重
* expireAfterAccess=[duration]: 最后一次写入或访问后经过固定时间过期
* expireAfterWrite=[duration]: 最后一次写入后经过固定时间过期
* refreshAfterWrite=[duration]: 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
* weakKeys: 打开key的弱引用
* weakValues:打开value的弱引用
* softValues:打开value的软引用
* recordStats:开发统计功能
* 注意:
* expireAfterWrite和expireAfterAccess同事存在时,以expireAfterWrite为准。
* maximumSize和maximumWeight不可以同时使用
* weakValues和softValues不可以同时使用
*/
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<CaffeineCache> list = new ArrayList<>();
List<String> cacheList = Arrays.asList("缓存1","缓存2");
//添加自定义的缓存
cacheList.forEach(cacheName->{
list.add(new CaffeineCache(cacheName,
Caffeine.newBuilder()
.initialCapacity(50)
.maximumSize(1000)
//过期时间一天
.expireAfterAccess(60*60*24, TimeUnit.SECONDS)
.build()));
});
cacheManager.setCaches(list);
return cacheManager;
}
}
⑤创建定时任务Task类,添加一个定时任务,调用原有的方法。由于Controller里面的方法有参数和一些特定的注解,而且里面可能根据参数走了不同的service方法,所以最好不要直接调用Controller里面的方法,可以将里面的方法复制到TaskHelper类,然后Controller和Task同时调用这个TaskHelper的方法即可。
(1)Controller代码(被注释的是原代码,没注释的是新代码)
/*@Resource
private XXXService service1;*/
@Resource
private XXXTaskHelper helper;
/*@PostMapping("/getData1")
public Object getData1(@RequestBody Req req1) {
return service1.getData1(req1);
}*/
@PostMapping("/getData1")
public Object getData1(Req req1) {
return helper.getData1(req1);
}
/*@PostMapping("/getData2")
public Object getData2(@RequestBody Req req2) {
return service1.getData2(req2);
}*/
@PostMapping("/getData2")
public Object getData1(Req req2) {
return helper.getData1(req2);
}
(2)TaskHelper代码,这里使用的Cacheable注解,并且key里面需要包含尽可能多的属性,这样才能保证唯一性,value的值要跟上面的config一致
@Resource
private XXXService service1;
@Cacheable(value = "缓存1",key = "#Req1.属性1+ '_' +#Req1.属性2")
public Object getData1(Req1 req1) {
return service1.getData1(req1);
}
@Cacheable(value = "缓存2",key = "#Req2.属性1+ '_' +#Req2.属性2")
public Object getData2(Req2 req2) {
return service1.getData2(req2);
}
(3)Task代码
@Resource
private XXXTaskHelper helper;
@Resource
private CacheManager cacheManager;
//每日三点刷新
@Scheduled(cron = "0 0 3 * * ?")
public void () {
//清空缓存
cacheManager.getCacheNames().forEach(cacheName -> {
if("缓存1".equals(cacheName) || "缓存2".equals(cacheName)){
Cache<Object, Object> cache = (Cache<Object, Object>) cacheManager.getCache(cacheName).getNativeCache();
cache.invalidateAll();
}
});
//省略了模拟入参Req1/Req2的代码
helper.getData1(new Req1());
helper.getData2(new Req2());
}
2922






