赞
踩
表单/请求重复提交,不得不说幂等性。
幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次。
1.1、常见场景:
- • 比如订单接口, 不能多次创建订单
- • 支付接口, 重复支付同一笔订单只能扣一次钱
- • 支付宝回调接口, 可能会多次回调, 必须处理重复回调
- • 普通表单提交接口, 因为网络超时,卡顿等原因多次点击提交, 只能成功一次等等
解决思路:
解决方案大致总结为:
- - 唯一索引 -- 防止新增脏数据
- - token机制 -- 防止页面重复提交,实现接口幂等性校验
- - 分布式锁 -- redis(jedis、redisson)或zookeeper实现
比如你的项目是一个单独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:
- * @Author: lw
- * @Date: 2020/10/22
- */
- @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: lw
- * @Date: 2020/10/22
- */
- @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: lw
- * @Date: 2020/10/22
- */
-
- @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: lw
- * @Date: 2020/10/22
- */
- @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: lw
- * @Date: 2020/10/22
- */
- 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: lw
- * @Date: 2020/10/22
- */
- @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: lw
- * @Date: 2020/10/22
- */
- @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: lw
- * @Date: 2020/10/22
- */
- @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: lw
- */
- @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 分布式锁实现
- */
- @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 信息
- */
- 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:
- */
- @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项目中防止表单/请求重复提交的一个方案,分为单机和分布式环境下。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。