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

值得深思!!!

重点

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

相关推荐
Asthenia041241 分钟前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz9651 小时前
ovs patch port 对比 veth pair
后端
Asthenia04121 小时前
Java受检异常与非受检异常分析
后端
uhakadotcom1 小时前
快速开始使用 n8n
后端·面试·github
JavaGuide1 小时前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9652 小时前
qemu 网络使用基础
后端
Asthenia04122 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04122 小时前
Spring 启动流程:比喻表达
后端
Asthenia04123 小时前
Spring 启动流程分析-含时序图
后端
ONE_Gua3 小时前
chromium魔改——CDP(Chrome DevTools Protocol)检测01
前端·后端·爬虫