在实际业务系统应用过程中,都会存在一个表单数据重复提交的问题。针对这个问题网上也存在N多种解决方案。
为节省选择时间,因此在这N多种方案中,我整理了自认为从根本上解决重复提交的问题的一个最优方案。
具体实现原理自己理解吧,真不想写了,2019年5月17日10:54:54 QingDao LaoShan这个月份 灰蒙蒙的天 冻的俺都不敢伸出手 嘿嘿~~
还是在啰嗦两句吧!
该方案是利用aop注解+redis,每次表单提交都会携带token,在aop中token+path组成一个唯一的key,然后存储到redis中,其中value为系统生成的随机字符串,唯一的不同是就是设定redis中该key的过期时间。redis就是利用这个时间周期,验证是否该key已存在,已存在就不在执行后续操作。
pom.xml
4.0.0
com.norepeat
springboot-norepeat
0.0.1-SNAPSHOT
jar
springboot-norepeat
http://maven.apache.org
org.springframework.boot
spring-boot-starter-parent
2.1.3.RELEASE
UTF-8
org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
io.lettuce
lettuce-core
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-aop
org.springframework.boot
spring-boot-devtools
runtime
org.springframework.boot
spring-boot-starter-test
test
redis.clients
jedis
org.apache.commons
commons-pool2
org.springframework.boot
spring-boot-maven-plugin
application.properties
server.port=8000
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=192.168.0.192
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
#spring.redis.password=yourpwd
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间
spring.redis.jedis.pool.max-wait=-1ms
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=5000ms
package com.norepeat;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
* @ClassName: App
* @Description:TODO(启动入口)
* @author: 寻找手艺人
* @email: [email protected]
* @date: 2019年5月17日 上午10:52:10
*
* @Copyright: 2019 www.maker-win.net Inc. All rights reserved.
*
*/
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
return new RestTemplate(factory);
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
return factory;
}
}
ApiResult.java
package com.norepeat;
/**
* @ClassName: ApiResult
* @Description:TODO(这里用一句话描述这个类的作用)
* @author: 寻找手艺人
* @email: [email protected]
* @date: 2019年5月17日 上午9:11:38
*
* @Copyright: 2019 www.maker-win.net Inc. All rights reserved.
*
*/
public class ApiResult {
private Integer code;
private String message;
private Object data;
public ApiResult(Integer code, String message, Object data) {
this.code = code;
this.message = message;
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message == null ? null : message.trim();
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
@Override
public String toString() {
return "ApiResult{" +
"code=" + code +
", message='" + message + '\'' +
", data=" + data +
'}';
}
}
NoRepeatSubmit.java
package com.norepeat.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @ClassName: NoRepeatSubmit
* @Description:TODO(这里用一句话描述这个类的作用)
* @author: 寻找手艺人
* @email: [email protected]
* @date: 2019年5月17日 上午9:08:27
*
* @Copyright: 2019 www.maker-win.net Inc. All rights reserved.
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {
/**
* 设置请求锁定时间
*
* @return
*/
int lockTime() default 10;
}
RepeatSubmitAspect .java
package com.norepeat.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import com.norepeat.ApiResult;
import com.norepeat.utils.RedisLock;
import com.norepeat.utils.RequestUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
/**
* @ClassName: RepeatSubmitAspect
* @Description:TODO(这里用一句话描述这个类的作用)
* @author: 寻找手艺人
* @email: [email protected]
* @date: 2019年5月17日 上午9:09:12
*
* @Copyright: 2019 www.maker-win.net Inc. All rights reserved.
*
*/
@Aspect
@Component
public class RepeatSubmitAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(RepeatSubmitAspect.class);
@Autowired
private RedisLock redisLock;
@Pointcut("@annotation(noRepeatSubmit)")
public void pointCut(NoRepeatSubmit noRepeatSubmit) {
}
@Around("pointCut(noRepeatSubmit)")
public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {
int lockSeconds = noRepeatSubmit.lockTime();
HttpServletRequest request = RequestUtils.getRequest();
Assert.notNull(request, "request can not null");
// 此处可以用token或者JSessionId
String token = request.getHeader("Authorization");
String path = request.getServletPath();
String key = getKey(token, path);
String clientId = getClientId();
boolean isSuccess = redisLock.tryLock(key, clientId, lockSeconds);
LOGGER.info("tryLock key = [{}], clientId = [{}]", key, clientId);
if (isSuccess) {
LOGGER.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);
// 获取锁成功
Object result;
try {
// 执行进程
result = pjp.proceed();
} finally {
// 解锁
redisLock.releaseLock(key, clientId);
LOGGER.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId);
}
return result;
} else {
// 获取锁失败,认为是重复提交的请求
LOGGER.info("tryLock fail, key = [{}]", key);
return new ApiResult(200, "重复请求,请稍后再试", null);
}
}
private String getKey(String token, String path) {
return token + path;
}
private String getClientId() {
return UUID.randomUUID().toString();
}
}
RedisLock.java
package com.norepeat.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import java.util.Collections;
/**
* @ClassName: RedisLock
* @Description:TODO(这里用一句话描述这个类的作用)
* @author: 寻找手艺人
* @email: [email protected]
* @date: 2019年5月17日 上午9:10:06
*
* @Copyright: 2019 www.maker-win.net Inc. All rights reserved.
* Redis 分布式锁实现
* 如有疑问可参考 @see Redis分布式锁的正确实现方式
*
*
*/
@Service
public class RedisLock {
private static final Long RELEASE_SUCCESS = 1L;
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
// 当前设置 过期时间单位, EX = seconds; PX = milliseconds
private static final String SET_WITH_EXPIRE_TIME = "EX";
// if get(key) == value return del(key)
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 该加锁方法仅针对单实例 Redis 可实现分布式加锁
* 对于 Redis 集群则无法使用
*
* 支持重复,线程安全
*
* @param lockKey 加锁键
* @param clientId 加锁客户端唯一标识(采用UUID)
* @param seconds 锁过期时间
* @return
*/
public boolean tryLock(String lockKey, String clientId, long seconds) {
return redisTemplate.execute((RedisCallback) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
});
}
/**
* 与 tryLock 相对应,用作释放锁
*
* @param lockKey
* @param clientId
* @return
*/
public boolean releaseLock(String lockKey, String clientId) {
return redisTemplate.execute((RedisCallback) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey),
Collections.singletonList(clientId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
});
}
}
RequestUtils.java
package com.norepeat.utils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* @ClassName: RequestUtils
* @Description:TODO(这里用一句话描述这个类的作用)
* @author: 寻找手艺人
* @email: [email protected]
* @date: 2019年5月17日 上午9:10:58
*
* @Copyright: 2019 www.maker-win.net Inc. All rights reserved.
*
*/
public class RequestUtils {
public static HttpServletRequest getRequest() {
ServletRequestAttributes ra= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return ra.getRequest();
}
}
SubmitController.java
package com.norepeat.controller;
/**
* @ClassName: SubmitController
* @Description:TODO(这里用一句话描述这个类的作用)
* @author: 寻找手艺人
* @email: [email protected]
* @date: 2019年5月17日 上午9:12:15
*
* @Copyright: 2019 www.maker-win.net Inc. All rights reserved.
*
*/
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.norepeat.ApiResult;
import com.norepeat.aop.NoRepeatSubmit;
@RestController
public class SubmitController {
@PostMapping("submit")
@NoRepeatSubmit(lockTime = 30)
public Object submit(@RequestBody UserBean userBean) {
try {
// 模拟业务场景
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new ApiResult(200, "成功", userBean.userId);
}
public static class UserBean {
private String userId;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId == null ? null : userId.trim();
}
}
}
RunTest.java
package com.norepeat.test;
/**
* @ClassName: RunTest
* @Description:TODO(这里用一句话描述这个类的作用)
* @author: 寻找手艺人
* @email: [email protected]
* @date: 2019年5月17日 上午9:13:45
*
* @Copyright: 2019 www.maker-win.net Inc. All rights reserved.
*
*/
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Component
public class RunTest implements ApplicationRunner {
private static final Logger LOGGER = LoggerFactory.getLogger(RunTest.class);
@Autowired
private RestTemplate restTemplate;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("执行多线程测试");
String url="http://localhost:8000/submit";
CountDownLatch countDownLatch = new CountDownLatch(1);
ExecutorService executorService = Executors.newFixedThreadPool(10);
for(int i=0; i<10; i++){
String userId = "userId" + i;
HttpEntity request = buildRequest(userId);
executorService.submit(() -> {
try {
countDownLatch.await();
System.out.println("Thread:"+Thread.currentThread().getName()+", time:"+System.currentTimeMillis());
ResponseEntity response = restTemplate.postForEntity(url, request, String.class);
System.out.println("Thread:"+Thread.currentThread().getName() + "," + response.getBody());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
countDownLatch.countDown();
}
private HttpEntity buildRequest(String userId) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "yourToken");
Map body = new HashMap<>();
body.put("userId", userId);
return new HttpEntity<>(body, headers);
}
}
以上代码源码均来自开演社区,寻找手艺人只是搬运到CSDN博客内了,就是方便大家查阅 汗颜~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~寻找手艺人