redission 防止重复提交

redission 防止重复提交

最近由于系统中的接口需要调用外部接口,接口响应时间过长,前端的小伙提交后也没有做处理,导致用户同一个按钮多次点击,导致数据重复提交,针对这个问题,整理了下重复提交问题。

产生原因

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

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

幂等

对于重复提交的问题 主要涉及到时 幂等 问题,那么先说一下什么是幂等。

幂等:F(F(X)) = F(X)多次运算结果一致;简单点说就是对于完全相同的操作,操作一次与操作多次的结果是一样的。

在开发中,我们都会涉及到对数据库操作。例如:

  • select 查询天然幂等
  • delete 删除也是幂等,删除同一个多次效果一样
  • update 直接更新某个值(如:状态 字段固定值),幂等
  • update 更新累加操作(如:商品数量 字段),非幂等
    (可以采用简单的乐观锁和悲观锁 个人更喜欢乐观锁。
    乐观锁:数据库表加version字段的方式;
    悲观锁:用了 select…for update 的方式,* 要使用悲观锁,我们必须关闭mysql数据库的自动提交属性。
    这种在大数据量和高并发下效率依赖数据库硬件能力,可针对并发量不高的非核心业务;)
  • insert 非幂等操作,每次新增一条 重点 (数据库简单方案:可采取数据库唯一索引方式;这种在大数据量和高并发下效率依赖数据库硬件能力,可针对并发量不高的非核心业务;)

解决方案

序号 前后端 方案 优点 缺点
1 前端 提交请求后,返回结果前提交按钮禁用 简单方便 只能前端控制,通过工具可以绕过,不安全
2 后端 提交后重定向到其他页面,防止用户F5和浏览器前进后退等重复提交问题 简单 方便 体验不好,适用部分场景,若是遇到网络问题 还会出现
3 后端 在表单、session、token 放入唯一标识符(如:UUID),每次操作时,保存标识一定时间后移除,保存期间有相同的标识就不处理或提示 相对简单 表单:有时需要前后端协商配合; session、token:加大服务性能开销
4 后端 ConcurrentHashMap 、LRUMap 、google Cache 都是采用唯一标识(如:用户ID+请求路径+参数) 相对简单 适用于单机部署的应用
5 后端 redis 是线程安全的,可以实现redis分布式锁。设置唯一标识(如:用户ID+请求路径+参数)当做key ,value值可以随意(推荐设置成过期的时间点),在设置key的过期时间 单机、分布式、高并发都可以决绝 相对复杂需要部署维护redis

java后台实现

这里通过redission实现

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    @Documented
    public @interface MesJRepeat{
    
        /**
         * 超时时间
         *
         * @return
         */
        int lockTime();
    
    
        /**
         * redis 锁key的
         *
         * @return redis 锁key
         */
        String lockKey() default "";
    
    
    }

    @Aspect
    @Component
    public class MesRepeatSubmitAspect extends BaseAspect {
    
        @Resource
        private RedissonLockClient redissonLockClient;
    
        /***
         * 定义controller切入点拦截规则,拦截JRepeat注解的业务方法
         */
        @Pointcut("@annotation(jRepeat)")
        public void pointCut(MesJRepeat jRepeat) {
        }
    
        /**
         * AOP分布式锁拦截
         *
         * @param joinPoint
         * @return
         * @throws Exception
         */
        @Around("pointCut(jRepeat)")
        public Object repeatSubmit(ProceedingJoinPoint joinPoint, MesJRepeat jRepeat) throws Throwable {
            String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod());
            if (Objects.nonNull(jRepeat)) {
                // 获取参数
                Object[] args = joinPoint.getArgs();
                // 进行一些参数的处理,比如获取订单号,操作人id等
                StringBuffer lockKeyBuffer = new StringBuffer();
                HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
                String reqestParams = getReqestParams(request, joinPoint);
                String key =getValueBySpEL(jRepeat.lockKey(), parameterNames, args,"RepeatSubmit").get(0) + reqestParams;
                // 公平加锁,lockTime后锁自动释放
                boolean isLocked = false;
                try {
                    isLocked = redissonLockClient.fairLock(key, TimeUnit.SECONDS, jRepeat.lockTime());
                    // 如果成功获取到锁就继续执行
                    if (isLocked) {
                        // 执行进程
                        return joinPoint.proceed();
                    } else {
                        // 未获取到锁
                        throw new ServiceException("请勿重复提交");
                    }
                } finally {
                    // 如果锁还存在,在方法执行完成后,释放锁
                    if (isLocked) {
                        redissonLockClient.unlock(key);
                    }
                }
            }
    
            return joinPoint.proceed();
        }
        
 		private String getReqestParams(HttpServletRequest request, JoinPoint joinPoint) {
            String httpMethod = request.getMethod();
            String params = "";
            if (CommonConstant.HTTP_POST.equals(httpMethod) || CommonConstant.HTTP_PUT.equals(httpMethod) || CommonConstant.HTTP_PATCH.equals(httpMethod)) {
                Object[] paramsArray = joinPoint.getArgs();
                Object[] arguments = new Object[paramsArray.length];
                for (int i = 0; i < paramsArray.length; i++) {
                    if (paramsArray[i] instanceof BindingResult || paramsArray[i] instanceof ServletRequest || paramsArray[i] instanceof ServletResponse || paramsArray[i] instanceof MultipartFile) {
                   
                        continue;
                    }
                    arguments[i] = paramsArray[i];
                }
                //update-begin-author:taoyan date:20200724 for:日志数据太长的直接过滤掉
                PropertyFilter profilter = new PropertyFilter() {
                    @Override
                    public boolean apply(Object o, String name, Object value) {
                        int length = 500;
                        if (value != null && value.toString().length() > length) {
                            return false;
                        }
                        return true;
                    }
                };
                params = JSONObject.toJSONString(arguments, profilter);
            } else {
                MethodSignature signature = (MethodSignature) joinPoint.getSignature();
                Method method = signature.getMethod();
                // 请求的方法参数值
                Object[] args = joinPoint.getArgs();
                // 请求的方法参数名称
                LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
                String[] paramNames = u.getParameterNames(method);
                if (args != null && paramNames != null) {
                    for (int i = 0; i < args.length; i++) {
                        params += "  " + paramNames[i] + ": " + args[i];
                    }
                }
            }
            return params;
        }
    
    }
/**
     * 分布式锁实现基于Redisson
     *
     * @date 2022-11-03
     */
    @Slf4j
    @Component
    public class RedissonLockClient {
    
        @Autowired
        private RedissonClient redissonClient;
    
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
    
        /**
         * 获取锁
         */
        public RLock getLock(String lockKey) {
            return redissonClient.getLock(lockKey);
        }
    
        /**
         * 加锁操作
         *
         * @return boolean
         */
        public boolean tryLock(String lockName, long expireSeconds) {
            return tryLock(lockName, 0, expireSeconds);
        }
    
    
        /**
         * 加锁操作
         *
         * @return boolean
         */
        public boolean tryLock(String lockName, long waitTime, long expireSeconds) {
            RLock rLock = getLock(lockName);
            boolean getLock = false;
            try {
                getLock = rLock.tryLock(waitTime, expireSeconds, TimeUnit.SECONDS);
                if (getLock) {
                    log.info("获取锁成功,lockName={}", lockName);
                } else {
                    log.info("获取锁失败,lockName={}", lockName);
                }
            } catch (InterruptedException e) {
                log.error("获取式锁异常,lockName=" + lockName, e);
                getLock = false;
            }
            return getLock;
        }
    
    
        public boolean fairLock(String lockKey, TimeUnit unit, int leaseTime) {
            RLock fairLock = redissonClient.getFairLock(lockKey);
            try {
                boolean existKey = existKey(lockKey);
                // 已经存在了,就直接返回
                if (existKey) {
                    return false;
                }
                return fairLock.tryLock(3, leaseTime, unit);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return false;
        }
    
        public boolean existKey(String key) {
            return redisTemplate.hasKey(key);
        }
        /**
         * 锁lockKey
         *
         * @param lockKey
         * @return
         */
        public RLock lock(String lockKey) {
            RLock lock = getLock(lockKey);
            lock.lock();
            return lock;
        }
    
        /**
         * 锁lockKey
         *
         * @param lockKey
         * @param leaseTime
         * @return
         */
        public RLock lock(String lockKey, long leaseTime) {
            RLock lock = getLock(lockKey);
            lock.lock(leaseTime, TimeUnit.SECONDS);
            return lock;
        }
    
    
        /**
         * 解锁
         *
         * @param lockName 锁名称
         */
        public void unlock(String lockName) {
            try {
                redissonClient.getLock(lockName).unlock();
            } catch (Exception e) {
                log.error("解锁异常,lockName=" + lockName, e);
            }
        }
    }
 @Slf4j
    public class BaseAspect {
    
        /**
         * 通过spring SpEL 获取参数
         *
         * @param key            定义的key值 以#开头 例如:#user
         * @param parameterNames 形参
         * @param values         形参值
         * @param keyConstant    key的常亮
         * @return
         */
        public List<String> getValueBySpEL(String key, String[] parameterNames, Object[] values, String keyConstant) {
            List<String> keys = new ArrayList<>();
            if (!key.contains("#")) {
                String s = "redis:lock:" + key + keyConstant;
                log.debug("lockKey:" + s);
                keys.add(s);
                return keys;
            }
            //spel解析器
            ExpressionParser parser = new SpelExpressionParser();
            //spel上下文
            EvaluationContext context = new StandardEvaluationContext();
            for (int i = 0; i < parameterNames.length; i++) {
                context.setVariable(parameterNames[i], values[i]);
            }
            Expression expression = parser.parseExpression(key);
            Object value = expression.getValue(context);
            if (value != null) {
                if (value instanceof List) {
                    List value1 = (List) value;
                    for (Object o : value1) {
                        addKeys(keys, o, keyConstant);
                    }
                } else if (value.getClass().isArray()) {
                    Object[] obj = (Object[]) value;
                    for (Object o : obj) {
                        addKeys(keys, o, keyConstant);
                    }
                } else {
                    addKeys(keys, value, keyConstant);
                }
            }
            log.info("表达式key={},value={}", key, keys);
            return keys;
        }
    
        private void addKeys(List<String> keys, Object o, String keyConstant) {
            keys.add("redis:lock:" + o.toString() + keyConstant);
        }
    }

测试接口:

   @RestController
    @RequestMapping("/hello")
    public class HelloController {
        @MesJRepeat(lockKey = "test", lockTime = 60)
        @GetMapping("/test")
        public Result<Integer> test(Long str) throws Exception {
            // 业务代码
            Thread.sleep(3000);
            return Result.OK(str + " - 操作成功");
        }
    }

测试结果:
使用jmeter测试工具测试,发现多次请求的话,只会有一次成功
redission 防止重复提交_第1张图片

redission 防止重复提交_第2张图片

你可能感兴趣的:(redis,springboot,java,开发语言,spring,boot)