【SpringBoot实现两级缓存】
spring boot中使用Caffeine + Redis实现二级缓存
1.依赖准备
首先确认Caffeine和redis这两者的依赖已导入(springboot版本为2.4.0):
<!-- redis与caffeine结合使用构成多级缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
2.配置文件(application.yml部分)
server:
port: 8087
spring:
main:
allow-bean-definition-overriding: true
datasource:
url: jdbc:mysql://123.45.67.890:3306/arknights?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
username: root
password: yourpassword
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: 123.45.67.890
port: ${SPRING_REDIS_PORT:6379}
password: ${SPRING_REDIS_PASSWORD:saria}
database: ${SPRING_REDIS_DATABASE:2}
jedis:
pool:
max-active: ${SPRING_REDIS_POOL_MAX_ACTIVE:50}
max-idle: ${SPRING_REDIS_POOL_MAX_IDLE:50}
max-wait: ${SPRING_REDIS_POOL_MAX_WAIT:5000}
3.代码层
下面假设有个查询接口是对资产风险预警结果表的查询,然后我们逐步看:
本地缓存配置类:CacheManager.java, 这里面初始化cache管理器(有10s,60s,600s过期三种cache配置),设置过期时间和单位。
/**
* 缓存管理配置
* @author zoe
*/
@Slf4j
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager localCacheManager() {
log.info("缓存实例化:no Primary");
return initCacheManager();
}
private CacheManager initCacheManager() {
SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
// 把各个cache注册到cacheManager中,Caffeine实现了org.springframework.cache.Cache接口
simpleCacheManager.setCaches(Arrays.asList(
// CacheBuilder构建多个cache
new CaffeineCache(
// 定义cache名称:@Cacheable的cacheNames(等价value)属性要和此对应
"TIMEOUT_10S",
Caffeine.newBuilder()
// 设置缓存大小上限
.maximumSize(20000)
//参数:过期时长、单位
.expireAfterWrite(Duration.ofSeconds(10))
.build()
),
new CaffeineCache(
"TIMEOUT_60S",
Caffeine.newBuilder()
.maximumSize(20000)
.expireAfterWrite(Duration.ofMinutes(1))
.build()
),
new CaffeineCache(
"TIMEOUT_600S",
Caffeine.newBuilder()
.maximumSize(20000)
.expireAfterWrite(Duration.ofMinutes(10))
.build()
)
));
return simpleCacheManager;
}
}
接下来是业务方法对应的接口:
public interface WarningResultService {
/**
* 查询预警结果
*
* @param warningResult
* @return
*/
WarningResult queryWarningResult(WarningResult warningResult);
/**
* 保存
*
* @param warningResult
* @param key
*/
void saveWarningResult(WarningResult warningResult, String key);
}
然后是业务接口实现类:
@Service
public class WarningResultServiceImpl implements WarningResultService {
public static final Logger LOGGER = LoggerFactory.getLogger(WarningResultServiceImpl.class);
private static final String cacheKey = "zoe:asset:";
@Autowired
private StringRedisTemplate redisTemplate;
@Cacheable(cacheNames = "TIMEOUT_60S", cacheManager = "localCacheManager", key = "'zoe:asset:' + #warningResult.getWhichYear()+ #warningResult.getWhichMonth()")
@Override
public WarningResult queryWarningResult(WarningResult warningResult) {
//判断redis中是否存在key为asset:result的数据
int whichYear = warningResult.getWhichYear();
int whichMonth = warningResult.getWhichMonth();
String key = cacheKey + whichYear + whichMonth;
// hasKey(key):判断是否有key所对应的值,有则返回true,没有则返回false
if (!redisTemplate.hasKey(key)) {
// 缓存中没有key则新增
saveWarningResult(warningResult, key);
} else {
LOGGER.info("redis缓存命中");
}
String s = redisTemplate.opsForValue().get(key);
LOGGER.info("数据返回给客户端");
return JSONObject.parseObject(s, WarningResult.class);
}
@Override
public void saveWarningResult(WarningResult warningResult, String key) {
WarningResult returnDto = new WarningResult();
int whichMonth = warningResult.getWhichMonth();
if (!(whichMonth >= 0 && whichMonth <= 12)) {
LOGGER.error("当前输入查询月份有误!");
return;
}
LOGGER.info("redis未命中,查询mysql");
// 模拟从数据库里获取数据封装DTO
// 本月零金额或小额资产数
returnDto.setThisMonthSmallCount(3000);
// 本月新增资产数
returnDto.setThisMonthSumCount(500);
// 保存数据到缓存中
// 调用fastJSON工具包进行序列化为JSONString格式
String result = JSONObject.toJSONString(returnDto);
// 设置360秒过期
redisTemplate.opsForValue().set(key, result, 360, TimeUnit.SECONDS);
LOGGER.info("更新二级缓存redis");
}
}
controller方法:
@ApiOperation(value = "查询预警结果(利用二级缓存)")
@GetMapping("/warning-result-test")
public WarningResult getWarningResultByMultiCache(WarningResult warningResult) {
return assetsWarningResultService.queryWarningResult(warningResult);
}
其实到这里就已经有二级缓存了,不过还是添加接口耗时打印看起来直观一些:利用AOP思想获取时间戳并相减可以获得时长。
耗时获取方法参考:https://www.cnblogs.com/architectforest/p/13357072.html
@Component
@Aspect
@Order(1)
public class CostTimeAspect {
ThreadLocal<Long> startTime = new ThreadLocal<>();
// 切点为刚刚的controller路径
@Pointcut("execution(public * com.arknights.bot.api.controller.*.*(..))")
private void pointcut() {}
@Before("pointcut()")
public void doBefore(JoinPoint joinPoint) throws Throwable{
startTime.set(System.currentTimeMillis());
}
@AfterReturning(returning = "result" , pointcut = "pointcut()")
public void doAfterReturning(Object result){
System.out.println("aop前后方法耗时:毫秒数:"+ (System.currentTimeMillis() - startTime.get()));
}
}
4.执行请求
http://localhost:8087/v1/risk-report/warning-result-test?whichMonth=6&whichYear=2023
返回结果(单次请求):
{
"whichMonth": 0,
"whichYear": 0,
"thisMonthSmallCount": 3000,
"thisMonthSumCount": 500
}
5.控制台打印
2023-07-31 12:57:32.419 INFO 24900 --- [nio-8087-exec-2] a.b.a.s.i.AssetsWarningResultServiceImpl : redis未命中,查询mysql
2023-07-31 12:57:32.490 INFO 24900 --- [nio-8087-exec-2] a.b.a.s.i.AssetsWarningResultServiceImpl : 更新二级缓存redis
2023-07-31 12:57:32.523 INFO 24900 --- [nio-8087-exec-2] a.b.a.s.i.AssetsWarningResultServiceImpl : 数据返回给客户端
aop前后方法耗时:毫秒数:1203
aop前后方法耗时:毫秒数:1
2023-07-31 12:59:05.264 INFO 24900 --- [nio-8087-exec-5] a.b.a.s.i.AssetsWarningResultServiceImpl : redis缓存命中
2023-07-31 12:59:05.296 INFO 24900 --- [nio-8087-exec-5] a.b.a.s.i.AssetsWarningResultServiceImpl : 数据返回给客户端
aop前后方法耗时:毫秒数:67
6.小结
根据打印信息可以看到一共有三次请求,第一次请求调用没有走缓存,走的数据库查询,耗时1203ms;第二次直接走本地缓存,耗时1ms;根据之前设置,本地缓存60秒过期,redis360秒过期,所以一分钟后我进行第三次请求,则一级缓存未命中,走redis,然后返回,并更新一级缓存,耗时67ms。
总结为:当用户获取数据时,先从一级缓存中获取数据,如果一级缓存有数据则返回数据,否则从二级缓存中获取数据。如果二级缓存中有数据则更新一级缓存,然后将数据返回客户端。如果二级缓存没有数据则去数据库查询数据,然后更新二级缓存,接着再更新一级缓存,最后将数据返回给客户端。
思考:缓存的存在是为了优化程序响应时长,并不是用的越多越好,要根据实际业务需求取舍。