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:

  • 相关阅读:
    一个 无私的 又不错的 博客
    byte[]与各种数据类型互相转换示例
    HttpClient之Get请求和Post请求示例
    jabberNet 发送出席信息
    jabberNet 修改花名册条目的昵称
    Openfire 配置连接SQL SERVER(非默认实例)
    WPF学习笔记——没有前途的WPF
    WPF学习笔记——设置ListBox选中项的背景颜色
    WPF学习笔记——为BUTTON添加背景图片
    EF + WCF学习笔记——EF实体类序列化
  • 原文地址:https://www.cnblogs.com/buguge/p/13256485.html
Copyright © 2011-2022 走看看