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


    开场白:老铁们对于文章有错误、不准确,或需要补充的请留言讨论 ,大家共同学习。如果觉得还不错的请关注、留言、点赞 、收藏。 创作不易,且看且珍惜

    一、产生原因

    对于重复提交的问题,主要由于重复点击或者网络重发请求, 我要先了解产生原因几种方式:

    1. 点击提交按钮两次;
    2. 点击刷新按钮;
    3. 使用浏览器后退按钮重复之前的操作,导致重复提交表单;
    4. 使用浏览器历史记录重复提交表单;
    5. 浏览器重复的HTTP请;
    6. nginx重发等情况;
    7. 分布式RPC的try重发等点击提交按钮两次;
    8. 等… …

    二、幂等

    对于重复提交的问题 主要涉及到时 幂等 问题,那么先说一下什么是幂等。
    幂等: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>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    配置文件 .yml

    resubmit:
      local:
        timeOut: 30
    
    • 1
    • 2
    • 3

    实现代码

    import java.lang.annotation.*;
    
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    public @interface LocalLock {
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    
    
    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;
        }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117

    使用:

    	@LocalLock
        public void save(@RequestBody User user) {
           
        }
    
    • 1
    • 2
    • 3
    • 4

    5)redis
    pom.xml 引入

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4

    .yml文件 redis 配置

    spring:
      redis:
        host: localhost
        port: :6379
        password: 123456
    
    • 1
    • 2
    • 3
    • 4
    • 5
    import java.lang.annotation.*;
    
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    public @interface RedisLock {
    
        int expire() default 5;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125

    使用

    	@RedisLock
        public void save(@RequestBody User user) {
           
        }
    
    • 1
    • 2
    • 3
    • 4
  • 相关阅读:
    8.JavaScript-注释
    聚类方法总结及code
    zabbix企业监控
    NLP&KG&Others会议投稿
    QT+OSG/osgEarth编译之三十二:Exiv2+Qt编译(一套代码、一套框架,跨平台编译,版本:Exiv2-0.27.5)
    【代码笔记】高并发场景下问题解决思路
    【VUE】process.env,require,vite.config.js等问题的解决
    PMP项目管理 新考纲概述
    学生信息系统(python实现)
    为什么需要 Buffer Pool?
  • 原文地址:https://blog.csdn.net/qq_33454058/article/details/125516310