Spring boot防止重复提交


最近在跟第三方对接接口的时候发现,当第三方请求可以增加或者修改数据的接口时候,此时数据库会有重复数据,经过排查后发现是由于网路抖动的原因触发的,
发现问题后经过分析后的解决方案:一种是加乐观锁,二是redis分布式锁,但是考虑到涉及到的业务比较特殊,需要及时返回结果,保证下游业务的处理,
决定使用redis来进行控制,对于此次的处理进行记录。以下是相关的代码:

处理的方法

因为此次是处理是不能修改目前涉及到的业务,以及后续其他接口可以继续使用故选择AOP(面向切面)的实现方案;

Redisson配置

引入依赖文件

1
2
3
4
5
6
7

<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.23.1</version>
</dependency>

配置文件

Redisson 使用手册
根据Redisson官网的介绍,Redisson是架设在Redis基础上的一个Java驻内存数据网格,则Redisson可以使用Redis的基础配置,Redis的配置如图所示
img.jpg

客户端配置:RedissonClientConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class RedissonClientConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.port}")
private String port;


@Bean
public RedissonClient getRedisson() {
Config config = new Config();
config.useSingleServer().
setAddress("redis://" + redisHost + ":" + port).
setPassword(password);
config.setCodec(new JsonJacksonCodec());
return Redisson.create(config);
}
}

自定义注解:ApiResubmit.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiResubmit {

/**
* 重复提交的前缀,用来区分不同的场景
*/
String prefix() default "";

/**
* 重复提交的key,用来辨别是否是一次重复的请求,支持SpEL,可以从方法的入参中获取
*/
String key() default "";

/**
* 请求禁止秒数,即在多少秒内禁止重复请求
*/
int forbidSeconds() default 4;

/**
* 厂商返回类型
* @return
*/
LimitResultType limitResultType() default LimitResultType.NAME_DEFAULT;

/**
* 返回类型 0 为xml 1为 json
* @return
*/
String resultType() default "0";
/**
* 重复提交的提示信息
*/
String message() default "正在提交中,请稍后再试";
}

AOP切面实现:ResubmitAspect.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/**
* 防止重复提交
*/
@EnableAspectJAutoProxy(exposeProxy = true)
@Aspect
@Component
@Slf4j
public class ResubmitAspect {


//默认的提示语
private static final String DEFAULT_BLOCKING_MESSAGE = "提交的频率过快,请稍后再试";
@Autowired
private RedissonClient redissonClient;

//切点
@Pointcut("@annotation(com.xxx.framework.aspectj.lang.annotation.ApiResubmit)"
+ "|| @within(com.xxx.framework.aspectj.lang.annotation.ApiResubmit)")
public void resubmitPointcut() {
}


@Around(value = "resubmitPointcut()")
public Object checkResubmit(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
ApiResubmit annotation = AnnotationUtils.findAnnotation(method, ApiResubmit.class);
if (annotation != null) { //如果这个方法标注这个注解
//以类名+方法名作为key的默认前缀
String defaultPrefix = joinPoint.getSignature().getDeclaringTypeName() + "#" + method.getName();
//获取此方法所传入的参数 map<参数名, 参数值>
Map<String, Object> methodParam = getMethodParam(joinPoint);
validate(annotation, defaultPrefix, methodParam);
}
return joinPoint.proceed();
}



/**
* 参数解析
* @param joinPoint
* @return
*/
private Map<String, Object> getMethodParam(ProceedingJoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
Map<String, Object> paramMap = new HashMap<>(args.length);
for (int i = 0; i < args.length; i++) {
paramMap.put(parameterNames[i], args[i]);
}
return paramMap;
}


public boolean tryLock(String redisKey, long waitMill, long leaseMill) {
RLock rLock =redissonClient.getLock(redisKey);
boolean locked = false;
try {
locked = rLock.tryLock(waitMill, leaseMill, TimeUnit.MILLISECONDS);
} catch (Exception ex) {
// log.warn("redisson tryLock failed redisKey:{}, e", redisKey, ex);
}

return locked;
}
private void validate(ApiResubmit annotation, String defaultPrefix, Map<String, Object> methodParam) {
try {
String prefix = StringUtils.isBlank(annotation.prefix()) ? defaultPrefix : annotation.prefix();
//使用SPEL进行key的解析
ExpressionParser parser = new SpelExpressionParser();
//去解析spel语句
StandardEvaluationContext context = new StandardEvaluationContext(methodParam);
context.addPropertyAccessor(new MapAccessor());
context.setVariables((Map<String, Object>) context.getRootObject().getValue());
String key = parser.parseExpression(annotation.key()).getValue(context, String.class);
//去获取redis锁,锁持有时间为注解属性forbidSeconds
boolean lock = tryLock(prefix + key, 0, annotation.forbidSeconds() * 1000L);

if (!lock) { //如果获取锁失败
// 拿到注解属性提示语,抛出异常
String message = StringUtils.isBlank(annotation.message()) ? DEFAULT_BLOCKING_MESSAGE : annotation.message();
//厂商信息
LimitResultType limitResultType=annotation.limitResultType();
//返回值类型
String resultType = annotation.resultType();
//Xml格式返回
if("0".equals(resultType)){
log.warn(message);
//PACS
if(limitResultType.getCode().equals(LimitResultType.NAME_PACS.getCode())){
throw new XmlRRException(LimitResultType.NAME_PACS,ResultCodeEnum.XINYI_FAIL,message);
}
//LIS
if(limitResultType.getCode().equals(LimitResultType.NAME_LIS_XINYI.getCode())){
throw new XmlRRException(LimitResultType.NAME_LIS_XINYI,ResultCodeEnum.SERVER_FAIL,message);
}
//ECG
if(limitResultType.getCode().equals(LimitResultType.NAME_ECG_NALONG.getCode())){
throw new XmlRRException(LimitResultType.NAME_ECG_NALONG,ResultCodeEnum.SERVER_FAIL,message);
}
}

}
} catch (XmlRRException e) {
throw e;
}
catch (Exception ex) {
//如果不是重复提交的异常,即出现了其他的异常,正常请求,但需要打印error日志
log.error("ApiResubmitAspectException: ", ex);
}

}

以上就是防止重复提交的AOP方法,具体使用如图所示:
img_1.png

参数解析:

  1. prefix = “pacsStatus” 重复提交的前缀,用来区分不同的场景
  2. key = “#paramData.applyId” 重复提交的key,用来辨别是否是一次重复的请求
  3. forbidSeconds = 6, #请求禁止秒数,即在多少秒内禁止重复请求
  4. resultType = “0” #返回类型 0 为xml 1为 json
  5. limitResultType = LimitResultType.NAME_PACS #厂商返回类型
  6. message = “正在提交中,请稍后再试” #重复提交的提示信息

结束

以上就是防重复提交的方法,这只是其中解决方法的其中之一,但是在实际的业务中会存在缓存失效,锁的失效,要真正做到幂等性的话
还有根据自身的业务进行判断。

-------------本文结束感谢您的阅读-------------