接口幂等性
幂等性是什么:一个接口在入参相同的情况下,被多次发起请求,多次调用产生的结果与一次调用是相同的,简单说其实就是必须保证只有一次请求操作被执行
主要原理:定义切面拦截请求,利用Redis的分布式锁Redisson,对接口的入参比如用户token设置成一个key,或者基于业务去组合一个唯一key,再给这个key设置一个过期时间,这样就保证了在一段时间内同样的请求只能取到一个锁,从而保证接口的幂等性。具体实现主要通过切面织入设置redis的key以及加锁等逻辑。
实现:
1、自定义注解
import java.lang.annotation.*;
/**
* @description: 幂等注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
/**
* 参数的表达式,用来确定key值,例如:"#req.userInfo.userId,#req.seqId"
* @return
*/
String key();
/**
* 幂等过期时间,即:在此时间段内,对API进行幂等处理。
*/
long expireTime();
}
2、幂等性切面
/**
* @description: 幂等性切面
*/
@Slf4j
@Aspect
@Component
public class IdempotentAspect {
/**
* redis缓存key的模板
*/
private static final String KEY_TEMPLATE = "idempotent:%s";
@Resource
RedissonDistributedLocker redissonDistributedLocker;
@Autowired
RedisRepository redisRepository;
/**
* 定义切点
*/
@Pointcut("@annotation(cn.com.bluemoon.spring.annotation.Idempotent)")
public void executeIdempotent() {
}
@Around("executeIdempotent()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//获取方法
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//获取幂等注解
Idempotent idempotent = method.getAnnotation(Idempotent.class);
// joinPoint获取参数值
Object[] args = joinPoint.getArgs();
String expressKey = idempotent.key();
if (!expressKey.contains("#")) {
//注解的值非SPEL表达式
throw new Exception("表达式错误",new Throwable() );
}
String key = parseKey(expressKey, method, args);
String cacheKey = String.format("MYAPP" + KEY_TEMPLATE, key);
//通过加锁确保只有一个接口能够正常访问
//尝试加锁
//这里是不限制时间,知道接口执行为止才释放 boolean tryLock = redissonDistributedLocker.tryLock(cacheKey, TimeUnit.SECONDS, 0, -1);
//这里根据入参设置锁过期时间,注意应该得确保在这个时间内接口能执行完
boolean tryLock = redissonDistributedLocker.tryLock(cacheKey, TimeUnit.SECONDS, 0, idempotent.expireTime());
if (tryLock) {
log.debug("get tryLock ,key:{}", cacheKey);
try {
Object proceed = joinPoint.proceed();
return proceed;
} finally {
log.debug("lease lock");
redissonDistributedLocker.unlock(cacheKey);
}
} else {
log.debug("get tryLock fail,key:{}", cacheKey);
throw new RuntimeException("请求已接收,请不要重复操作!");
}
}
/**
* 获取缓存的key
* key 定义在注解上,支持SPEL表达式
*
* @return
*/
private String parseKey(String key, Method method, Object[] args) {
if (StringUtils.isEmpty(key)) return null;
//获取被拦截方法参数名列表(使用Spring支持类库)
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = u.getParameterNames(method);
//使用SPEL进行key的解析
ExpressionParser parser = new SpelExpressionParser();
//SPEL上下文
StandardEvaluationContext context = new StandardEvaluationContext();
//把方法参数放入SPEL上下文中
for (int i = 0; i < paraNameArr.length; i++) {
context.setVariable(paraNameArr[i], args[i]);
}
return parser.parseExpression(key).getValue(context, String.class);
}
}
3、使用:
在controller上,通过接口入参判断,加锁时间为2秒
@Idempotent(key = "#dto.userId", expireTime = 2L)
AOP-面向切面编程
特征:多个步骤间的隔离性、原代码无关性
相关注解:
1、@Aspect
切面,作用在定义的切面类上,在里面做需要织入的增强逻辑,一个Java类加了这个注解之后就声明这是一个切面类
2、@Pointcut
切点、连接点,在哪个层面使用的你的增强逻辑。可通配路径到所有controller,也可自定义一个注解,切点通过注解去作用于某个“点”。
3、@Before 前置通知,在某连接点(JoinPoint)之前执行的通知
4、@After 前置通知,在某连接点(JoinPoint)之后执行的通知
5、@Around 环绕通知,等价于@After 和 @Before,但又有点区别
@Around和@Before+@After的区别
@Around用这个注解的方法入参传的是ProceedingJionPoint pjp,可以决定当前线程能否进入核心方法中——通过调用pjp.proceed(),
而@After 和 @Before的方法入参只能是JoinPoint joinPoint,这个类没有proceed()方法,所以不能决定线程是否进入核心方法中
eg:
(1)自定义一个注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
/**
* 参数的表达式,用来确定key值
* @return
*/
String key();
/**
* 幂等过期时间,即:在此时间段内,对API进行幂等处理
*/
long expireTime() default -1;
}
(2)aop切面
@Component
public class MyAspect {
/**
* 定义切点,此处切点为自定义注解。
* 【如果不通过注解进行切面织入,也可以使用通配对所有controller进行切面加强,比如@Pointcut("execution(public * com.myproject..*.*(..))"),
* 如果使用这种,直接@Before("Pointcut()")、@After("Pointcut()")、@Around("Pointcut()")即可】
*/
@Pointcut("@annotation(cn.com.myproject.annotation.MyAnnotation)")
public void executeMyAnnotation() {
}
@Before("executeMyAnnotation()")
public void beforeMethod(JoinPoint joinPoint) {
//dosomething
}
@After("executeMyAnnotation()")
public void afterMethod(JoinPoint joinPoint) {
//dosomething
}
/**
* @Around注解 环绕执行,就是在调用目标方法之前和调用之后都会执行你的代码逻辑,划分点是执行Object proceed = joinPoint.proceed()。等价于@After 和 @Before
*/
@Around("executeMyAnnotation()")
public Object Around(ProceedingJoinPoint pjp) throws Throwable {
//dosomething=>@Before
// 调用执行目标方法(result为目标方法执行结果)
Object result = pjp.proceed();
//dosomething=>@After
return result;
}