Pre
缓存 - Caffeine 不完全指北
名词解释
@Cacheable:表示该方法支持缓存。当调用被注解的方法时,如果对应的键已经存在缓存,则不再执行方法体,而从缓存中直接返回。当方法返回null时,将不进行缓存操作。
@CachePut:表示执行该方法后,其值将作为最新结果更新到缓存中,每次都会执行该方法。
@CacheEvict:表示执行该方法后,将触发缓存清除操作。
@Caching:用于组合前三个注解,比如
代码语言:javascript复制@Caching(cacheable = @Cacheable("CacheConstants.GET_USER"),
evict = {@CacheEvict("CacheConstants.GET_DYNAMIC",allEntries = true)}
public User find(Integer id) {
return null;
}
注解属性
- cacheNames/value:缓存组件的名字,即cacheManager中缓存的名称。
- key:缓存数据时使用的key。默认使用方法参数值,也可以使用SpEL表达式进行编写。
- keyGenerator:和key二选一使用。
- cacheManager:指定使用的缓存管理器。
- condition:在方法执行开始前检查,在符合condition的情况下,进行缓存
- unless:在方法执行完成后检查,在符合unless的情况下,不进行缓存
- sync:是否使用同步模式。若使用同步模式,在多个线程同时对一个key进行load时,其他线程将被阻塞。
sync开启或关闭,在Cache和LoadingCache中的表现是不一致的:
- Cache中,sync表示是否需要所有线程同步等待
- LoadingCache中,sync表示在读取不存在/已驱逐的key时,是否执行被注解方法
指导步骤
要在Spring Boot中整合Caffeine缓存,可以按照以下步骤进行操作:
步骤 1:添加依赖
在pom.xml
文件中添加Caffeine依赖项。确保选择与您的Spring Boot版本兼容的Caffeine版本。以下是一个示例依赖项:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.0</version>
</dependency>
步骤 2:配置缓存
在Spring Boot的配置文件(例如application.properties
或application.yml
)中添加Caffeine缓存的配置。以下是一个示例配置:
application.properties:
代码语言:javascript复制spring.cache.type=caffeine
spring.cache.cache-names=myCache
spring.cache.caffeine.spec=maximumSize=100,expireAfterAccess=600s
application.yml:
代码语言:javascript复制spring:
cache:
type: caffeine
cache-names: myCache
caffeine:
spec: maximumSize=100,expireAfterAccess=600s
这将配置一个名为myCache
的Caffeine缓存,最大容量为100,访问后在600秒内过期。
步骤 3:使用缓存
在需要使用缓存的地方,使用@Cacheable
注解标记方法。例如,以下是一个使用缓存的示例:
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class MyService {
@Cacheable("myCache")
public String getDataFromCache(String key) {
// 如果缓存中存在数据,则直接返回
// 如果缓存中不存在数据,则执行相应的业务逻辑,并将结果放入缓存
return fetchDataFromDatabase(key);
}
private String fetchDataFromDatabase(String key) {
// 执行获取数据的业务逻辑
return "Data for key: " key;
}
}
在上面的示例中,getDataFromCache
方法使用了@Cacheable("myCache")
注解,表示该方法的结果将被缓存到名为myCache
的缓存中。
现在,当调用getDataFromCache
方法时,首先会检查缓存中是否存在与给定参数对应的数据。如果存在,将直接返回缓存的数据;如果不存在,则会执行方法体内的业务逻辑,并将结果放入缓存。
这就是在Spring Boot中整合Caffeine缓存的基本步骤。我们可以根据自己的需求进行进一步的配置和定制。
Code
接下来我们使用另一种方式来实现
pom
代码语言:javascript复制<?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 https://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.7.13</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.artisan</groupId>
<artifactId>caffeine-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>caffeine-demo</name>
<description>caffeine-demo</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.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<!-- Java 8 users can continue to use version 2.x, which will be supported -->
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Config
代码语言:javascript复制package com.artisan.caffeinedemo.spring.config;
import com.artisan.caffeinedemo.spring.enums.CacheEnum;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author 小工匠
* @version 1.0
* @mark: show me the code , change the world
*/
@Configuration
@EnableCaching
public class CacheConfig {
/**
* Caffeine配置说明:
* initialCapacity=[integer]: 初始的缓存空间大小
* maximumSize=[long]: 缓存的最大条数
* maximumWeight=[long]: 缓存的最大权重
* expireAfterAccess=[duration]: 最后一次写入或访问后经过固定时间过期
* expireAfterWrite=[duration]: 最后一次写入后经过固定时间过期
* refreshAfterWrite=[duration]: 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
* weakKeys: 打开key的弱引用
* weakValues:打开value的弱引用
* softValues:打开value的软引用
* recordStats:开发统计功能
* 注意:
* expireAfterWrite和expireAfterAccess同事存在时,以expireAfterWrite为准。
* maximumSize和maximumWeight不可以同时使用
* weakValues和softValues不可以同时使用
*/
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<CaffeineCache> list = new ArrayList<>();
//循环添加枚举类中自定义的缓存,可以自定义
for (CacheEnum cacheEnum : CacheEnum.values()) {
list.add(new CaffeineCache(cacheEnum.getName(),
Caffeine.newBuilder()
.initialCapacity(50)
.maximumSize(1000)
.expireAfterAccess(cacheEnum.getExpireTime(), TimeUnit.SECONDS)
.build()));
}
cacheManager.setCaches(list);
return cacheManager;
}
}
Service
代码语言:javascript复制package com.artisan.caffeinedemo.spring.service;
import com.artisan.caffeinedemo.spring.constants.CacheConstants;
import com.artisan.caffeinedemo.spring.domains.Artisan;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
/**
* @author 小工匠
* @version 1.0
* @mark: show me the code , change the world
*/
@Service
@Slf4j
public class ArtisanService {
/**
* @Cacheable注解是 通过 Spring AOP机制进行的,因此类内的调用将无法触发缓存操作,必须由外部进行调用,之前也算是踩了一遍坑,特别提醒一下
*/
/**
* cacheNames/value:缓存组件的名字,即cacheManager中缓存的名称。
* key:缓存数据时使用的key。默认使用方法参数值,也可以使用SpEL表达式进行编写。
* keyGenerator:和key二选一使用。
* cacheManager:指定使用的缓存管理器。
* condition:在方法执行开始前检查,在符合condition的情况下,进行缓存
* unless:在方法执行完成后检查,在符合unless的情况下,不进行缓存
* sync:是否使用同步模式。若使用同步模式,在多个线程同时对一个key进行load时,其他线程将被阻塞。
*/
@Cacheable(value = CacheConstants.GET_USER, key = "'user' #userId", sync = true)
public Artisan getUserByUserId(Integer userId) {
log.info("----------------触发DB查询----------------------------");
// 模拟从DB查询数据
Artisan artisan = Artisan.builder().id(userId).name("artisan-" userId).address("China-" userId).build();
return artisan;
}
@CacheEvict(value = CacheConstants.GET_USER, key = "'user' #userId")
public Integer cacheEvictTest(Integer userId) {
log.info("cacheEvictTest 清除 {} 对应的缓存", userId);
return 1;
}
@CachePut(value = CacheConstants.GET_USER, key = "'user' #userId")
public Artisan cachePut(Integer userId) {
log.info("cachePut execute -------------------");
Artisan artisan = Artisan.builder().id(userId).name("artisan1").address("China").build();
return artisan;
}
}
缓存名枚举 & 常量
代码语言:javascript复制package com.artisan.caffeinedemo.spring.enums;
import com.artisan.caffeinedemo.spring.constants.CacheConstants;
import lombok.Getter;
/**
* @author 小工匠
* @version 1.0
* @mark: show me the code , change the world
*/
@Getter
public enum CacheEnum {
MY_CACHE_1("工匠1", CacheConstants.DEFAULT_EXPIRES),
MY_CACHE_2("工匠2", CacheConstants.EXPIRES_5_MIN),
MY_CACHE_3(CacheConstants.GET_USER, CacheConstants.EXPIRES_10_MIN);
private String name;
private Integer expireTime;
CacheEnum(String name, Integer expireTime) {
this.name = name;
this.expireTime = expireTime;
}
}
代码语言:javascript复制package com.artisan.caffeinedemo.spring.constants;
/**
* @author 小工匠
* @version 1.0
* @mark: show me the code , change the world
*/
public class CacheConstants {
/**
* 默认过期时间(配置类中使用的时间单位是秒,所以这里如 3*60 为3分钟)
*/
public static final int DEFAULT_EXPIRES = 3 * 60;
public static final int EXPIRES_5_MIN = 5 * 60;
public static final int EXPIRES_10_MIN = 10 * 60;
public static final String GET_USER = "GET:USER";
public static final String GET_DYNAMIC = "GET:DYNAMIC";
}
创建缓存常量类,把公共的常量提取一层,复用,这里也可以通过配置文件加载这些数据,例如@ConfigurationProperties和@Value
代码语言:javascript复制package com.artisan.caffeinedemo.spring.domains;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* @author 小工匠
* @version 1.0
* @mark: show me the code , change the world
*/
@Slf4j
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Artisan {
private Integer id;
private String name;
private String address;
}
测试入口
代码语言:javascript复制package com.artisan.caffeinedemo.spring.controller;
import com.artisan.caffeinedemo.spring.domains.Artisan;
import com.artisan.caffeinedemo.spring.service.ArtisanService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* @author 小工匠
* @version 1.0
* @mark: show me the code , change the world
*/
@Slf4j
@RestController
@RequestMapping("/testCache")
public class CacheController {
@Resource
private ArtisanService artisanService;
@RequestMapping("/get/{id}")
@ResponseBody
public String getArtisanById(@PathVariable Integer id) {
Artisan artisan = artisanService.getUserByUserId(id);
log.info("--------->{}", artisan.toString());
return artisan.toString();
}
@RequestMapping("/evit/{id}")
@ResponseBody
public String cacheEvit(@PathVariable Integer id) {
artisanService.cacheEvictTest(id);
return "cacheEvit";
}
@RequestMapping("/put/{id}")
@ResponseBody
public String cachePut(@PathVariable Integer id) {
Artisan artisan = artisanService.cachePut(id);
return artisan.toString();
}
}
测试
根据以下的访问,可以得到如下结论
代码语言:javascript复制http://127.0.0.1:8080/testCache/get/1 -----> 查询DB ,加入缓存
http://127.0.0.1:8080/testCache/get/1 ------> 直接从缓存中读取
http://127.0.0.1:8080/testCache/evit/1 ------> 清除缓存的id=1的数据
http://127.0.0.1:8080/testCache/get/1 -----> 查询DB ,加入缓存
http://127.0.0.1:8080/testCache/put/1 -----> 操作DB ,加入缓存
http://127.0.0.1:8080/testCache/put/2 -----> 操作DB ,加入缓存
http://127.0.0.1:8080/testCache/get/2 ------> 直接从缓存中读取