springboot 缓存框架Cache整合redis组成二级缓存
项目性能优化的解决方案除开硬件外的方案无非就是优化sql,减少sql 的执行时间,合理运用缓存让同样的请求和数据库之间的连接尽量减少,内存的处理速度肯定比直接查询数据库来的要快一些。今天就记录一下spring的缓存框架和redis形成二级缓存来优化查询效率,废话不多说直接上代码:
整体目录:
首先定义注解:缓存注解和删除注解
package com.example.test1.cache.aspect.annoation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
* @Author xx
* @Date 2024/6/27 11:12
* @Description: 数据缓存注解
* @Version 1.0
*/
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DateCachePut {
/**
* 模块名:区分模块下的不同功能key,key可能重复
*/
String module() default "";
/**
* 缓存的数据key
*/
String key() default "";
/**
* key生成
*/
String keyGenerator() default "DefaultKeyGenerate";
/**
* 过期时间,默认30
*/
long passTime() default 30;
/**
* 过期时间单位,默认:秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 是否出发条件,支持springEl表达式
*/
String condition() default "true";
}
package com.example.test1.cache.aspect.annoation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
* @Author xx
* @Date 2024/6/27 11:13
* @Description: 缓存删除注解
* @Version 1.0
*/
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataCacheEvict {
/**
* 模块名:区分模块下的不同功能key,key可能重复
*/
String module() default "";
/**
* 缓存的数据key
*/
String key() default "";
/**
* key生成
*/
String keyGenerator() default "DefaultKeyGenerate";
/**
* 删除时间,默认1
*/
long delay() default 1;
/**
* 过期时间单位,默认:秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 是否出发条件,支持springEl表达式
*/
String condition() default "true";
}
注解切面类
package com.example.test1.cache.aspect;
import com.example.test1.cache.aspect.annoation.DataCacheEvict;
import com.example.test1.cache.aspect.annoation.DateCachePut;
import com.example.test1.cache.generate.IKeyGenerate;
import com.example.test1.cache.handle.CacheHandle;
import com.example.test1.util.SpElUtils;
import com.example.test1.util.SpiUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.Optional;
import java.util.StringJoiner;
/**
* @Author xx
* @Date 2024/6/27 11:34
* @Description:
* @Version 1.0
*/
@Slf4j
@Aspect
@Component
public class CacheAspect {
/**
* 缓存前缀
*/
private static final String CHAR_PREFIX = "cache";
@Resource
private CacheHandle cacheHandle;
@Value(value = "${spring.application.name}")
private String applicationName;
@SneakyThrows
@Around(value = "@annotation(dateCachePut)")
public Object cachePut(ProceedingJoinPoint joinPoint, DateCachePut dateCachePut){
String applicationName = StringUtils.isBlank(dateCachePut.module()) ?
this.applicationName : dateCachePut.module();
//解析key并查询缓存
String key = buildCacheKey(applicationName,
dateCachePut.module(),
joinPoint,
dateCachePut.key(),
dateCachePut.keyGenerator());
Object result = cacheHandle.get(key);
if(result == null){
result = joinPoint.proceed();
if(result != null){
Boolean condition = SpElUtils.getValue(joinPoint,dateCachePut.condition(),Boolean.class,result);
if(condition){
cacheHandle.put(key,result,dateCachePut.passTime(),dateCachePut.timeUnit());
}
}
}
return result;
}
/**
* 删除缓存
* @param joinPoint 连接点
* @param dataCacheEvict 删除注解
* @return
*/
@SneakyThrows
@Around(value = "@annotation(dataCacheEvict)")
public Object cacheRemove(ProceedingJoinPoint joinPoint, DataCacheEvict dataCacheEvict){
String applicationName = StringUtils.isBlank(dataCacheEvict.module()) ?
this.applicationName : dataCacheEvict.module();
//解析key并查询缓存
String key = buildCacheKey(applicationName,
dataCacheEvict.module(),
joinPoint,
dataCacheEvict.key(),
dataCacheEvict.keyGenerator());
cacheHandle.evict(key);
//执行目标方法
Object result = joinPoint.proceed();
// 条件成立则异步删除
Boolean condition = SpElUtils.getValue(joinPoint, dataCacheEvict.condition(), Boolean.class, result);
if(condition){
cacheHandle.asyEvict(key,dataCacheEvict.delay(),dataCacheEvict.timeUnit());
}
return result;
}
/**
* 构建缓存key
* @param applicationName 服务名
* @param module 模块名
* @param joinPoint 链接点
* @param key 编写的key表达式
* @param keyGenerator key生成器实现类名称
* @return
*/
private String buildCacheKey(String applicationName,String module,
ProceedingJoinPoint joinPoint,String key,String keyGenerator){
return new StringJoiner("::")
.add(CHAR_PREFIX)
.add(applicationName)
.add(module)
.add(generateKey(joinPoint,key,keyGenerator))
.toString();
}
/**
* 生成key
* 1:key为空的情况下将会是方法参数列表中的toString集合
* 2:将表达式传递的key生成器实现类生成
*
* @param joinPoint 连接点
* @param key 编写的key表达式
* @param keyGenerator key生成器实现类名
* @return
*/
private CharSequence generateKey(ProceedingJoinPoint joinPoint, String key, String keyGenerator) {
return StringUtils.isEmpty(keyGenerator) ? Arrays.toString(joinPoint.getArgs()) :
Optional.ofNullable(SpiUtils.getServiceImpl(keyGenerator, IKeyGenerate.class))
.map(keyGenerate ->{
Assert.notNull(keyGenerate,String.format("%s找不到keyGenerate实现类", keyGenerator));
return keyGenerate.generateKey(joinPoint,key);
}).orElse(null);
}
}
工具类:
package com.example.test1.util;
import lombok.SneakyThrows;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
/**
* @Author xx
* @Date 2024/6/27 15:10
* @Description:
* @Version 1.0
*/
public class SpElUtils {
private final static String RESULT = "result";
/**
* 用于springEL表达式的解析
*/
private static SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
/**
* 用于获取方法参数定义的名字
*/
private static DefaultParameterNameDiscoverer defaultParameterNameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* 根据EL表达式获取值
*
* @param joinPoint 连接点
* @param key springEL表达式
* @param classes 返回对象的class
* @return 获取值
*/
public static <T> T getValue(ProceedingJoinPoint joinPoint, String key, Class<T> classes) {
// 解析springEL表达式
EvaluationContext evaluationContext = getEvaluationContext(joinPoint, null);
return spelExpressionParser.parseExpression(key).getValue(evaluationContext, classes);
}
/**
* 根据EL表达式获取值
*
* @param joinPoint 连接点
* @param expression springEL表达式
* @param classes 返回对象的class
* @param result result
* @return 获取值
*/
public static <T> T getValue(JoinPoint joinPoint, String expression, Class<T> classes, Object result) throws NoSuchMethodException {
// 解析springEL表达式
EvaluationContext evaluationContext = getEvaluationContext(joinPoint, result);
return spelExpressionParser.parseExpression(expression).getValue(evaluationContext, classes);
}
/**
* 获取参数上下文
*
* @param joinPoint 连接点
* @return 参数上下文
*/
@SneakyThrows
private static EvaluationContext getEvaluationContext(JoinPoint joinPoint, Object result) {
EvaluationContext evaluationContext = new StandardEvaluationContext();
String[] parameterNames = defaultParameterNameDiscoverer.getParameterNames(getMethod(joinPoint));
for (int i = 0; i < parameterNames.length; i++) {
evaluationContext.setVariable(parameterNames[i], joinPoint.getArgs()[i]);
}
evaluationContext.setVariable(RESULT, result);
return evaluationContext;
}
/**
* 获取目标方法
*
* @param joinPoint 连接点
* @return 目标方法
*/
private static Method getMethod(JoinPoint joinPoint) throws NoSuchMethodException {
Signature signature = joinPoint.getSignature();
return joinPoint.getTarget().getClass()
.getMethod(signature.getName(), ((MethodSignature) signature).getParameterTypes());
}
}
package com.example.test1.util;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.concurrent.TimeUnit;
/**
* @Author xx
* @Date 2024/6/27 14:19
* @Description:
* @Version 1.0
*/
public class SpiUtils {
/**
* SPI缓存key
*
* @param <T>
*/
private static final class SpiCacheKeyEntity<T> {
/**
* 实现的接口class
*/
private Class<T> classType;
/**
* 实现类的名称
*/
private String serviceName;
public SpiCacheKeyEntity(Class<T> classType, String serviceName) {
this.classType = classType;
this.serviceName = serviceName;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SpiCacheKeyEntity<?> spiCacheKeyEntity = (SpiCacheKeyEntity<?>) o;
return Objects.equals(classType, spiCacheKeyEntity.classType) && Objects.equals(serviceName, spiCacheKeyEntity.serviceName);
}
@Override
public int hashCode() {
return Objects.hash(classType, serviceName);
}
}
private SpiUtils() {
}
/**
* 单例
* 根据接口实现类的名称以及接口获取实现类
*
* @param serviceName 实现类的名称
* @param classType 实现的接口class
* @return 具体的实现类
*/
public static <T> T getServiceImpl(String serviceName, Class<T> classType) {
return (T) SERVICE_IMPL_CACHE.get(new SpiCacheKeyEntity(classType, serviceName));
}
/**
* SPI接口实现类 Caffeine软引用同步加载缓存(其内部做了同步处理)
*/
public final static LoadingCache<SpiCacheKeyEntity, Object> SERVICE_IMPL_CACHE = Caffeine.newBuilder()
.expireAfterAccess(24, TimeUnit.HOURS)
.maximumSize(100)
.softValues()
.build(spiCacheKeyEntity -> getServiceImplByPrototype(spiCacheKeyEntity.serviceName, spiCacheKeyEntity.classType));
/**
* 多例
* 根据接口实现类的名称以及接口获取实现类
*
* @param serviceName 实现类的名称
* @param classType 实现的接口class
* @return 具体的实现类
*/
public static <T> T getServiceImplByPrototype(String serviceName, Class<T> classType) {
ServiceLoader<T> services = ServiceLoader.load(classType, Thread.currentThread().getContextClassLoader());
for (T s : services) {
if (s.getClass().getSimpleName().equals(serviceName)) {
return s;
}
}
return null;
}
}
缓存key生成接口
package com.example.test1.cache.generate;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* @Author xx
* @Date 2024/6/27 15:05`在这里插入代码片`
* @Description: 缓存key 接口生成器
* @Version 1.0
*/
public interface IKeyGenerate {
/**
*生成key
* @param joinPoint 连接点
* @param key 编写的key表达式
* @return
*/
String generateKey(ProceedingJoinPoint joinPoint,String key);
}
实现
package com.example.test1.cache.generate.impl;
import com.example.test1.cache.generate.IKeyGenerate;
import com.example.test1.util.SpElUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* @Author xx
* @Date 2024/6/27 15:08
* @Description: 默认key生成器
* @Version 1.0
*/
@Slf4j
public class DefaultKeyGenerate implements IKeyGenerate {
@Override
public String generateKey(ProceedingJoinPoint joinPoint, String key) {
try {
return SpElUtils.getValue(joinPoint,key,String.class);
} catch (Exception e) {
log.error("DefaultKeyGenerate 抛出异常:{}", e.getMessage(), e);
throw new RuntimeException("DefaultKeyGenerate 生成key出现异常", e);
}
}
}
缓存处理
package com.example.test1.cache.handle;
import com.example.test1.cache.schema.ICacheSchema;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @Author xx
* @Date 2024/6/27 15:26
* @Description: 缓存处理
* @Version 1.0
*/
@Component
public class CacheHandle {
@Resource
private ICacheSchema cacheSchema;
@Resource
private ScheduledThreadPoolExecutor asyScheduledThreadPoolExecutor;
/**
*查询缓存
* @param key 缓存key
* @return 缓存值
*/
public Object get(String key){
return cacheSchema.get(key);
}
/**
*存入缓存
* @param key 缓存key
* @param value 缓存值
* @param expirationTime 缓存过期时间
* @param timeUnit 时间单位
*/
public void put(String key, Object value, long expirationTime, TimeUnit timeUnit){
cacheSchema.put(key,value,expirationTime,timeUnit);
}
/**
* 移除缓存
* @param key 缓存key
* @return
*/
public boolean evict(String key){
boolean evict = cacheSchema.evict(key);
if(evict){
}
return evict;
}
/**
* 异步定时删除缓存
* @param key 缓存key
* @param passTime 定时删除时间
* @param timeUnit 定时删除单位
*/
public void asyEvict(String key, long passTime, TimeUnit timeUnit) {
asyScheduledThreadPoolExecutor.schedule(()->this.evict(key),passTime,timeUnit);
}
}
缓存公共接口,和多级缓存接口
package com.example.test1.cache.schema;
import java.util.concurrent.TimeUnit;
/**
* @Author xx
* @Date 2024/6/27 15:28
* @Description: 缓存公共接口
* @Version 1.0
*/
public interface ICacheSchema {
/**
* 查询缓存
*
* @param key 缓存的key
* @return 缓存的值
*/
Object get(String key);
/**
* 存入缓存
*
* @param key 缓存的key
* @param value 缓存的值
* @param expirationTime 缓存的过期时间
* @param timeUnit 缓存过期时间的单位
*/
void put(String key, Object value, long expirationTime, TimeUnit timeUnit);
/**
* 移除缓存
*
* @param key 缓存的key
* @return 移除操作结果
*/
boolean evict(String key);
}
package com.example.test1.cache.schema;
/**
* @Author xx
* @Date 2024/6/27 16:25
* @Description: 多级缓存
* @Version 1.0
*/
public interface IMultipleCache extends ICacheSchema{
/**
* 移除一级缓存
*
* @param key 缓存的key
* @return 移除状态
*/
boolean evictHeadCache(String key);
}
本地缓存实现类
package com.example.test1.cache.schema.caffeien;
import com.example.test1.cache.schema.ICacheSchema;
import com.github.benmanes.caffeine.cache.Cache;
import java.util.concurrent.TimeUnit;
/**
* @Author xx
* @Date 2024/6/27 15:30
* @Description: Caffeine 本地缓存
* @Version 1.0
*/
public class CaffeineCache implements ICacheSchema {
private final Cache cache;
public CaffeineCache(Cache cache) {
this.cache = cache;
}
@Override
public Object get(String key) {
return cache.getIfPresent(key);
}
@Override
public void put(String key, Object value, long expirationTime, TimeUnit timeUnit) {
cache.put(key,value);
}
@Override
public boolean evict(String key) {
cache.invalidate(key);
return true;
}
}
redis缓存实现类
package com.example.test1.cache.schema.redis;
import com.example.test1.cache.schema.ICacheSchema;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* @Author xx
* @Date 2024/6/27 15:51
* @Description: redis分布式缓存
* @Version 1.0
*/
public class RedisCache implements ICacheSchema {
private final RedisTemplate redisTemplate;
public RedisCache(RedisTemplate redisTemplate){
this.redisTemplate = redisTemplate;
}
@Override
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
@Override
public void put(String key, Object value, long expirationTime, TimeUnit timeUnit) {
if(expirationTime == -1){
redisTemplate.opsForValue().set(key,value);
}else {
redisTemplate.opsForValue().set(key,value.toString(),expirationTime,timeUnit);
}
}
@Override
public boolean evict(String key) {
return redisTemplate.delete(key);
}
}
多级缓存实现类
package com.example.test1.cache.schema.multiple;
import com.example.test1.cache.config.CacheConfig;
import com.example.test1.cache.schema.ICacheSchema;
import com.example.test1.cache.schema.IMultipleCache;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
/**
* @Author xx
* @Date 2024/6/27 16:23
* @Description: 多级缓存实现
* @Version 1.0
*/
@Slf4j
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class MultipleCache implements IMultipleCache {
/**
* 一级缓存
*/
private ICacheSchema head;
/**
*下级缓存实现
*/
private MultipleCache next;
private CacheConfig cacheConfig;
public MultipleCache(ICacheSchema head){
this.head = head;
}
@Override
public Object get(String key) {
Object value = head.get(key);
if(value == null && next != null){
value = next.get(key);
if(value != null && cacheConfig != null){
head.put(key,value,cacheConfig.getCaffeineDuration(),TimeUnit.SECONDS);
}
}
return value;
}
@Override
public void put(String key, Object value, long expirationTime, TimeUnit timeUnit) {
head.put(key,value,expirationTime,timeUnit);
if(next != null){
next.put(key,value,expirationTime,timeUnit);
}
}
@Override
public boolean evict(String key) {
head.evict(key);
if(next != null){
next.evict(key);
}
return true;
}
@Override
public boolean evictHeadCache(String key) {
log.debug("移除一级缓存key={}", key);
return head.evict(key);
}
}
配置类:
package com.example.test1.cache.config;
import com.example.test1.cache.schema.ICacheSchema;
import com.example.test1.cache.schema.caffeien.CaffeineCache;
import com.example.test1.cache.schema.multiple.MultipleCache;
import com.example.test1.cache.schema.redis.RedisCache;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import javax.annotation.Resource;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @Author xx
* @Date 2024/6/27 16:12
* @Description: 缓存管理实例注册
* @Version 1.0
*/
@Slf4j
@EnableCaching
@Configuration
@ComponentScan("com.example.test1.cache")
@EnableConfigurationProperties(CacheConfig.class)
public class CacheManagerAutoConfiguration {
@Resource
private CacheConfig cacheConfig;
@Resource
private RedisTemplate redisTemplate;
@Bean
public ICacheSchema cacheSchema(){
//构建多级缓存
CaffeineCache caffeineCache = buildCaffeineCache();
RedisCache redisCache = buildRedisCache();
//构建组合多级缓存
return new MultipleCache(caffeineCache)
.setNext(new MultipleCache(redisCache))
.setCacheConfig(cacheConfig);
}
@Bean
public ScheduledThreadPoolExecutor asyScheduledEvictCachePool() {
return new ScheduledThreadPoolExecutor(cacheConfig.getAsyScheduledEvictCachePoolSize(),
new CustomizableThreadFactory("asy-evict-cache-pool-%d"));
}
private RedisCache buildRedisCache() {
return new RedisCache(redisTemplate);
}
/**
* 构建Caffeine缓存
*
* @return Caffeine缓存
*/
private CaffeineCache buildCaffeineCache() {
Cache<String, Object> caffeineCache = Caffeine.newBuilder()
.expireAfterWrite(cacheConfig.getCaffeineDuration(), TimeUnit.SECONDS)
.maximumSize(cacheConfig.getCaffeineMaximumSize())
.removalListener((RemovalListener) (k, v, removalCause) -> log.info("caffeine缓存移除:key={},cause={}", k, removalCause))
.build();
return new CaffeineCache(caffeineCache);
}
}
package com.example.test1.cache.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @Author xx
* @Date 2024/6/27 16:10
* @Description:
* @Version 1.0
*/
@Data
@ConfigurationProperties(prefix = "test-cache", ignoreInvalidFields = true)
public class CacheConfig {
/**
* 缓存模式(默认多级缓存)
*/
// private SchemaEnum schemaEnum = SchemaEnum.MULTIPLE;
/**
* 定时异步清理缓存线程池大小(默认50)
*/
private int asyScheduledEvictCachePoolSize = 50;
/**
* caffeine写入后失效时间(默认 5 * 60 秒)
*/
private long caffeineDuration = 5 * 60;
/**
* caffeine最大容量大小(默认500)
*/
private long caffeineMaximumSize = 5000;
}
具体使用示例:
经测试,请求详情后的10秒内(设置的有效时间是10秒)不论请求几次都仅和数据库连接一次,过期后重复第一次的结果,过期时间可以自定义。
如有不合理的地方还请指教!