java开发中防止重复提交的几种解决方案

 更新时间:2022年10月20日 12:01:51   作者:暴力小熊  
我们日常开发中有很多的应用场景都会遇到重复提交问题,下面这篇文章主要给大家介绍了关于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防止重复提交内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解Java中Hibernate的基本原理

    详解Java中Hibernate的基本原理

    持久化是将程序数据在持久状态和瞬时状态间转换的机制。JDBC就是一种持久化机制。文件IO也是一种持久化机制。下面通过本文给大家介绍Java中Hibernate的基本原理,需要的朋友参考下吧
    2017-09-09
  • SpringCloud Bus消息总线的实现

    SpringCloud Bus消息总线的实现

    消息总线是一种通信工具,可以在机器之间互相传输消息、文件等,这篇文章主要介绍了SpringCloud Bus消息总线的实现,Spring cloud bus 通过轻量消息代理连接各个分布的节点,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-05-05
  • Mybatis实现数据的增删改查实例(CRUD)

    Mybatis实现数据的增删改查实例(CRUD)

    本篇文章主要介绍了Mybatis实现数据的增删改查实例(CRUD),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-05-05
  • java对接微信小程序详细流程(登录&获取用户信息)

    java对接微信小程序详细流程(登录&获取用户信息)

    这篇文章主要给大家介绍了关于java对接微信小程序(登录&获取用户信息)的相关资料,我们在开发微信小程序时经常需要获取用户微信用户名以及头像信息,微信提供了专门的接口API用于返回这些信息,需要的朋友可以参考下
    2023-08-08
  • java中使用interrupt通知线程停止详析

    java中使用interrupt通知线程停止详析

    这篇文章主要介绍了java中使用interrupt通知线程停止详析,文章介绍的是使用interrupt来通知线程停止运行,而不是强制停止,详细内容需要的小伙伴可以参考一下
    2022-09-09
  • 使用多个servlet时Spring security需要指明路由匹配策略问题

    使用多个servlet时Spring security需要指明路由匹配策略问题

    这篇文章主要介绍了使用多个servlet时Spring security需要指明路由匹配策略问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-08-08
  • Java Web项目中验证码功能的制作攻略

    Java Web项目中验证码功能的制作攻略

    使用servlet制作验证码中最关键的部分是缓存的使用,验证session中的字符串,接下来我们就来看一下Java Web项目中验证码功能的制作攻略
    2016-05-05
  • Java编程获取当前屏幕分辨率的方法示例

    Java编程获取当前屏幕分辨率的方法示例

    这篇文章主要介绍了Java编程获取当前屏幕分辨率的方法,涉及java针对系统硬件信息的相关操作技巧,需要的朋友可以参考下
    2017-08-08
  • SpringBoot集成Mybatis实现对多数据源访问原理

    SpringBoot集成Mybatis实现对多数据源访问原理

    本文主要分析讨论在SpringBoot应用中我们该如何配置SqlSessionFactoryBean对象,进而实现对多个不同的数据源的操纵,文章通过代码示例介绍的非常详细,需要的朋友可以参考下
    2023-11-11
  • 浅谈JVM 底层解析 i++和 ++i 区别

    浅谈JVM 底层解析 i++和 ++i 区别

    这篇文章主要介绍了JVM 底层解析 i++和 ++i 区别,需要的朋友可以参考下面文章的具体内容
    2021-09-09

最新评论