1、前言
在前台查询首页数据的方法上加了注解:
@Cacheable(value = "courseAndTeacher", key = "'selectIndexList'")
另外还有两个注解是用在更新缓存:
本来只要这几个注解配合起来使用就能实现数据自动更新,但是由于前台数据只需要查数据,而后台则有全部的增删改查,接口比较多,许多方法都要加注解,容易疏漏也不方便管理,所以想写个加在控制类上的注解,一个注解搞定自动更新缓存
网上比较流行的那种写法,是对每个方法加都实现缓存操作,但是并没有实际的用处,因为每个方法都有自己的key值而不会去更新指定的缓存。
不过参考那种写法,有了个新思路:前台数据查询时,缓存注解还是使用原来的@Cacheable,然后仿照这个注解增加个自定义注解,也能传入key,value的值,这样的话可以拼接出需要自动更新的缓存的key值,然后直接清除这个缓存,利用aop切面对标注了这个注解的类中所有方法中的增删改操作都会去清除缓存,该注解适合直接加在后台控制类上
下面是实现
2、自定义注解:RedisCutCache
/**
* @Description: redis清除缓存注解 编写在需要缓存的类上
**/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisCutCache {
String key() default "";
String value() default "default";
}
3、编写切面类:RedisCutCacheAOP
import com.yuanhan.baseservice.aop.redisaop.annotation.RedisCutCache;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.regex.Pattern;
/**
* @author hanlin
* @date 2022年03月12日 10:42
* Redis清除缓存切面类
*/
@Component
@Aspect
@Slf4j
public class RedisCutCacheAOP {
/**
* 更新缓存验证
* 如果是更改或新增或删除数据 则清除缓存
* 切入方法名中以这些单词为前缀的方法
*/
private static final Pattern SET_CACHE_PATTERN = Pattern.compile("^((add)|(insert)|(save)|(batchInsert)|(batchUpdate)|(update)|(delete)|(remove)).*$");
@Resource
private RedisTemplate redisTemplate;
/**
* @Title: queryCachePointcut
* @Description: 定义切点为缓存注解
* @return void
**/
@Pointcut("@within(com.yuanhan.baseservice.aop.redisaop.annotation.RedisCutCache)")
public void queryCachePointcut(){
}
@Around("queryCachePointcut()&&@within(redisCutCache)")
public Object Interceptor(ProceedingJoinPoint joinPoint, RedisCutCache redisCutCache) throws Throwable{
long beginTime = System.currentTimeMillis();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取方法名
String methodName = signature.getMethod().getName();
//动态获取注解中的数据
String prekey = redisCutCache.key().replace("'","");
String value = redisCutCache.value();
//拼接key值
String key = value + "::" + prekey;
if(SET_CACHE_PATTERN.matcher(methodName).matches()){
if (redisTemplate.hasKey(key)){
redisTemplate.delete(key);
log.warn("执行方法 : [ "+methodName+" ] : 清除 key 为 ["+key+ "] :的缓存数据");
}else {
log.warn("执行方法 : [ "+methodName+" ] : 没有 key 为 ["+key+ "] :的缓存数据");
}
log.warn("AOP 清除缓存 >>>> end 耗时:" + (System.currentTimeMillis() - beginTime) + "ms.");
}
// 调用原始方法
return joinPoint.proceed();
}
// @Autowired(required = false)
// public void setRedisTemplate(RedisTemplate redisTemplate) {
// RedisSerializer stringSerializer = new StringRedisSerializer();//序列化为String
// Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);//序列化为Json
// redisTemplate.setKeySerializer(stringSerializer);
// redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// redisTemplate.setHashKeySerializer(stringSerializer);
// redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
// this.redisTemplate = redisTemplate;
// }
}
下图是需要注意的地方
4、配置redis配置类:RedisConfig
上面的代码中我把有关RedisTemplate的配置注释掉了,因为我配置过了,使用起来没有问题,这里我把配置类也贴出来,或者不加配置类把那段注释打开,具体效果如何我没测试过
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@EnableCaching //开启缓存
@Configuration //配置类
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//enableDefaultTyping(...) 建议改成 activateDefaultTyping(om.getPolymorphicTypeValidator(), ...)
om.activateDefaultTyping(om.getPolymorphicTypeValidator(),ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(om.getPolymorphicTypeValidator(),ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
5、需要用到的依赖
可能有一些漏掉的,需要自己补充了
<!-- aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
6、使用
在前台查询数据接口上添加注解
@Cacheable(value = "courseAndTeacher", key = "'selectIndexList'")
这是在redis中生成缓存的key
可以看到拼接规则是:
String key = value + "::" + prekey;
所以想要做到及时更新这个缓存,只需要同样的传入value和key,然后去清空缓存
在后台增删改查的控制类上添加注解:
@RedisCutCache(value = "courseAndTeacher", key = "'selectIndexList'")
这样只要数据有了变化(增删改),就会自动清除缓存,查询接口就会从数据库中取出最新的值
这里有个细节
为了保持一致性(其实是为了方便直接复制),我们自定义注解也是这样传参数,但是会在取值时过滤掉单引号,这是自定义注解类中的源码
到这一步就可以大功告成了
7、实战效果
控制台打印了日志
清空控制台,不断刷新首页 ,无日志显示
使用缓存查询前台数据生效
然后在后台对课程信息进行操作,比如更改课程标题,控制台打印
查看缓存
课程跟讲师相关缓存已清除,下次访问首页即可从数据库中获取最新的数据,完成!
8、总结
1、编写自定义注解:RedisCutCache
2、编写切面类:RedisCutCacheAOP
3、检查是否配置redis配置类,没有的话把切面类下面的注释打开(这个方式本人未尝试,无用的话建议复制我的配置类)
4、在查询接口方法上添加注解:@Cacheable(value = "xxx", key = "'xxx'")
6、在后台控制类(是类注解)上添加注解:@RedisCutCache(value = "xxx", key = "'xxx'")
注意这两个注解的value和key要完全一样,因为是为了操作同一个key的缓存,一个是用在前台查询方法上,另一个是用在后台控制类上