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

值得深思!!!

重点

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

相关推荐
这孩子叫逆6 分钟前
Spring Boot项目的创建与使用
java·spring boot·后端
星星法术嗲人10 分钟前
【Java】—— 集合框架:Collections工具类的使用
java·开发语言
一丝晨光28 分钟前
C++、Ruby和JavaScript
java·开发语言·javascript·c++·python·c·ruby
天上掉下来个程小白31 分钟前
Stream流的中间方法
java·开发语言·windows
xujinwei_gingko42 分钟前
JAVA基础面试题汇总(持续更新)
java·开发语言
liuyang-neu43 分钟前
力扣 简单 110.平衡二叉树
java·算法·leetcode·深度优先
一丝晨光1 小时前
Java、PHP、ASP、JSP、Kotlin、.NET、Go
java·kotlin·go·php·.net·jsp·asp
罗曼蒂克在消亡1 小时前
2.3MyBatis——插件机制
java·mybatis·源码学习
_GR1 小时前
每日OJ题_牛客_牛牛冲钻五_模拟_C++_Java
java·数据结构·c++·算法·动态规划
coderWangbuer1 小时前
基于springboot的高校招生系统(含源码+sql+视频导入教程+文档+PPT)
spring boot·后端·sql