zoukankan      html  css  js  c++  java
  • 接口防重复提交的技术解决方案

    【本文完善中...】

     

    无论是http接口,还是rpc接口,防重复提交(接口防重)都是绕不过的话题。

    重复提交与幂等,既有区别,又有联系。幂等的意思是,对资源的一次请求与多次请求,作用是相同的。例如,HTTP的POST方法是非幂等的。如果程序处理不好,重复提交会导致非幂等,引起系统数据故障。防重复提交,当属于幂等的范畴,首先通过技术手段来实现,其次,又要有对业务数据的唯一性验证。

    常见的B/S场景的重复提交,用户手抖或因为网络问题,服务端在极短时间内两次甚至更多次收到同样的http请求。

    rpc接口的重复提交,一种是不恰当的程序调用,即程序漏洞导致重复提交。在一种,比如拿dubbo来说,因为网络传输问题,会触发重试调用。

    防重提交的方案,常见的是加锁。分布式系统,一般是借助redis或zk等分布式锁。对于java单体应用,有网友说可以用语言本身的synchronized锁机制,严格来说,这样是不恰当的,因为synchronized是多线程下的同步锁,只会阻塞线程执行,而不会阻断线程的执行。

    【说明几点】

    1.  lockKey的设置
    2. 锁的有效期
    3. 上锁的原子性
    4. 关于释放锁

     

    redis分布式锁的实现

    类图:

    RedisDistributedLock

    package com.emax.zhenghe.rpcapi.provider.config.distributeRedisLock;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    import java.util.Arrays;
    import java.util.UUID;
    import java.util.concurrent.TimeUnit;
    
    @Component
    @Slf4j
    public class RedisDistributedLock extends AbstractDistributedLock {
    
        @Autowired
        @Resource
        private RedisTemplate<Object, Object> redisTemplate;
    
        private ThreadLocal<String> lockFlag = new ThreadLocal<String>();
    
        public static final String UNLOCK_LUA;
    
        static {
            StringBuilder sb = new StringBuilder();
            sb.append("if redis.call("get",KEYS[1]) == ARGV[1] ");
            sb.append("then ");
            sb.append("    return redis.call("del",KEYS[1]) ");
            sb.append("else ");
            sb.append("    return 0 ");
            sb.append("end ");
            UNLOCK_LUA = sb.toString();
        }
    
    
        public RedisDistributedLock() {
            super();
        }
    
        @Override
        public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {
            boolean result = setRedis(key, expire);
            // 如果获取锁失败,按照传入的重试次数进行重试
            while ((!result) && retryTimes-- > 0) {
                try {
                    log.debug("lock failed, retrying..." + retryTimes);
                    Thread.sleep(sleepMillis);
                } catch (InterruptedException e) {
                    return false;
                }
                result = setRedis(key, expire);
            }
            return result;
        }
    
        /**
         *
         * @param key
         * @param expire MILLISECONDS
         * @return
         */
        private boolean setRedis(final String key, final long expire) {
            try {
                String uuid = UUID.randomUUID().toString();
                lockFlag.set(uuid);
                return redisTemplate.opsForValue().setIfAbsent(key,uuid,expire,TimeUnit.MILLISECONDS);
            } catch (Exception e) {
                log.info("redis lock error.", e);
            }
            return false;
        }
    
    
        @Override
        public boolean releaseLock(String key) {
            // 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
            try {
                DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript<Boolean>(UNLOCK_LUA,Boolean.class);
                return redisTemplate.execute(defaultRedisScript,Arrays.asList(key),lockFlag.get());
            } catch (Exception e) {
                log.error("release lock occured an exception", e);
            } finally {
                // 清除掉ThreadLocal中的数据,避免内存溢出
                lockFlag.remove();
            }
            return false;
        }
    }
        

    AbstractDistributedLock

    package com.emax.zhenghe.rpcapi.provider.config.distributeRedisLock;
    
    public abstract class AbstractDistributedLock implements DistributedLock {
     
        @Override
        public boolean lock(String key) {
            return lock(key , TIMEOUT_MILLIS, RETRY_TIMES, SLEEP_MILLIS);
        }
     
        @Override
        public boolean lock(String key, int retryTimes) {
            return lock(key, TIMEOUT_MILLIS, retryTimes, SLEEP_MILLIS);
        }
     
        @Override
        public boolean lock(String key, int retryTimes, long sleepMillis) {
            return lock(key, TIMEOUT_MILLIS, retryTimes, sleepMillis);
        }
     
        @Override
        public boolean lock(String key, long expire) {
            return lock(key, expire, RETRY_TIMES, SLEEP_MILLIS);
        }
     
        @Override
        public boolean lock(String key, long expire, int retryTimes) {
            return lock(key, expire, retryTimes, SLEEP_MILLIS);
        }
     
    }

    DistributedLock 

    package com.emax.zhenghe.rpcapi.provider.config.distributeRedisLock;
    
    public interface DistributedLock {
        
         long TIMEOUT_MILLIS = 30000;
        
         int RETRY_TIMES = 2;
        
         long SLEEP_MILLIS = 500;
        
         boolean lock(String key);
        
         boolean lock(String key, int retryTimes);
        
         boolean lock(String key, int retryTimes, long sleepMillis);
        
         boolean lock(String key, long expire);
        
         boolean lock(String key, long expire, int retryTimes);
        
         boolean lock(String key, long expire, int retryTimes, long sleepMillis);
        
         boolean releaseLock(String key);
    }

    进一步封装,实现代码解耦

    上面的加锁和释放锁都暴露在了业务调用方,增加了业务调用方的职责,同时,如果使用不当,还会产生bug。

    接下来,我们稍作重构。看看下面的RedisLockTemplate

    package com.emax.zhenghe.rpcapi.provider.config.distributeRedisLock;
    
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    /**
     * redis分布式锁并发控制模板类
     *
     * @author zhangguozhan
     */
    @Slf4j
    @Component
    public class RedisLockTemplate {
        @Autowired
        private RedisDistributedLock redisDistributedLock;
    
        /**
         * redis分布式锁控制
         *
         * @param key               锁名
         * @param expireMS          锁的生命周期,单位:毫秒
         * @param redisLockCallback callback方法
         * @return
         */
        public Object execute(String key, long expireMS, RedisLockCallback redisLockCallback) {
            return execute(key, expireMS, redisLockCallback, false, 2);
        }
    
        /**
         * redis分布式锁控制
         *
         * @param key
         * @param expireMS
         * @param redisLockCallback
         * @param isAutoReleaseLock callback方法执行完成后自动释放锁
         * @return
         */
        public Object execute(String key, long expireMS, RedisLockCallback redisLockCallback,
                              boolean isAutoReleaseLock,
                              int retryTimes) {
            log.info("redis分布式锁控制 key={}", key);
            if (StringUtils.isBlank(key)) {
                log.info("try lock failure:key is null");
                return null;
            }
            boolean lock = redisDistributedLock.lock(key, expireMS, retryTimes);
            if (lock) {
                try {
                    Object o = redisLockCallback.doInRedisLock();
                    return o;
                } finally {
                    if (isAutoReleaseLock) {
                        redisDistributedLock.releaseLock(key);
                    }
                }
            } else {
                log.info("###key已存在,终止 key={}", key);
                return null;
            }
    
        }
    }

    RedisLockCallback是一个函数式接口

    package com.emax.zhenghe.rpcapi.provider.config.distributeRedisLock;
    
    public interface RedisLockCallback {
        Object doInRedisLock();
    }

    这样,业务的调用就变得很easy了。

     

    关于ajax异步请求

     现在的web项目一般都是采用前后端分离的开发模式了,前端的程序框架也百花齐放,常见的有vue、nodejs等等。

     对于用户手抖导致的重复提交,服务端的做法就是利用上面的分布式控制,非首次的请求因为上锁失败而中断处理,前端收到的是“请勿重复提交”这样的提示。我原以为这样可能会影响用户体验。后来咨询前端同事,原来事实并非如此。

    自己写了一个demo,模拟重复提交。页面异步重复发起相同的请求,服务端重复处理。第一次是加锁,正常处理请求,第二次是发现锁已存在,上锁失败,直接返回“请勿重复提交”的提示。页面会收到两次的响应结果。不过,因为第二次的请求上锁失败直接返回错误提示,所以响应早于第一次的响应。ajax判断响应的逻辑是如果是成功(正常响应,视为成功),就触发相应的后续处理,如果是失败(“请勿重复提交”视为失败),就toast提示。 因此,虽然toast了一下,只是一瞬间,第一次请求的响应来了之后,就会正常处理页面逻辑。

    所以,上面的防重机制,也是比较合适的方案。

    当然,应该校验的业务逻辑还是要有的,尤其是数据校验。这属于业务范畴了。

    本文代码已放到github:

  • 相关阅读:
    Swift3 重写一个带占位符的textView
    Swift3 使用系统UIAlertView方法做吐司效果
    Swift3 页面顶部实现拉伸效果代码
    Swift3 倒计时按钮扩展
    iOS 获取当前对象所在的VC
    SpringBoot在IDEA下使用JPA
    hibernate 异常a different object with the same identifier value was already associated with the session
    SpringCloud IDEA 教学 番外篇 后台运行Eureka服务注册中心
    SpringCloud IDEA 教学 (五) 断路器控制台(HystrixDashboard)
    SpringCloud IDEA 教学 (四) 断路器(Hystrix)
  • 原文地址:https://www.cnblogs.com/buguge/p/13256485.html
Copyright © 2011-2022 走看看