0%

SpringBoot项目接口防止重复提交

SpringBoot项目接口防止重复提交

接口重复提交:是由于网络等原因,造成一瞬间发送多个请求,造成insert,update操作,多次数据被修改。如果多个请求时间间隔足够的小,那么可以理解为并发问题;即并发情况下,只有一次操作成功。

实现原理: 自定义注解+AOP切面+分布式锁。

自定义注解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 防止重复提交的注解
*
* @author yxh
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AvoidRepeatSubmit {

long lockTime() default 1000;

}

分布式锁
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
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisCommands;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

/**
* Created by yxh on 2022/1/10 18:02
*/
@Component
public class RedisDistributedLock {

@Resource
private RedisTemplate<String, Object> redisTemplate;

public static final String UNLOCK_LUA;

static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
UNLOCK_LUA = sb.toString();
}

private final Logger logger = LoggerFactory.getLogger(RedisDistributedLock.class);

public boolean setLock(String key, String clientId, long expire) {
try {
//该的值只能取 EX 或者 PX,代表数据过期时间的单位,EX 代表秒,PX 代表毫秒
RedisCallback<String> callback = (connection) -> {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
return commands.set(key, clientId, "NX", "PX", expire);
};
String result = redisTemplate.execute(callback);

return !StringUtils.isEmpty(result);
} catch (Exception e) {
logger.error("set redis occured an exception", e);
}
return false;
}

public String get(String key) {
try {
RedisCallback<String> callback = (connection) -> {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
return commands.get(key);
};
String result = redisTemplate.execute(callback);
return result;
} catch (Exception e) {
logger.error("get redis occured an exception", e);
}
return "";
}

public boolean releaseLock(String key, String requestId) {
// 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
try {
List<String> keys = new ArrayList<>();
keys.add(key);
List<String> args = new ArrayList<>();
args.add(requestId);

// 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
// spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
RedisCallback<Long> callback = (connection) -> {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
}

// 单机模式
else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
return 0L;
};
Long result = redisTemplate.execute(callback);

return result != null && result > 0;
} catch (Exception e) {
logger.error("release lock occured an exception", e);
} finally {
// 清除掉ThreadLocal中的数据,避免内存溢出
//lockFlag.remove();
}
return false;
}

}

AOP切面:
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
114
115
116
117
118
119
120
121
122

import huayue.sports.venue.annotation.AvoidRepeatSubmit;
import huayue.sports.venue.common.BusinessException;
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.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;

/**
* Created by yxh on 2022/1/10 18:00
* 防止重复提交的切面
*/
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {
// 重复提交code
private static final int REPEAT_SUBMIT_CODE = 507;

@Autowired
private RedisDistributedLock redisDistributedLock;


/**
* 切点
*
* @param avoidRepeatSubmit 注解
*/
@Pointcut("@annotation(avoidRepeatSubmit)")
public void pointCut(AvoidRepeatSubmit avoidRepeatSubmit) {
}

/**
* 利用环绕通知进行处理重复提交问题
*
* @param pjp ProceedingJoinPoint
* @param avoidRepeatSubmit 注解
* @return Object
* @throws Throwable
*/
@Around(value = "pointCut(avoidRepeatSubmit)", argNames = "pjp,avoidRepeatSubmit")
public Object around(ProceedingJoinPoint pjp, AvoidRepeatSubmit avoidRepeatSubmit) throws Throwable {

long lockMillSeconds = avoidRepeatSubmit.lockTime();

//获得request对象
HttpServletRequest request = httpServletRequest();

Assert.notNull(request, "request can not null");

// 此处可以用token
String token = request.getParameter("access_token");
String path = request.getServletPath();
String key = getKey(token, path);
log.info("key={}", key);
String clientId = getClientId();
//锁定多少毫秒
boolean isSuccess = redisDistributedLock.setLock(key, clientId, lockMillSeconds);
Object result;
if (isSuccess) {
log.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);
// 获取锁成功, 执行进程
try {
result = pjp.proceed();
} finally {
//解锁
redisDistributedLock.releaseLock(key, clientId);
log.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId);
}
return result;
} else {
// 获取锁失败,认为是重复提交的请求
log.info("tryLock fail, key = [{}]", key);
throw new BusinessException("重复请求,请稍后再试");
// return ResultUtils.fail(REPEAT_SUBMIT_CODE, "重复请求,请稍后再试");
}
}

/**
* 获得request对象
*
* @return HttpServletRequest对象
*/
private HttpServletRequest httpServletRequest() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert requestAttributes != null;
return requestAttributes.getRequest();
}


/**
* 获得请求key
*
* @param token token
* @param path 路径
* @return 组合key
*/
private String getKey(String token, String path) {
return token + ":" + path;
}

/**
* 获得uuid
*
* @return uuid
*/
private String getClientId() {
return UUID.randomUUID().toString();
}


}

添加注解

只要在需要防止重复提交的接口上面加上@AvoidRepeatSubmit 注解即可实现。

所需的依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>