java开发中防止重复提交的几种解决方案
一、产生原因
对于重复提交的问题,主要由于重复点击或者网络重发请求, 我要先了解产生原因几种方式:
- 点击提交按钮两次;
- 点击刷新按钮;
- 使用浏览器后退按钮重复之前的操作,导致重复提交表单;
- 使用浏览器历史记录重复提交表单;
- 浏览器重复的HTTP请;
- nginx重发等情况;
- 分布式RPC的try重发等点击提交按钮两次;
- 等… …
二、幂等
对于重复提交的问题 主要涉及到时 幂等 问题,那么先说一下什么是幂等。
幂等:F(F(X)) = F(X)多次运算结果一致;简单点说就是对于完全相同的操作,操作一次与操作多次的结果是一样的。
在开发中,我们都会涉及到对数据库操作。例如:
select 查询天然幂等
delete 删除也是幂等,删除同一个多次效果一样
update 直接更新某个值(如:状态 字段固定值),幂等
update 更新累加操作(如:商品数量 字段),非幂等
(可以采用简单的乐观锁和悲观锁 个人更喜欢乐观锁。
乐观锁:数据库表加version字段的方式;
悲观锁:用了 select…for update 的方式,* 要使用悲观锁,我们必须关闭mysql数据库的自动提交属性。
这种在大数据量和高并发下效率依赖数据库硬件能力,可针对并发量不高的非核心业务;)
insert 非幂等操作,每次新增一条 重点 (数据库简单方案:可采取数据库唯一索引方式;这种在大数据量和高并发下效率依赖数据库硬件能力,可针对并发量不高的非核心业务;)
三、解决方案
1. 方案对比
序号 | 前端/后端 | 方案 | 优点 | 缺点 | 代码实现 |
---|---|---|---|---|---|
1) | 前端 | 前端js提交后禁止按钮,返回结果后解禁等 | 简单 方便 | 只能控制页面,通过工具可绕过不安全 | 略 |
2) | 后端 | 提交后重定向到其他页面,防止用户F5和浏览器前进后退等重复提交问题 | 简单 方便 | 体验不好,适用部分场景,若是遇到网络问题 还会出现 | 略 |
3) | 后端 | 在表单、session、token 放入唯一标识符(如:UUID),每次操作时,保存标识一定时间后移除,保存期间有相同的标识就不处理或提示 | 相对简单 | 表单:有时需要前后端协商配合; session、token:加大服务性能开销 | 略 |
4) | 后端 | ConcurrentHashMap 、LRUMap 、google Cache 都是采用唯一标识(如:用户ID+请求路径+参数) | 相对简单 | 适用于单机部署的应用 | 见下 |
5) | 后端 | redis 是线程安全的,可以实现redis分布式锁。设置唯一标识(如:用户ID+请求路径+参数)当做key ,value值可以随意(推荐设置成过期的时间点),在设置key的过期时间 | 单机、分布式、高并发都可以决绝 | 相对复杂需要部署维护redis | 见下 |
2. 代码实现
4). google cache 代码实现 注解方式 Single lock
pom.xml 引入
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>28.2-jre</version> </dependency>
配置文件 .yml
resubmit: local: timeOut: 30
实现代码
import java.lang.annotation.*; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface LocalLock { }
import com.alibaba.fastjson.JSONObject; import com.example.mydemo.common.utils.IpUtils; import com.example.mydemo.common.utils.Result; import com.example.mydemo.common.utils.SecurityUtils; import com.example.mydemo.common.utils.sign.MyMD5Util; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import lombok.Data; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; /** * @author: xx * @description: 单机放重复提交 */ @Data @Aspect @Configuration public class LocalLockMethodInterceptor { @Value("${spring.profiles.active}") private String springProfilesActive; @Value("${spring.application.name}") private String springApplicationName; private static int expireTimeSecond =5; @Value("${resubmit:local:timeOut}") public void setExpireTimeSecond(int expireTimeSecond) { LocalLockMethodInterceptor.expireTimeSecond = expireTimeSecond; } //定义缓存,设置最大缓存数及过期日期 private static final Cache<String,Object> CACHE = CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(expireTimeSecond, TimeUnit.SECONDS).build(); @Around("execution(public * *(..)) && @annotation(com.example.mydemo.common.interceptor.annotation.LocalLock)") public Object interceptor(ProceedingJoinPoint joinPoint){ MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); // LocalLock localLock = method.getAnnotation(LocalLock.class); try{ String key = getLockUniqueKey(signature,joinPoint.getArgs()); if(CACHE.getIfPresent(key) != null){ return Result.fail("不允许重复提交,请稍后再试"); } CACHE.put(key,key); return joinPoint.proceed(); }catch (Throwable throwable){ throw new RuntimeException(throwable.getMessage()); }finally { } } /** * 获取唯一标识key * * @param methodSignature * @param args * @return */ private String getLockUniqueKey(MethodSignature methodSignature, Object[] args) throws NoSuchAlgorithmException { //请求uri, 获取类名称,方法名称 RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; HttpServletRequest request = servletRequestAttributes.getRequest(); // HttpServletResponse responese = servletRequestAttributes.getResponse(); //获取用户信息 String userMsg = SecurityUtils.getUsername(); //获取登录用户名称 //1.判断用户是否登录 if (StringUtils.isEmpty(userMsg)) { //未登录用户获取真实ip userMsg = IpUtils.getIpAddr(request); } String hash = ""; List list = new ArrayList(); if (args.length > 0) { String[] parameterNames = methodSignature.getParameterNames(); for (int i = 0; i < parameterNames.length; i++) { Object obj = args[i]; list.add(obj); } hash = JSONObject.toJSONString(list); } //项目名称 + 环境编码 + 获取类名称 + 方法名称 + 唯一key String key = "locallock:" + springApplicationName + ":" + springProfilesActive + ":" + userMsg + ":" + request.getRequestURI(); if (StringUtils.isNotEmpty(key)) { key = key + ":" + hash; } key = MyMD5Util.getMD5(key); return key; }
使用:
@LocalLock public void save(@RequestBody User user) { }
5)redis
pom.xml 引入
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
.yml文件 redis 配置
spring: redis: host: localhost port: :6379 password: 123456
import java.lang.annotation.*; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface RedisLock { int expire() default 5; }
import com.alibaba.fastjson.JSONObject; import com.google.common.collect.Lists; import com.heshu.sz.blockchain.utonhsbs.common.utils.MyMD5Util; import com.heshu.sz.blockchain.utonhsbs.common.utils.SecurityUtils; import com.heshu.sz.blockchain.utonhsbs.common.utils.ip.IpUtils; import com.heshu.sz.blockchain.utonhsbs.framework.interceptor.annotation.RedisLock; import com.heshu.sz.blockchain.utonhsbs.framework.system.domain.BaseResult; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; 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.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; /** * @author :xx * @description: * @date : 2022/7/1 9:41 */ @Slf4j @Aspect @Configuration public class RedisLockMethodInterceptor { @Value("${spring.profiles.active}") private String springProfilesActive; @Value("${spring.application.name}") private String springApplicationName; @Autowired private StringRedisTemplate stringRedisTemplate; @Pointcut("@annotation(com.heshu.sz.blockchain.utonhsbs.framework.interceptor.annotation.RedisLock)") public void point() { } @Around("point()") public Object doaround(ProceedingJoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); RedisLock localLock = method.getAnnotation(RedisLock.class); try { String lockUniqueKey = getLockUniqueKey(signature, joinPoint.getArgs()); Integer expire = localLock.expire(); if (expire < 0) { expire = 5; } ArrayList<String> keys = Lists.newArrayList(lockUniqueKey); String result = stringRedisTemplate.execute(setNxWithExpireTime, keys, expire.toString()); if (!"ok".equalsIgnoreCase(result)) {//不存在 return BaseResult.error("不允许重复提交,请稍后再试"); } return joinPoint.proceed(); } catch (Throwable throwable) { throw new RuntimeException(throwable.getMessage()); } } /** * lua脚本 */ private RedisScript<String> setNxWithExpireTime = new DefaultRedisScript<>( "return redis.call('set', KEYS[1], 1, 'ex', ARGV[1], 'nx');", String.class ); /** * 获取唯一标识key * * @param methodSignature * @param args * @return */ private String getLockUniqueKey(MethodSignature methodSignature, Object[] args) throws NoSuchAlgorithmException { //请求uri, 获取类名称,方法名称 RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; HttpServletRequest request = servletRequestAttributes.getRequest(); // HttpServletResponse responese = servletRequestAttributes.getResponse(); //获取用户信息 String userMsg = SecurityUtils.getUsername(); //获取登录用户名称 //1.判断用户是否登录 if (StringUtils.isEmpty(userMsg)) { //未登录用户获取真实ip userMsg = IpUtils.getIpAddr(request); } String hash = ""; List list = new ArrayList(); if (args.length > 0) { String[] parameterNames = methodSignature.getParameterNames(); for (int i = 0; i < parameterNames.length; i++) { Object obj = args[i]; list.add(obj); } String param = JSONObject.toJSONString(list); hash = MyMD5Util.getMD5(param); } //项目名称 + 环境编码 + 获取类名称 + 加密参数 String key = "lock:" + springApplicationName + ":" + springProfilesActive + ":" + userMsg + ":" + request.getRequestURI(); if (StringUtils.isNotEmpty(key)) { key = key + ":" + hash; } return key; }
使用
@RedisLock public void save(@RequestBody User user) { }
总结
到此这篇关于java开发中防止重复提交的几种解决方案的文章就介绍到这了,更多相关java防止重复提交内容请搜索猪先飞以前的文章或继续浏览下面的相关文章希望大家以后多多支持猪先飞!
原文出处:https://blog.csdn.net/qq_33454058/article/details/125516310
相关文章
- 这篇文章主要介绍了如何利用java语言实现经典《复杂迷宫》游戏,文中采用了swing技术进行了界面化处理,感兴趣的小伙伴可以动手试一试...2022-02-01
java 运行报错has been compiled by a more recent version of the Java Runtime
java 运行报错has been compiled by a more recent version of the Java Runtime (class file version 54.0)...2021-04-01- 这篇文章主要介绍了在java中获取List集合中最大的日期时间操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-15
- 这篇文章主要介绍了教你怎么用Java获取国家法定节假日,文中有非常详细的代码示例,对正在学习java的小伙伴们有非常好的帮助,需要的朋友可以参考下...2021-04-23
- 这篇文章主要介绍了Java如何发起http请求的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-03-31
- 说起C#和Java这两门语言(语法,数据类型 等),个人以为,大概有90%以上的相似,甚至可以认为几乎一样。但是在工作中,我也发现了一些细微的差别...2020-06-25
- 这篇文章主要介绍了解决Java处理HTTP请求超时的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-03-29
- 这篇文章主要介绍了java 判断两个时间段是否重叠的案例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-15
- 这篇文章主要介绍了超简洁java实现双色球若干注随机号码生成(实例代码),本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-04-02
- 这篇文章主要介绍了Java生成随机姓名、性别和年龄的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-10-01
java 画pdf用itext调整表格宽度、自定义各个列宽的方法
这篇文章主要介绍了java 画pdf用itext调整表格宽度、自定义各个列宽的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-01-31- 这篇文章主要介绍了java正则表达式判断前端参数修改表中另一个字段的值,需要的朋友可以参考下...2021-05-07
Java使用ScriptEngine动态执行代码(附Java几种动态执行代码比较)
这篇文章主要介绍了Java使用ScriptEngine动态执行代码,并且分享Java几种动态执行代码比较,需要的朋友可以参考下...2021-04-15- 这篇文章主要介绍了Java开发实现人机猜拳游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-08-03
- 这篇文章主要介绍了Java List集合返回值去掉中括号('[ ]')的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-29
Java中lombok的@Builder注解的解析与简单使用详解
这篇文章主要介绍了Java中lombok的@Builder注解的解析与简单使用,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-01-06- 下面小编就为大家带来一篇java中String类型变量的赋值问题介绍。小编觉得挺不错的。现在分享给大家,给大家一个参考。...2016-03-28
Java 8 Stream 的终极技巧——Collectors 功能与操作方法详解
这篇文章主要介绍了Java 8 Stream Collectors 功能与操作方法,结合实例形式详细分析了Java 8 Stream Collectors 功能、操作方法及相关注意事项,需要的朋友可以参考下...2020-05-20- 这篇文章主要介绍了Java线程池中的各个参数如何合理设置操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-06-19
- 在Java中,我们可以利用多线程来最大化地压榨CPU多核计算的能力,下面这篇文章主要给大家介绍了关于java中多线程与线程池基本使用的相关资料,需要的朋友可以参考下...2021-09-13