原理是使用aop
1. 创建注解
package com.zykj.newsell.common.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* redis缓存注解
*
* @author lc
* @version 1.0
* @date 2022/4/19 14:41
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME) //运行时存在,SOURCE则编译时不存在,如@overwrited
public @interface RedisCacheAnnotation {
//注解被自定义的RedisCacheProxy使用,则拥有其功能
String redisKeyPrefix() default "newsell:";// redisKey的默认前缀
String value();
}
2.切面类
可以使用多个注解,但是切面类是只有一个的,可以有多个环绕通知
package com.zykj.newsell.common.aop;
import com.zykj.newsell.common.enums.ResultCodeEnum;
import com.zykj.newsell.common.exception.ServiceException;
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.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 参数判断
* 用户米和密码格式判断
*
* @author lc
* @version 1.0
* @date 2022/4/18 13:21
*/
@Aspect
@Component
@Slf4j
public class ParameterJudgeAop {
@Autowired
private RedisTemplate redisTemplate;
@Around("@annotation(com.zykj.newsell.common.aop.ParameterJudgeAnnotation)")
public Object aroundAdviceByParam(ProceedingJoinPoint pjp) throws Throwable {
String reg = "^[a-zA-Z0-9]+$"; // 只允许中文数字英文
// 获取方法签名
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Object[] args = pjp.getArgs();
// 对参数进行处理
args = Arrays.stream(args).map(arg -> {
Optional<Object> argOptional = Optional.ofNullable(arg);
if (!argOptional.isPresent() || !arg.toString().matches(reg)) {
throw new ServiceException(ResultCodeEnum.BAD_REQUEST_PARAM);
}
return arg;
}).toArray(Object[]::new);
// 放行
return pjp.proceed(args);
}
@Around("@annotation(com.zykj.newsell.common.aop.RedisCacheAnnotation)")
public Object aroundAdviceByRedisCache(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); //获取信息
Method method = signature.getMethod();
RedisCacheAnnotation annotation = method.getAnnotation(RedisCacheAnnotation.class); //获取方法上的注解
Class<?> returnType = method.getReturnType();
Object[] args = joinPoint.getArgs();
String redisKey=annotation.redisKeyPrefix();
String value = annotation.value();
redisKey=redisKey+value;//从自定义注解获取key前缀
Object cache = redisTemplate.opsForValue().get(redisKey);
// 1.没有缓存
if (null==cache){
// 1.1 开启redis_nx分布式锁
String strUUID = UUID.randomUUID().toString(); // 随机设置值
// true 表示加锁,false表示被锁
Boolean isOK = redisTemplate.opsForValue().setIfAbsent(redisKey+":lock", strUUID, 2, TimeUnit.SECONDS);
if (isOK){
try {
// 1.2 执行被代理方法,访问db
cache = joinPoint.proceed();
// 1.3 并更新缓存
if (null!=cache){
redisTemplate.opsForValue().set(redisKey,cache);
}else {
// 1.4 防止不存在的key高频访问,(给缓存假数据instance)
redisTemplate.opsForValue().set(redisKey,returnType.newInstance(),20,TimeUnit.SECONDS);
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
// 释放锁
// lua脚本释放锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 设置lua脚本返回的数据类型
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
// 设置lua脚本返回类型为Long
redisScript.setResultType(Long.class);
redisScript.setScriptText(script);
redisTemplate.execute(redisScript, Arrays.asList(redisKey + ":lock"), strUUID);
}else {
// 没有拿到锁的请求,自旋,回调重新获取缓存,不能直接返回null
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return redisTemplate.opsForValue().get(redisKey);
}
}
//执行被代理方法,访问db
// try {
// cache = joinPoint.proceed();
// } catch (Throwable throwable) {
// throwable.printStackTrace();
// }
// 2.有缓存直接放行缓存
System.out.println("aop代理redis缓存成功");
//虽然返回的是Object类型,但是spirngAOP会帮我们转为代理方法需要的类型,如Map<String,Object>
//return cache;
return joinPoint.proceed();
}
}
3.使用
/**
* @return 用户信息
*/
@GetMapping("/getUserInfo")
@ApiOperation(value = "获取用户信息,token登录之后要执行的接口")
@RedisCacheAnnotation("2")
public ResultInfo getUserInfo() {
return ResultInfo.ok(userService.getUserInfo());
}
第一次没有数据,缓存进redis,后面你的请求就会直接走缓存了
注意,假如更改了缓存的数据的原数据源数据,那就清除这个缓存。