SpringBoot 整合 Caffeine 本地缓存

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 为偶数满足条件时,才写入缓存,测试一下:

  1. 传参 age :24 调用两次,看控制台结果

是偶数,只打印了一次,说明满足条件,第一次调用已经将结果缓存起来了。

  1. 传参 age :23 调用两次,看控制台结果

不是偶数,不满足条件,所以不会将结果缓存起来,每次调用都会执行方法体。

unless 表达式:

unless = "#age % 2 == 0"

意思是当参数 age 为奇数不满足条件时,才写入缓存,测试一下:

  1. 传参 age :24 调用两次,看控制台结果

不是奇数,所以满足条件,不写入缓存,每次调用都执行方法体。

  1. 传参 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 作为缓存实现,通常不同的数据缓存时间是不同的,那么这个失效时间如何实现?

值得深思!!!

重点

从缓存中拿到的对象引用,不能直接修改,会影响缓存中的数据,要深拷贝一个对象引用出来再进行其他操作!

相关推荐
handsomestWei14 分钟前
java实现多图合成mp4和视频附件下载
java·开发语言·音视频·wutool·图片合成视频·视频附件下载
全栈若城25 分钟前
03 Python字符串与基础操作详解
java·开发语言·python
伯牙碎琴36 分钟前
二、Spring Framework基础:IoC(控制反转)和DI(依赖注入)
java·spring·log4j
菲力蒲LY39 分钟前
输入搜索、分组展示选项、下拉选取,全局跳转页,el-select 实现 —— 后端数据处理代码,抛砖引玉展思路
java·前端·mybatis
南宫生1 小时前
力扣每日一题【算法学习day.130】
java·学习·算法·leetcode
!!!5251 小时前
Java实现斗地主-做牌以及对牌排序
java·算法
我要最优解1 小时前
关于在mac中配置Java系统环境变量
java·flutter·macos
二十七剑1 小时前
jvm调试和查看工具
java·linux·jvm
过客猫20221 小时前
使用 deepseek实现 go语言,读取文本文件的功能,要求支持 ascii,utf-8 等多种格式自适应
开发语言·后端·golang
刘立军1 小时前
本地大模型编程实战(20)用langgraph和智能体实现RAG(Retrieval Augmented Generation,检索增强生成)(4)
人工智能·后端·llm