Caffeine 简介
- Caffeine 是基于 Java 8 的高性能缓存库,并且在 Spring5(SpringBoot 2.x)后 Spring 官方放弃了 Guava,转而使用了性能更加优秀的 Caffeine 作为默认缓存组件
- 支持异步加载和事件提交队列
- 内部使用 W-TinyLFU 算法,命中率非常高,内存占用更加的小
- 一般在 Redis 之后,作为二级缓存
Caffeine 可结合 Spring 的 @Cacheable
注解作为方法级别的缓存使用,但是 Spring 的缓存注解的底层实现不限于 Caffeine,也可以是 Redis、Memcache、Guava 等。
本文我们主要讲解 @Cacheable
结合 Caffeine 的缓存的使用姿势。
Spring 提供了四个常用的缓存注解:
@Cacheable
:缓存存在,则使用缓存;不存在,则执行方法体,并将结果塞入缓存中@CacheEvit
:失效缓存@CachePut
:更新缓存@Caching
:可组合多个注解一起使用
重点讲解@Cacheable
。
核心配置
开启 Spring 开启注解缓存依赖:
xml
<!-- spring的@Cacheable相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
上述说了,Spring 缓存注解是结合底层具体的缓存实现使用的,本文选择 Caffeine 所以还需要添加以下依赖:
xml
<!-- caffeine 缓存使用姿势 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
注册缓存管理器:
java
/**
* 定义缓存管理器,配合Spring的 @Cache 来使用
*
* @return {@link CacheManager}
*/
@Bean("caffeineCacheManager")
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
// 设置过期时间,写入后5分钟过期
.expireAfterWrite(5, TimeUnit.MINUTES)
// 初始化缓存空间大小
.initialCapacity(100)
// 最大的缓存条数
.maximumSize(200)
);
return cacheManager;
}
上述是通过注册 Bean 的方式实现缓存管理器,也可以通过配置文件的形式实现,如下:
yaml
spring:
cache:
# 底层缓存使用 Caffeine 实现
type: caffeine
caffeine:
# 初始化缓存空间容量100,最大缓存容量200个,写入数据5分钟后失效
spec: initialCapacity=100,maximumSize=200,expireAfterWrite=5m
之所以没有使用配置的方式实现缓存管理器,是因为后续要结合 Spring 提供的缓存注解配合使用,注解可以指定使用不同的底层缓存:Redis、Memcache、Guava 等。
使用 Caffeine
第一步,要在项目的启动入口,添加 @EnableCaching
注解,不添加此注解,后续的缓存注解都不会生效。

@Cacheable
使用姿势
此注解用于修饰类或者方法,如果某个类中的方法使用同一个类型的缓存管理器,可以将此注解直接标注在当前类上,这样类中所有的标注注解的方法都使用同一个缓存管理器。
此注解作用,当我们访问它修饰的方法时,优先从缓存中取,如果有缓存则直接取缓存;缓存不存在,则执行方法体,并将结果写入到缓存中。
此注解有三个核心属性:
- cacheNames :可以简单理解为是缓存的前缀,例如:home::articleId_111,其中
home
就是当前属性值 - key:SpEL 表达式,可以基于方法参数来生成对应的 key,若是常量字符串,需要用单引号包裹
- cacheManager:指定配置类缓存管理器(根据业务场景可以选择不同的缓存实现配置)
less
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
@AliasFor("cacheNames")
String[] value() default {};
@AliasFor("value")
String[] cacheNames() default {};
String key() default "";
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
String condition() default "";
String unless() default "";
boolean sync() default false;
}
使用示例:
typescript
/**
* cacheNames:类似于缓存前缀
* key:键
* cacheManager:缓存管理器
* 最终的缓存key样式:demo::name_name
*
* @param name
* @return {@link String}
*/
@Cacheable(cacheNames = "demo", key = "'name_' + #name", cacheManager = "caffeineCacheManager")
public String testCacheable1(String name) {
String content = "[Caffeine] hello " + name + "!";
System.out.println("新增缓存:" + content);
return content;
}
传参 name:一宿君
对应的缓存 key:demo::name_一宿君
接口第一次调用,控制台打印:
新增缓存:[Caffeine] hello 一宿君!
返回结果:[Caffeine] hello 一宿君!
接口在有效期内调用,控制台不会打印内容,直接返回结果:[Caffeine] hello 一宿君!
因为第一次调用成功,已经将结果集写入缓存中,同样的条件参数再次调用,都会优先从缓存中查询。
直到缓存失效,再次调用会执行方法体逻辑,并将结果再次填写入缓存。
@CachePut
使用姿势
更新缓存注解:
typescript
/**
* cacheNames:类似于缓存前缀
* key:键
* cacheManager:缓存管理器
* 最终的缓存key样式:demo::name_name
*
* @param name
* @return {@link String}
*/
@CachePut(cacheNames = "demo", key = "'name_' + #name", cacheManager = "caffeineCacheManager")
public String testCachePut1(String name) {
String content = "[Caffeine] hello 一宿君2!";
System.out.println("更新缓存:" + content);
return content;
}
注意上述字符串时写死的

传参 name:一宿君
【此处是为了保证和上述缓存的 key 保持一致】
对应的缓存 key:demo::name_一宿君
接口第一次调用,控制台打印:
更新缓存:[Caffeine] hello 一宿君2!
返回结果:[Caffeine] hello 一宿君2!
上述第一次缓存的内容为:[Caffeine] hello 一宿君!
两者内容不同 一宿君2
,所以这次的调用会将结果更新到同一个 key 的 value 中。
此时再次调用第一个方法:testCacheable1
控制台没有打印任何内容,直接返回结果:[Caffeine] hello 一宿君2!


@CacheEvict
使用姿势
失效缓存注解:
typescript
/**
* cacheNames:类似于缓存前缀
* key:键
* cacheManager:缓存管理器
* 最终的缓存key样式:demo::name_name
*
* @param name
* @return {@link String}
*/
@CachePut(cacheNames = "demo", key = "'name_' + #name", cacheManager = "caffeineCacheManager")
public String testCacheEvict1(String name) {
String content = "[Caffeine] hello " + name + "!";
System.out.println("失效缓存:" + content);
return content;
}
第一步:先执行一次方法:testCacheable1
,将结果缓存起来。

第二步:执行方法:testCacheEvict1
,将上述方法的缓存主动失效。

此时再次调用方法testCacheEvict1
,看控制台

说明同样的传参,调用失效方法成功将缓存中同样的 key:demo::name_一宿君
失效了,所以再次调用时需要重新执行方法体,将结果再次写入缓存中。
@Caching
使用姿势
在实际的工作中,经常会遇到一个数据变动,需要更新其他多个缓存的场景,对于这个场景,可以通过 @Caching
组合注解来实现
less
@Caching(cacheable = @Cacheable(cacheNames = "demo", key = "'name_' + #name", cacheManager = "caffeineCacheManager"),
evict = @CacheEvict(cacheNames = "condition", key = "'age_24'", cacheManager = "caffeineCacheManager"),
put = @CachePut(cacheNames = "unless", key = "'age_23'", cacheManager = "caffeineCacheManager"))
public String testCaching(String name) {
String content = "[Caffeine] Caching name=" + name + "!";
System.out.println("组合缓存:" + content);
return content;
}
当前注解中包含了三个注解:
- @Cacheable(cacheNames = "demo", key = "'name_' + #name", cacheManager = "caffeineCacheManager"),缓存
demo::name_#name
- @CacheEvict(cacheNames = "condition", key = "'age_24'", cacheManager = "caffeineCacheManager"),失效
condition::age_24
- @CachePut(cacheNames = "unless", key = "'age_23'", cacheManager = "caffeineCacheManager"),更新
unless::age_23

左侧这个标识,可以跳转到关联的同样条件的代码中,同等条件的缓存都会收到影响,失效的失效,更新的更新!
注解条件
使用姿势
细心的小伙伴可能发现了 @Cachable
有两个属性:
- condition:支持SpEL,表达式满足 condition 条件才写入缓存
- unless:支持SpEL,表达式满足 unless 条件才写入缓存
举个栗子:
typescript
@Cacheable(cacheNames = "condition", key = "'age_' + #age", condition = "#age % 2 == 0", cacheManager = "caffeineCacheManager")
public String testCacheable2(Integer age) {
String content = "[Caffeine] condition " + age + "!";
System.out.println("新增缓存:" + content);
return content;
}
condition 表达式:
condition = "#age % 2 == 0"
意思是当参数 age 为偶数满足条件时,才写入缓存,测试一下:
- 传参 age :24 调用两次,看控制台结果

是偶数,只打印了一次,说明满足条件,第一次调用已经将结果缓存起来了。
- 传参 age :23 调用两次,看控制台结果

不是偶数,不满足条件,所以不会将结果缓存起来,每次调用都会执行方法体。
unless 表达式:
unless = "#age % 2 == 0"
意思是当参数 age 为奇数不满足条件时,才写入缓存,测试一下:
- 传参 age :24 调用两次,看控制台结果

不是奇数,所以满足条件,不写入缓存,每次调用都执行方法体。
- 传参 age :23 调用两次,看控制台结果

是奇数,说明满足条件,第一次调用已经将结果缓存起来了。
异常时,缓存会怎样?
less
@Cacheable(cacheNames = "exception", key = "#age")
@CacheEvict(cacheNames = "demo", key = "'一宿君'")
@CachePut(cacheNames = "demo", key = "'一宿君2'")
public Integer exception(Integer age) {
return 100 / age;
}
传参 age:0,发生异常,上述三个注解都不会生效!!!
小结
总结知识点
Spring 提供了四个常用的缓存注解:
@Cacheable
:缓存存在,则使用缓存;不存在,则执行方法体,并将结果塞入缓存中@CacheEvit
:失效缓存@CachePut
:更新缓存@Caching
:可组合多个注解一起使用
扩展知识点
上述说了,如果同一个类中所有的方法使用的缓存管理器和缓存前缀都相同,则可以直接在类上标注如下注解:
@CacheConfig(cacheNames = "customCache", cacheManager = "caffeineCacheManager")
less
@Service
@CacheConfig(cacheNames = "customCache", cacheManager = "caffeineCacheManager")
public class CaffeineDemoServiceImpl implements CaffeineDemoService {
// 省略
}
标注这个直接之后,下面所有的方法就不需要再指定缓存管理器了,只需要指定对应的 key 就可以了!
扩展问题
上述场景虽然说可以满足大部分的使用场景了,但是有一个非常重要的点没有说明,缓存时间怎么设置?
本文是使用 Caffeine 作为缓存实现,在配置类中已经写死了缓存失效时间,不够灵活。
如果底层选择 Redis 作为缓存实现,通常不同的数据缓存时间是不同的,那么这个失效时间如何实现?
值得深思!!!
重点
从缓存中拿到的对象引用,不能直接修改,会影响缓存中的数据,要深拷贝一个对象引用出来再进行其他操作!