接口幂等性就是用户对同一操作发起了一次或多次请求的对数据的影响是一致不变的,不会因为多次的请求而产生副作用。网上有很多资料对幂等性接口及其实现方案进行了介绍,其中应用比较广泛的是token+redis。其实这种思想的本质就是给一个请求分配一个有关联的唯一键,请求时根据这个唯一键是否存在来判断是否是重复请求。
本文也基于这种思路,通过AOP的方式设计了一种接口幂等性实现方案,通过自定义注解来控制接口幂等性,能够细化到哪些接口要满足幂等性,并且提出了同步幂等和异步幂等的概念。这种方案目前主要是解决用户在上一次请求还没结束的情况下,多次点击重复请求的问题,比如下载导出请求,这种请求一般都比较消耗资源,因此应该避免多次请求(当前可以在前端控制,但对于异步场景,这种方案也支持)。
此方案主要由两个注解和一个切面实现,具体代码如下:
//此注解用来表示哪些参数要用于幂等性校验中
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface IdempotentParam {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface IdempotentRequest {
/**
* 自定义子健,最终幂等性键是由用户+子健+幂等入参组成的。
* 注意:在自定义subKey时,要考虑唯一性,因为幂等入参和用户有可能会相同,此时就需要保证subKey是唯一的。
*/
String subKey();
/**
* 是否为同步幂等,同步幂等会在aop的after中自动清除缓存key重置状态,异步幂等需要在代码中手动清除缓存。
*/
boolean syncIdempotent() default false;
}
/**
* 幂等性接口
* 实现逻辑:
* 以当前用户+@IdempotentRequest.subKey+@IdempotentParam幂等入参为唯一性校验的值进行幂等性拦截,如果缓存中没有对应的键,则放入缓存并放行请求;
* 如果缓存中有对应的键,则说明是重复请求,拦截返回;
* 此外,如果是异步幂等,对应的业务在处理完后需要清除缓存中的键
**/
@Aspect
@Component
@Lazy(false)
public class IdempotentAspect {
private static Logger logger = LoggerFactory.getLogger(IdempotentAspect.class);
/**
* 存放每个请求线程的幂等性校验键,用于在请求结束后自动删除缓存,实现同步幂等
*/
private static ThreadLocal<String> keyThreadLocal = new ThreadLocal<>();
@Pointcut("@annotation(com.hxyy.exam.aop.IdempotentRequest)")
private void cutMethod() {}
@Around("cutMethod()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String key = this.generateKey(joinPoint);
if (CacheUtil.hasKey(key)){
CommonResponse<Object> response = new CommonResponse<>();
response.setCode(ResponseEnum.DUPOLICATE_REQUEST.getEnumCode());
response.setMsg(ResponseEnum.DUPOLICATE_REQUEST.getEnumMsg());
return response;
}else {
CacheUtil.set(key,"");
return joinPoint.proceed(joinPoint.getArgs());
}
}
@After("cutMethod()")
public void after(){
String key = keyThreadLocal.get();
/**
* 如果ThreadLocal中存在key,则说明是同步幂等,因此需要在after中清除缓存
*/
if (StringUtils.isNotEmpty(key)){
CacheUtil.del(key);
keyThreadLocal.remove();
}
}
private String generateKey(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
String user = JwtUtil.getUserNameFromToken();
Object[] params = joinPoint.getArgs();
Method targetMethod = getTargetMethod(joinPoint);
IdempotentRequest declaredAnnotation = targetMethod.getDeclaredAnnotation(IdempotentRequest.class);
String subKey = declaredAnnotation.subKey();
String methodName = targetMethod.getName();
Annotation[][] parameterAnnotations = targetMethod.getParameterAnnotations();
StringBuilder paramStr = new StringBuilder();
for (int i = 0; i < params.length; i++) {
for (int j = 0; j < parameterAnnotations[i].length; j++) {
if ("IdempotentParam".equals(parameterAnnotations[i][j].annotationType().getSimpleName())){
paramStr.append(JSONObject.toJSONString(params[i]));
}
}
}
String key = user+subKey+ paramStr;
/**
* 如果是同步幂等,则使用ThreadLocal存储key,方便线程后续获取
*/
if (declaredAnnotation.syncIdempotent()){
keyThreadLocal.set(key);
}
return key;
}
private Method getTargetMethod(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
String methodName = joinPoint.getSignature().getName();
Class<?> targetClass = joinPoint.getTarget().getClass();
Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getParameterTypes();
Method objMethod = targetClass.getMethod(methodName, parameterTypes);
return objMethod;
}
}
使用方式:
@PostMapping("/syncIdempotent")
@IdempotentRequest(subKey = "syncIdempotent",syncIdempotent = true)
public String syncIdempotent(@IdempotentParam @RequestBody Conditions conditions,@RequestParam(value = "param1") String param1, @IdempotentParam @RequestParam(value = "param") String param){
return "同步幂等接口测试";
}
@PostMapping("/asyncIdempotent")
@IdempotentRequest(subKey = "asyncIdempotent",syncIdempotent = false)
public String asyncIdempotent(@IdempotentParam @RequestBody Conditions conditions){
new Thread(()->{
try{
//业务逻辑处理
}catch (RuntimeException e){
}finally {
//清除缓存
String user = "xxx";
String key = user+"asyncIdempotent"+ JSONObject.toJSONString(conditions);//key由用户+subKey+conditions组成
CacheUtil.del(key);
}
}).run();
return "异步幂等接口测试";
}