赞
踩
说起web项目的防止表单/请求重复提交,不得不说幂等性。
幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次。
• 订单接口, 不能多次创建订单
• 支付接口, 重复支付同一笔订单只能扣一次钱
• 支付宝回调接口, 可能会多次回调, 必须处理重复回调
• 普通表单提交接口, 因为网络超时,卡顿等原因多次点击提交, 只能成功一次等等
解决思路:
解决方案大致总结为:
- 唯一索引 -- 防止新增脏数据
- token机制 -- 防止页面重复提交,实现接口幂等性校验
- 分布式锁 -- redis(jedis、redisson)或zookeeper实现
- 悲观锁 -- 获取数据的时候加锁(锁表或锁行)
- 乐观锁 -- 基于版本号version实现, 在更新数据那一刻校验数据
- 状态机 -- 状态变更, 更新数据时判断状态
前三种方式最为常见,本文则从应用层面考虑,给出单机服务还是分布式服务下的解决方案。
比如你的项目是一个单独springboot项目,SSM项目,或者其他的单体服务,就是打个jar或者war直接扔服务器上跑的。
采用【AOP解析自定义注解+google的Cache缓存机制】来解决表单/请求重复的提交问题。
思路:
好了,接下里就是新建一个springboot项目,然后开整了。
需要新增一个google.common.cache.Cache;
源码如下:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>24.0-jre</version>
</dependency>
一个自定义注解接口类,是interface类哟 ,里面什么都不写,为了就是个重构。
源码如下:
package com.gitee.taven.aop; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @title: NoRepeatSubmit * @Description: 自定义注解,用于标记Controller中的提交请求 * @Author: ZouTao * @Date: 2020/4/14 */ @Target(ElementType.METHOD) // 作用到方法上 @Retention(RetentionPolicy.RUNTIME) // 运行时有效 public @interface NoRepeatSubmit { }
这是个AOP的解析注解类,使用到了Cache缓存机制。
以cache.getIfPresent(key)的url值来进行if判断,如果不为空,证明已经发过请求,那么在规定时间内的再次请求,视为无效,为重复请求。如果为空,则正常响应请求。
源码如下:
package com.gitee.taven.aop; import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; 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.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import com.google.common.cache.Cache; /** * @Description: aop解析注解-配合google的Cache缓存机制 * @Author: Zoutao * @Date: 2020/4/14 */ @Aspect @Component public class NoRepeatSubmitAop { private Log logger = LogFactory.getLog(getClass()); @Autowired private Cache<String, Integer> cache; @Pointcut("@annotation(noRepeatSubmit)") public void pointCut(NoRepeatSubmit noRepeatSubmit) { } @Around("pointCut(noRepeatSubmit)") public Object arround(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) { try { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); String sessionId = RequestContextHolder.getRequestAttributes().getSessionId(); HttpServletRequest request = attributes.getRequest(); String key = sessionId + "-" + request.getServletPath(); if (cache.getIfPresent(key) == null) {// 如果缓存中有这个url视为重复提交 Object o = pjp.proceed(); cache.put(key, 0); return o; } else { logger.error("重复请求,请稍后在试试。"); return null; } } catch (Throwable e) { e.printStackTrace(); logger.error("验证重复提交时出现未知异常!"); return "{\"code\":-889,\"message\":\"验证重复提交时出现未知异常!\"}"; } } }
用来获取缓存和设置有效期,目前设置有效期为2秒。
源码如下:
package com.gitee.taven; import java.util.concurrent.TimeUnit; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; /** * @Description: 内存缓存配置类 * @Author: Zoutao * @Date: 2020/4/14 */ @Configuration public class UrlCache { @Bean public Cache<String, Integer> getCache() { return CacheBuilder.newBuilder().expireAfterWrite(2L, TimeUnit.SECONDS).build();// 缓存有效期为2秒 } }
一个请求控制类,用来模拟响应请求和业务处理。
源码如下:
package com.gitee.taven.controller; import com.gitee.taven.ApiResult; import com.gitee.taven.aop.NoRepeatSubmit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @Description: 测试Cache方式的Controller * @Author: Zoutao * @Date: 2020/4/14 */ @RestController public class CacheTestController { private Object data; @RequestMapping("/TestSubmit") @NoRepeatSubmit() public Object test() { data = "程序逻辑返回,假设是一大堆DB来的数据。。。"; return new ApiResult(200, "请求成功",data); // 也可以直接返回。return (",请求成功,程序逻辑返回"); } }
ps:这里可以在建立一个ApiResult.java类,来规范返回的数据格式体:
ApiResult.java(非必须)
源码如下:
package com.gitee.taven; /** * @title: ApiResult * @Description: 统一规范结果格式 * @Param: code, message, data * @return: ApiResult * @Author: ZouTao * @Date: 2020/4/14 */ 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 + '}'; } }
纯粹为了规范而加的,你可以不用。
运行springboot项目,启动成功后,在浏览器输入:http://localhost:8080/TestSubmit
然后F5刷新(模拟重复发起请求),查看效果:
可以看到只有一次请求被成功响应,返回了数据,在有效时间内,其他请求被判定为重复提交,不予执行。
两个关键信息,第一:防止重复提交;第二:最简单。
前提是是单机环境下。
前端拦截是指通过 HTML 页面来拦截重复请求,比如在用户点击完“提交”按钮后,我们可以把按钮设置为不可用或者隐藏状态。
实现代码:
<html>
<script>
function subAdd(){
// 按钮设置为不可用
document.getElementById("btn_sub").disabled="disabled";
document.getElementById("dv1").innerText = "按钮被点击了~";
}
</script>
<body style="margin-top: 100px;margin-left: 100px;">
<input id="btn_sub" type="button" value="添 加" onclick="subAdd()">
<div id="dv1" style="margin-top: 80px;"></div>
</body>
</html>
执行效果:
但前端拦截有一个致命的问题,如果是懂行的程序员或非法用户可以直接绕过前端页面,所以除了前端拦截一部分正常的误操作之外,后端的拦截也是必不可少。
后端拦截的实现思路是在方法执行之前,先判断此业务是否已经执行过,如果执行过则不再执行,否则就正常执行。
提及将数据存储在内存中,最简单的方法就是使用 HashMap 存储,HashMap 的防重(防止重复)版本也是最原始的 。缺点是 HashMap 是无限增长的,因此它会占用越来越多的内存,并且随着 HashMap 数量的增加查找的速度也会降低,已不再推荐。
推荐使用最新的单例中著名的 DCL(Double Checked Locking,双重检测锁)来防止重复提交。
原理不需要深究,好在 Apache 为我们提供了一个 commons-collections 的框架,里面有一个非常好用的数据结构 LRUMap 可以保存指定数量的固定的数据,并且它就会按照 LRU 的算法,帮你清除最不常用的数据。
LRU 是 Least Recently Used 的缩写,即最近最少使用,是一种常用的数据淘汰算法,选择最近最久未使用的数据予以淘汰。
L R U M a p 版 防 止 重 复 提 交 方 案 : \color{red}{LRUMap版防止重复提交方案:} LRUMap版防止重复提交方案:
1.首先,我们先来添加 Apache commons collections 的引用:
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
本文已封装为一个公共的方法,以供所有类使用。
实现代码如下:
import org.apache.commons.collections4.map.LRUMap; /** * 幂等性判断 * 使用LRUMap。 */ public class IdempotentUtils { // 根据 LRU(Least Recently Used,最近最少使用)算法淘汰数据的 Map 集合,最大容量 100 个 private static LRUMap<String, Integer> reqCache = new LRUMap<>(100); /** * 幂等性判断 * @return */ public static boolean judge(String id, Object lockClass) { synchronized (lockClass) { // 重复请求判断 if (reqCache.containsKey(id)) { // 重复请求 System.out.println("请勿重复提交!!!" + id); return false; } // 非重复请求,存储请求 ID reqCache.put(id, 1); } return true; } }
调用代码:
import com.example.idempote.util.IdempotentUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RequestMapping("/user") @RestController public class UserController4 { @RequestMapping("/add") public String addUser(String id) { // 非空判断(忽略)... // -------------- 幂等性调用(开始) -------------- if (!IdempotentUtils.judge(id, this.getClass())) { return "执行失败"; } // -------------- 幂等性调用(结束) -------------- // 业务代码... System.out.println("添加用户ID:" + id); return "执行成功!"; } }
当然,熟悉spring注解的朋友,可以通过自定义注解,将业务代码写到注解中,需要调用的方法只需要写一行注解就可以防止数据重复提交了。
注意:
1.DCL 适用于重复提交频繁比较高的业务场景,对于相反的业务场景下 DCL 并不适用。
2.LRUMap 的本质是持有头结点的环回双链表结构,当使用元素时,就将该元素放在双链表 header 的前一个位置,在新增元素时,如果容量满了就会移除 header 的后一个元素。
效果:
上诉方式仅适用于单机环境下的重复数据拦截,如果是分布式环境需要配合数据库或 Redis 来实现。
如果你的spirngboot项目,后面要放到分布式集群中去使用,那么这个单体的Cache机制怕是会出问题,所以,为了解决项目在集群部署时请求可能会落到多台机器上的问题,我们把内存缓存换成了redis。
利用token机制+redis的分布式锁(jedis)来防止表单/请求重复提交。
思路如下:
打开application.properties或application.yml配置redis:
内容如下:
server.port=8080 # Redis数据库索引(默认为0) spring.redis.database=0 # Redis服务器地址 spring.redis.host=localhost # 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
pom.xml需要一些redis的依赖,使用Redis 是为了在负载均衡部署,
直接贴出整个项目的吧:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.gitee.taven</groupId> <artifactId>repeat-submit-intercept</artifactId> <version>0.0.1-SNAPSHOT</version> <name>repeat-submit-intercept</name> <description>Demo project for Spring Boot</description> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!--方式一:缓存类--> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>24.0-jre</version> </dependency> <!--方式二:redis类--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </exclusion> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
也是一个自定义注解,其中设置请求锁定时间。
package com.gitee.taven.aop; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @title: NoRepeatSubmit * @Description: 自定义注解,用于标记Controller中的提交请求 * @Author: ZouTao * @Date: 2020/4/14 */ @Target(ElementType.METHOD) // 作用到方法上 @Retention(RetentionPolicy.RUNTIME) // 运行时有效 public @interface NoRepeatSubmit { /* * 防止重复提交标记注解 * 设置请求锁定时间 * @return */ int lockTime() default 10; }
一个AOP解析注解类。
获取当前用户的Token(或者JSessionId)+ 当前请求地址,作为一个唯一 KEY,然后以Key去获取 Redis 分布式锁(如果此时并发获取,只有一个线程会成功获取锁。)
源码如下:
package com.gitee.taven.aop; import com.gitee.taven.ApiResult; import com.gitee.taven.utils.RedisLock; import com.gitee.taven.utils.RequestUtils; 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 javax.servlet.http.HttpServletRequest; import java.util.UUID; /** * @title: RepeatSubmitAspect * @Description: AOP类解析注解-配合redis-解决程序集群部署时请求可能会落到多台机器上的问题。 * 作用:对标记了@NoRepeatSubmit的方法进行拦截 * @Author: ZouTao * @Date: 2020/4/14 */ @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) { } /** * @title: RepeatSubmitAspect * @Description:在业务方法执行前,获取当前用户的 * token(或者JSessionId)+ 当前请求地址,作为一个唯一 KEY, * 去获取 Redis 分布式锁(如果此时并发获取,只有一个线程会成功获取锁。) * @Author: ZouTao * @Date: 2020/4/14 */ @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); } } // token(或者JSessionId)+ 当前请求地址,作为一个唯一KEY private String getKey(String token, String path) { return token + path; } // 生成uuid private String getClientId() { return UUID.randomUUID().toString(); } }
这是一个测试接口的请求控制类,模拟业务场景,
package com.gitee.taven.controller; import com.gitee.taven.ApiResult; import com.gitee.taven.aop.NoRepeatSubmit; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; /** * @title: SubmitController * @Description: 测试接口 * @Author: ZouTao * @Date: 2020/4/14 */ @RestController public class SubmitController { @PostMapping("submit") @NoRepeatSubmit() 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(); } } }
需要一个工具类来实现Redis分布式锁,具体实现原理请参考另外一篇文章。这里贴出源码。
新建RedisLock类,如下:
package com.gitee.taven.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; /** * @title: RedisLock * @Description: Redis 分布式锁实现 * @Author: ZouTao * @Date: 2020/4/14 */ @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<Boolean>) 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<Boolean>) 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工具类,用来获取一下getRequest的。
RequestUtils.java 如下:
package com.gitee.taven.utils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; /** * @title: RequestUtils * @Description: 获取 Request 信息 * @Author: ZouTao * @Date: 2020/4/14 */ public class RequestUtils { public static HttpServletRequest getRequest() { ServletRequestAttributes ra= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); return ra.getRequest(); } }
在上一个示例代码中,我们采用了启动项目,访问浏览器,手动测试的方式,接下里这个,
参考以前的一篇文章springboot启动项目自动运行测试方法,使用自动测试类来模拟测试。
模拟了10个并发请求同时提交:
package com.gitee.taven.test; 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; /** * @title: RunTest * @Description: 多线程测试类 * @Param: 模拟十个请求并发同时提交 * @return: * @Author: ZouTao * @Date: 2020/4/14 */ @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<String> 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<String, Object> body = new HashMap<>(); body.put("userId", userId); return new HttpEntity<>(body, headers); } }
启动项目,先启动redis,再运行springboot,会自动执行测试方法,然后控制台查看结果。
成功防止重复提交,控制台日志,可以看到十个线程的启动时间几乎同时发起,只有一个请求提交成功了。
ps:
有些人使用jedis3.1.0版本貌似已经没有这个set方法,则可以改为:
String result = jedis.set(lockKey, clientId, new SetParams().nx().px(seconds));
也ok了
整体项目结构图:
两套解决方案都在里面了,其中NoRepeatSubmit自定义注解类是共用的,区别在于有一个int lockTime()方法,不是使用redis的时候,注释掉即可。
上述就是SpringBoot/Web项目中防止表单/请求重复提交的一个方案,分为单机和分布式环境下。有什么疑问请留言吧。需要源码可以评论留下邮箱,后期会贴出git地址。
参考地址:
[1]: https://www.jianshu.com/p/09c6b05b670a
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。