一、先看场景:
- 填写完页面表单数据,手抖或者恶意在极短的时间内连续多次调用保存操作,表中出现了业务数据完全重复的数据,只有ID不一样。
- 老生常谈的付款操作,正常操作,我们只触发一次扣款操作,即使遇到其他的情况发生了多次扣款,但是也只应该扣款一次。
- ...
不同的场景,需要不同的幂等操作方式实现。
今天主要针对,上述第一种场景,通过注解+Redis+aop切面的形式处理。
二、撸码
废话不多说,直接撸码。
定义注解
package com.aida.annotation.common.annotation;
import com.aida.annotation.common.aspect.em.VariableProvider;
import java.lang.annotation.*;
/**
* 防止重复提交
*
* @author Mr.SoftRock
* @Date 2021/7/13 17:14
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 参数的提供方式
* @return
*/
VariableProvider variableProvider() default VariableProvider.PATH_VARIABLE;
/**
* 待校验 属性或者变量名称
* @return
*/
String variableName();
/**
* 参数变量位置
* @return
*/
int variablePosition() default 0;
/**
* 要切的资源名称,用于描述接口功能
* @return
*/
String name() default "";
/**
* key 前缀
* @return
*/
String prefix() default "";
/**
* 时间显示
* 这个参数,我们可以随便设定,默认单位是 秒
* 可以根据不通的业务要求去设定
* @return
*/
int period();
}
变量提供方式枚举VariableProvider
这个地方,可以去根据自己的实际业务去扩展。
package com.aida.annotation.common.aspect.em;
/**
* 变量提供方式
*
* @author Mr.SoftRock
* @Date 2021/7/13 17:23
**/
public enum VariableProvider {
/**
* 通过PATH路径提供
* url/{p}
*/
PATH_VARIABLE,
/**
* 通过请求参数提供
* url?p=1
*/
REQUEST_PARAMETER,
/**
* 通过请求体提供
*/
REQUEST_BODY,
}
定义切面类:
package com.aida.annotation.common.aspect;
import com.aida.annotation.common.annotation.RepeatSubmit;
import com.aida.annotation.common.aspect.em.VariableProvider;
import com.aida.annotation.common.redis.CommonRedisCache;
import com.aida.annotation.common.utils.ServletUtils;
import com.aida.annotation.support.security.AccountPrincipalUtils;
import com.aida.annotation.support.security.userdetails.AccountPrincipal;
import com.baomidou.mybatisplus.core.toolkit.SystemClock;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 1、自定义业务防止重复提交切面 在controller层注入,标记key变量获取的方式和变量名称
* 2、本切面主要是用来识别解析得到的key
* 3、将获取到的key,根据业务规则去执行相应的处理
* 4、如果判断重复操作,直接断言出异常
*
* @author Mr.SoftRock
* @Date 2021/7/13 17:28
**/
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {
public final String CACHE_REPEAT_KEY = "repeatSubmitData:";
public final String REPEAT_TIME = "repeatTime";
public final String REPEAT_PARAMS = "repeatParams";
@Autowired
CommonRedisCache redisCache;
@Before("@annotation(repeatSubmit)")
public void repeatSubmitCheck(JoinPoint joinPoint, RepeatSubmit repeatSubmit) {
AccountPrincipal handler = AccountPrincipalUtils.getCurrentHandler();
String aopTarget = this.getAopTarget(joinPoint);
HttpServletRequest request = ServletUtils.getRequest();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method signatureMethod = signature.getMethod();
RepeatSubmit limit = signatureMethod.getAnnotation(RepeatSubmit.class);
int period = limit.period();
//ImmutableList是一个不可变、线程安全的列表集合,它只会获取传入对象的一个副本。
ImmutableList
三、验证
1、新建一个controller 调用方法
@RepeatSubmit(variableProvider = VariableProvider.REQUEST_BODY, variableName = "com.aida.annotation.common.controller.dto.Test", period = 5,
name = "testRepeatSubmit", prefix = "repeat")
@PostMapping("/repeat")
public int testRepeatSubmit(@RequestBody Test test) {
return ATOMIC_INTEGER.incrementAndGet();
}
2、用到的测试class类对象
package com.aida.annotation.common.controller.dto;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* @author Mr.SoftRock
* @Date 2021/7/13 19:28
**/
@Data
public class Test {
String name;
Integer age;
List list;
Test1 test1;
@Data
public static class Test1 implements Serializable {
private static final long serialVersionUID = -4262288319285897072L;
String name;
Integer age;
List list;
}
}
3、启动项目,通过postman调用看下效果
在设定的5秒
内调用一次,可以正常返回,如下图:
Redis中也存在了对应的key值,如下图:
如果在设定的时间内多次操作,则触发幂等校验,如下图:
总结
幂等性的问题确实是在很多种场景都会需要,实现的方式有很多种,找一种最合适自己的。