在系统中, 有些数据, 访问十分频繁, 往往把这些数据放入分布式缓存中, 但为了减少网络传输, 加快响应速度, 缓存分布式缓存读压力, 会把这些数据缓存到本地 JVM 中, 大多是先取本地缓存中, 再取分布式缓存中的数据, Caffeine 是一个高性能 Java 缓存库, 使用 Java8 对 Guava 缓存重写版本, 在 Spring Boot 2.0 中将取代 Guava
本文讲解 SpringBoot 缓存注解的理论和整合 Caffeine 的基本使用
一. SpringBoot 缓存注解相关知识点
1. @Cacheable:
@Cacheable 可以标记在一个方法上, 也可以标记在一个类上. 当标记在一个方法上时表示该方法是支持缓存的, 当标记在一个类上时则表示该类所有的方法都是支持缓存的. 对于一个支持缓存的方法, Spring 会在其被调用后将其返回值缓存起来, 以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果, 而不需要再次执行该方法. Spring 在缓存方法的返回值时是以键值对进行缓存的, 值就是方法的返回结果, 至于键的话, Spring 又支持两种策略, 默认策略和自定义策略, 这个稍后会进行说明. 需要注意的是当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的.@Cacheable 可以指定三个属性, value,key 和 condition.
参数 | 解释 | 例子 |
---|---|---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | 例如:@Cacheable(value=”mycache”) |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如不指定,则按照方法所有参数组合 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 | @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
使用案例:
- // key 是指传入时的参数
- @Cacheable(value="users", key="#id")
- public Integer find(Integer id) {
- return id;
- }
- // 表示第一个参数
- @Cacheable(value="users", key="#p0")
- public Long find(Long id) {
- return id;
- }
- // 表示 User 中的 id 值
- @Cacheable(value="users", key="#user.id")
- public User find(User user) {
- return user;
- }
- // 表示第一个参数里的 id 属性值
- @Cacheable(value="users", key="#p0.id")
- public User find(User user) {
- return user;
- }
除了上面的案例使用方法, 还有以下几种:
属性名称 | 描述 | 示例 |
---|---|---|
methodName | 当前方法名 | #root.methodName |
method | 当前方法 | #root.method.name |
target | 当前被调用的对象 | #root.target |
targetClass | 当前被调用的对象的 class | #root.targetClass |
args | 当前方法参数组成的数组 | #root.args[0] |
caches | 当前被调用的方法使用的 Cache | #root.caches[0].name |
condition 属性指定发生的条件
有的时候我们可能并不希望缓存一个方法所有的返回结果. 通过 condition 属性可以实现这一功能. condition 属性默认为空, 表示将缓存所有的调用情形. 其值是通过 SpringEL 表达式来指定的, 当为 true 时表示进行缓存处理; 当为 false 时表示不进行缓存处理, 即每次调用该方法时该方法都会执行一次. 如下示例表示只有当 user 的 id 为偶数时才会进行缓存.
- // 根据条件判断是否缓存
- @Cacheable(value="users", key="#user.id", condition="#user.id%2==0")
- public User find(User user) {
- return user;
- }
- 2. CacheEvict
@CacheEvict 是用来标注在需要清除缓存元素的方法或类上的. 当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作.@CacheEvict 可以指定的属性有 value,key,condition,allEntries 和 beforeInvocation. 其中 value,key 和 condition 的语义与 @Cacheable 对应的属性类似. 即 value 表示清除操作是发生在哪些 Cache 上的 (对应 Cache 的名称);key 表示需要清除的是哪个 key, 如未指定则会使用默认策略生成的 key;condition 表示清除操作发生的条件. 下面我们来介绍一下新出现的两个属性 allEntries 和 beforeInvocation.
allEntries 属性
allEntries 是 boolean 类型, 表示是否需要清除缓存中的所有元素. 默认为 false, 表示不需要. 当指定了 allEntries 为 true 时, Spring Cache 将忽略指定的 key. 有的时候我们需要 Cache 一下清除所有的元素, 这比一个一个清除元素更有效率.
- @CacheEvict(value="user", allEntries=true)
- public void delete(Integer id) {
- System.out.println(id);
- }
beforeInvocation 属性
清除操作默认是在对应方法成功执行之后触发的, 即方法如果因为抛出异常而未能成功返回时也不会触发清除操作. 使用 beforeInvocation 可以改变触发清除操作的时间, 当我们指定该属性值为 true 时, Spring 会在调用该方法之前清除缓存中的指定元素.
- @CacheEvict(value="user", beforeInvocation=true)
- public void delete(Integer id) {
- System.out.println(id);
- }
- 3. @Caching
@Caching 注解可以让我们在一个方法或者类上同时指定多个 Spring Cache 相关的注解. 其拥有三个属性: cacheable,put 和 evict, 分别用于指定 @Cacheable,@CachePut 和 @CacheEvict.
- @Caching(
- cacheable = @Cacheable("user"),
- evict = {
- @CacheEvict(value = "user1", key = "#id"),
- @CacheEvict(value = "user", allEntries = true)})
- public Integer find(Integer id) {
- return id;
- }
4. 自定义注解
Spring 允许我们在配置可缓存的方法时使用自定义的注解, 前提是自定义的注解上必须使用对应的注解进行标注. 如我们有如下这么一个使用 @Cacheable 进行标注的自定义注解
二. Caffeine 相关知识点
Caffeine 常用配置说明:
initialCapacity=[integer]: 初始的缓存空间大小
maximumSize=[long]: 缓存的最大条数
maximumWeight=[long]: 缓存的最大权重
expireAfterAccess=[duration]: 最后一次写入或访问后经过固定时间过期
expireAfterWrite=[duration]: 最后一次写入后经过固定时间过期
refreshAfterWrite=[duration]: 创建缓存或者最近一次更新缓存后经过固定的时间间隔, 刷新缓存
注意点:
expireAfterWrite 和 expireAfterAccess 同事存在时, 以 expireAfterWrite 为准
maximumSize 和 maximumWeight 不可以同时使用
配置案例:
- spring:
- # 配置缓存, 初始缓存容量为 10, 最大容量为 200, 过期时间 (这里配置写入后过期时间为 3 秒)
- cache:
- type: caffeine
- caffeine:
- spec: initialCapacity=10,maximumSize=200,expireAfterWrite=3s
sa. SpringBoot 集成 Caffeine 简单 demo
1. pom 文件
- <?xml version="1.0" encoding="UTF-8"?>
- <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>2.1.6.RELEASE</version>
- <relativePath/> <!-- lookup parent from repository -->
- </parent>
- <groupId>com.gj</groupId>
- <artifactId>boot-cache-demo</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <name>boot-cache-demo</name>
- <description>Demo project for Spring Boot</description>
- <properties>
- <java.version>1.8</java.version>
- </properties>
- <dependencies>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <optional>true</optional>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-jpa</artifactId>
- </dependency>
- <dependency>
- <groupId>MySQL</groupId>
- <artifactId>MySQL-connector-java</artifactId>
- </dependency>
- <dependency>
- <groupId>cn.gjing</groupId>
- <artifactId>tools-common</artifactId>
- <version>1.0.4</version>
- </dependency>
- <dependency>
- <groupId>cn.gjing</groupId>
- <artifactId>tools-starter-swagger</artifactId>
- <version>1.0.9</version>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-cache</artifactId>
- </dependency>
- <dependency>
- <groupId>com.GitHub.ben-manes.caffeine</groupId>
- <artifactId>caffeine</artifactId>
- <version>2.7.0</version>
- </dependency>
- </dependencies>
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
- </project>
2. 配置文件
- server:
- port: 8080
- spring:
- application:
- name: springboot-cache-demo
- # 配置数据库信息和连接池
- datasource:
- url: jdbc:MySQL://127.0.0.1:3306/cache?characterEncoding=utf8&useSSL=false&serverTimezone=UTC
- password: root
- username: root
- driver-class-name: com.MySQL.cj.jdbc.Driver
- type: com.zaxxer.hikari.HikariDataSource
- hikari:
- minimum-idle: 1
- maximum-pool-size: 15
- idle-timeout: 30000
- connection-timeout: 20000
- # 开启 jpa 自动建表
- jpa:
- database: MySQL
- hibernate:
- ddl-auto: update
- database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
- # 配置缓存, 初始缓存容量, 最大容量, 过期时间 (这里配置写入后过期时间)
- cache:
- type: caffeine
- caffeine:
- spec: initialCapacity=10,maximumSize=200,expireAfterWrite=3s
- # 配置 controller 路径
- swagger:
- base-package: com.gj.Web
title: springboot 使用 caffeine 缓存
3. 启动类
- @SpringBootApplication
- @EnableSwagger
- @EnableJpaAuditing
- @EnableCaching
- public class BootCacheDemoApplication {
- public static void main(String[] args) {
- SpringApplication.run(BootCacheDemoApplication.class, args);
- }
- }
4. 定义一个实体
- /**
- * @author Gjing
- **/
- @Entity
- @Table(name = "custom")
- @Data
- @Builder
- @NoArgsConstructor
- @AllArgsConstructor
- @EntityListeners(AuditingEntityListener.class)
- public class Custom {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Integer id;
- @Column(name = "custom_name", columnDefinition = "varchar(20) not null comment'用户名'")
- private String customName;
- @Column(name = "custom_number", columnDefinition = "int not null comment'用户编号'")
- private Integer customNumber;
- @Column(name = "create_time", columnDefinition = "datetime")
- @CreatedDate
- private Date createTime;
- @Column(name = "update_time", columnDefinition = "datetime")
- @LastModifiedDate
- private Date updateTime;
- }
5. 定义持久层接口
- /**
- * @author Gjing
- **/
- @Repository
- public interface CustomRepository extends JpaRepository<Custom,Integer> {
- /**
- * 通过用户名查询
- * @param customName 用户名
- * @return 用户
- */
- Custom findByCustomName(String customName);
- }
6. 定义 service
- /**
- * @author Gjing
- **/
- @Service
- @Slf4j
- public class CustomService {
- @Resource
- private CustomRepository customRepository;
- /**
- * 获取一个用户
- *
- * @param customId 用户 id
- * @return custom
- */
- @Cacheable(value = "user", key = "#customId")
- public Custom getCustom(Integer customId) {
- log.warn("通过数据库去查询, 用户 id 为:{}", customId);
- return customRepository.findById(customId)
- .orElseThrow(() -> new UserNotFoundException("Users don't exist"));
- }
- @CacheEvict(value = "user", key = "#customId")
- public void deleteCustom(Integer customId) {
- Custom custom = customRepository.findById(customId)
- .orElseThrow(() -> new UserNotFoundException("Users don't exist"));
- customRepository.delete(custom);
- }
- public Boolean insertCustom(String customName) {
- Custom custom = customRepository.findByCustomName(customName);
- if (custom == null) {
- customRepository.save(Custom.builder()
- .customName(customName)
- .customNumber(Integer.valueOf(RandomUtil.generateNumber(6)))
- .build());
- return true;
- }
- return false;
- }
- }
7. 定义异常
- /**
- * @author Gjing
- **/
- public class UserNotFoundException extends RuntimeException{
- public UserNotFoundException(String message) {
- super(message);
- }
- }
- /**
- * @author Gjing
- **/
- @RestControllerAdvice
- class DemoExceptionHandler {
- @ExceptionHandler(UserNotFoundException.class)
- public ResponseEntity userNot(UserNotFoundException e) {
- return ResponseEntity.badRequest().body(ErrorResult.error(e.getMessage()));
- }
- }
8. 定义接口
- /**
- * @author Gjing
- **/
- @RestController
- public class CustomController {
- @Resource
- private CustomService customService;
- @PostMapping("/custom")
- @ApiOperation(value = "添加用户", httpMethod = "POST")
- @ApiImplicitParam(name = "customName", value = "用户名", required = true, dataType = "String", paramType = "Query")
- public ResponseEntity insertCustom(String customName) {
- Boolean insertCustom = customService.insertCustom(customName);
- if (insertCustom) {
- return ResponseEntity.ok("New successful");
- }
- return ResponseEntity.ok("Add failed, user already exists");
- }
- @GetMapping("/custom/{custom-id}")
- @ApiOperation(value = "查询指定用户", httpMethod = "GET")
- public ResponseEntity getCustom(@PathVariable("custom-id") Integer customId) {
- return ResponseEntity.ok(customService.getCustom(customId));
- }
- @DeleteMapping("/custom")
- @ApiOperation(value = "删除指定用户", httpMethod = "DELETE")
- @ApiImplicitParam(name = "customId", value = "用户 id", required = true, dataType = "int", paramType = "Query")
- public ResponseEntity deleteCustom(Integer customId) {
- customService.deleteCustom(customId);
- return ResponseEntity.ok("Delete successful");
- }
- }
启动后访问 http://localhost:8080/swagger-ui.html 即可测试, 第一次获取数据会从数据库中查询, 接下来会直接读取缓存直到缓存失效
Demo 源码地址: 点击前往
来源: https://yq.aliyun.com/articles/706596