1. 主页 > 2018世界杯阿根廷 >

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 引入

com.google.guava

guava

28.2-jre

配置文件 .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 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 引入

org.springframework.boot

spring-boot-starter-data-redis

.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 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 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) {

}