简单的幂等性校验

简单的幂等性校验

1. 需求

项目开发过程中,发现有部分接口数据操作不正确,出现数据重复问题,最后发现是接口连点发生了重复数据生成,所以此处我们新增了一个简易的防连点幂等校验功能,主要是使用redis以及spring aop切面进行的实现。

2. 具体实现逻辑

  • 拦截用户发起请求
  • 获取请求中的请求路径以及用户携带token信息,确定唯一用户单一功能点击
  • 使用获取信息进行redis加锁【加锁带有超时时间】,如果加锁成功正常执行后续操作,失败进行用户连点提示
  • 执行完毕、超过、执行报错抛出异常情况下,自动释放锁,方便用户后续再次进行此功能操作

3. 实现代码

代码部分主要包含三部分,切面部分,注解部分以及redis锁部分,具体代码实现如下

  • 注解部分

    package cn.git.common.idempotent;
    
    import cn.git.common.lock.LockTypeEnum;
    
    import java.lang.annotation.*;
    
    /**
     * @program: bank-credit-sy
     * @description: 接口幂等性注解
     * @author: wanglianli
     * @create: 2021-02-26 08:57
     */
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface ApiIdempotent {
    
        /**
         * 幂等操作枚举类型
         * @return
         */
        LockTypeEnum lockTypeEnum() default LockTypeEnum.BASE_SERVER_IDEMPOTENCE;
    
    }
    
    
  • 切面部分

    package cn.git.common.idempotent;
    
    import cn.git.common.constant.HeaderConstant;
    import cn.git.common.exception.ServiceException;
    import cn.git.common.lock.LockTypeEnum;
    import cn.git.common.lock.SimpleDistributeLock;
    import cn.git.common.log.LogOperateProperties;
    import cn.hutool.core.util.ObjectUtil;
    import cn.hutool.core.util.StrUtil;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import javax.servlet.http.HttpServletRequest;
    
    /**
     * @program: bank-credit-sy
     * @description: 幂等性切面实现
     * @author: wanglianli
     * @create: 2021-03-02 10:40
     */
    @Aspect
    @Slf4j
    @Component
    public class IdempotentAspect {
    
        /**
         * 幂等加锁value值
         */
        private static final ThreadLocal<String> IDEMPOTENT_VALUE = new ThreadLocal<>();
    
        /**
         * 设置幂等加锁值
         * @param idempotentValue 值信息
         */
        private void setIdempotentValue(String idempotentValue) {
            if (StrUtil.isNotBlank(idempotentValue)) {
                IDEMPOTENT_VALUE.set(idempotentValue);
            }
        }
    
        /**
         * 获取幂等加锁值
         * @return 加锁值信息
         */
        private String getIdempotentValue() {
            return IDEMPOTENT_VALUE.get();
        }
    
        /**
         * 删除幂等加锁值信息
         */
        private void removeIdempotentValue() {
            IDEMPOTENT_VALUE.remove();
        }
    
        @Autowired
        private SimpleDistributeLock simpleDistributeLock;
    
        /**
         * 前置切面
         * @param joinPoint
         */
        @Before("@annotation(apiIdempotent)")
        public void beforePointCut(JoinPoint joinPoint, ApiIdempotent apiIdempotent) {
            // 获取请求url信息以及token信息,并且进行幂等加锁
            String lockKeySuffix = getLockKeySuffix();
    
            // 幂等类型
            LockTypeEnum lockTypeEnum = LockTypeEnum.BASE_SERVER_IDEMPOTENCE;
            if (ObjectUtil.isNotNull(apiIdempotent)) {
                lockTypeEnum = apiIdempotent.lockTypeEnum();
            }
    
            // 开始进行加锁
            String lockResult = simpleDistributeLock.tryLock(lockTypeEnum, lockKeySuffix);
    
            // 如果返回加锁信息不为空,则加锁成功
            if (StrUtil.isBlank(lockResult)) {
                log.error("幂等请求加锁失败,加锁key信息为[{}]", lockKeySuffix);
                throw new ServiceException(StrUtil.format("幂等校验失败,请勿重复快速点击!", lockKeySuffix));
            } else {
                // 设置线程信息,后续解锁使用
                setIdempotentValue(lockResult);
            }
        }
    
        /**
         * 后置切面
         * @param joinPoint
         */
        @AfterReturning("@annotation(apiIdempotent)")
        public void afterReturning(JoinPoint joinPoint, ApiIdempotent apiIdempotent) {
            try {
                // 获取加锁key后缀信息
                String lockKeySuffix = getLockKeySuffix();
                if (StrUtil.isBlank(lockKeySuffix)) {
                    log.error("后置切面获取加锁后缀值信息失败!");
                    return;
                }
    
                // 幂等类型
                LockTypeEnum lockTypeEnum = LockTypeEnum.BASE_SERVER_IDEMPOTENCE;
                if (ObjectUtil.isNotNull(apiIdempotent)) {
                    lockTypeEnum = apiIdempotent.lockTypeEnum();
                }
    
                // 获取线程加锁value信息
                String idempotentValue = getIdempotentValue();
                if (ObjectUtil.isNotNull(idempotentValue)) {
                    // 释放锁信息
                    simpleDistributeLock.releaseLock(lockTypeEnum, lockKeySuffix, idempotentValue);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 释放设定值信息
                removeIdempotentValue();
            }
        }
    
        /**
         * 后置异常切面 捕获异常信息
         * @param joinPoint joinPoint
         * @param e 异常信息
         */
        @AfterThrowing(value = "@annotation(apiIdempotent)" , throwing = LogOperateProperties.LOG_EXCEPTION)
        public void afterThrowing(JoinPoint joinPoint, ApiIdempotent apiIdempotent, Exception e) {
            try {
                // 获取加锁key后缀信息
                String lockKeySuffix = getLockKeySuffix();
                if (StrUtil.isBlank(lockKeySuffix)) {
                    log.error("后置切面获取加锁后缀值信息失败!");
                    return;
                }
    
                // 幂等类型
                LockTypeEnum lockTypeEnum = LockTypeEnum.BASE_SERVER_IDEMPOTENCE;
                if (ObjectUtil.isNotNull(apiIdempotent)) {
                    lockTypeEnum = apiIdempotent.lockTypeEnum();
                }
    
                // 获取线程加锁value信息
                String idempotentValue = getIdempotentValue();
                if (ObjectUtil.isNotNull(idempotentValue)) {
                    // 释放锁信息
                    simpleDistributeLock.releaseLock(lockTypeEnum, lockKeySuffix, idempotentValue);
                    // 释放线程值信息
                    removeIdempotentValue();
                }
            } catch (Exception exception) {
                exception.printStackTrace();
            } finally {
                // 释放设定值信息
                removeIdempotentValue();
            }
        }
    
        /**
         * 获取幂等性锁后缀信息
         * @return 后缀 信息
         */
        public String getLockKeySuffix() {
            // 获取请求url信息以及token信息
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (ObjectUtil.isNotNull(attributes) && ObjectUtil.isNotNull(attributes.getRequest())) {
                HttpServletRequest request = attributes.getRequest();
                // 设置锁key信息
                return request.getRequestURI()
                        .concat(StrUtil.COLON)
                        .concat(request.getHeader((HeaderConstant.HEADER_TOKEN)));
            }
            return null;
        }
    }
    
    
  • redis锁部分,包含加锁以及释放锁

    package cn.git.common.lock;
    
    import cn.git.redis.RedisUtil;
    import cn.hutool.core.util.IdUtil;
    import cn.hutool.core.util.ObjectUtil;
    import cn.hutool.core.util.StrUtil;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import org.springframework.data.redis.core.script.RedisScript;
    import org.springframework.stereotype.Component;
    
    import java.util.Collections;
    
    /**
     * 简单分布式锁
     * todo: 暂时未实现重入,阻塞队列等锁定功能后续添加
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2022-04-25
     */
    @Slf4j
    @Component
    public class SimpleDistributeLock {
    
        /**
         * LUA解锁脚本
         */
        private static final String UNLOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    
        /**
         * 释放锁失败标识
         */
        private static final long RELEASE_OK_FLAG = 0L;
    
        @Autowired
        private RedisUtil redisUtil;
    
        /**
         * 加锁方法
         * @param lockTypeEnum 锁信息
         * @param customKey 自定义锁定key
         * @return true 成功,false 失败
         */
        public String tryLock(LockTypeEnum lockTypeEnum, String customKey) {
            // 锁对应值信息
            String lockValue = IdUtil.simpleUUID();
            // 对自定义key进行加锁操作,value值与key值相同
            boolean result = redisUtil.setNxEx(lockTypeEnum.getLockType().concat(StrUtil.COLON).concat(customKey),
                    lockValue,
                    lockTypeEnum.getExpireTime().intValue());
            if (result) {
                log.info("[{}]加锁成功!", lockTypeEnum.getLockType().concat(StrUtil.COLON).concat(customKey));
                return lockValue;
            }
    
            return null;
        }
    
        /**
         * 解锁操作
         * @param lockTypeEnum 锁定类型
         * @param customKey 自定义key
         * @param releaseValue 释放value
         * @return true 成功,false 失败
         */
        public boolean releaseLock(LockTypeEnum lockTypeEnum, String customKey, String releaseValue) {
            // 各个模块服务启动时间差,预留5秒等待时间,防止重调用
            if (ObjectUtil.isNotNull(lockTypeEnum.getLockedWaitTimeMiles())) {
                try {
                    Thread.sleep(lockTypeEnum.getLockedWaitTimeMiles());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
            // 设置释放锁定key,value值
            String releaseKey = lockTypeEnum.getLockType().concat(StrUtil.COLON).concat(customKey);
    
            // 释放锁定资源
            RedisScript<Long> longDefaultRedisScript = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
            Long result = redisUtil.executeLuaCustom(longDefaultRedisScript, Collections.singletonList(releaseKey), releaseValue);
            // 根据返回结果判断是否成功成功匹配并删除 Redis 键值对,若果结果不为空和0,则验证通过
            if (ObjectUtil.isNotNull(result) && result != RELEASE_OK_FLAG) {
                log.info("[{}]释放锁成功!", releaseKey);
                return true;
            } else {
                log.error("[{}]释放锁失败!", releaseKey);
                return false;
            }
        }
    
    }
    
    
  • 其他部分工具类代码
    redisUtil加锁释放锁实现

        /**
         * 如果不存在,则设置对应key,value 键值对,并且设置过期时间
         * @param key 锁key
         * @param value 锁值
         * @param time 时间单位second
         * @return 设定结果
         */
        public Boolean setNxEx(String key, String value, long time) {
            return redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
        }
    
        /**
         * @Description: 执行lua脚本,只对key进行操作
         * @Param: [redisScript, keys, value]
         * @return: java.lang.Long
         * @Date: 2021/2/21 15:00
         */
        public Long executeLuaCustom(RedisScript<Long> redisScript, List keys, Object value) {
            return redisTemplate.execute(redisScript, keys, value);
        }
    

    加锁参数枚举类

    package cn.git.common.lock;
    
    import lombok.Getter;
    
    /**
     * 分布式锁类型设定enum
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2022-04-25
     */
    @Getter
    public enum LockTypeEnum {
    
        /**
         * 分布式锁类型详情
         */
        DISTRIBUTE_TASK_LOCK("DISTRIBUTE_TASK_LOCK", 120L, "xxlJob初始化分布式锁", 5000L),
        CACHE_INIT_LOCK("CACHE_INIT_LOCK", 120L, "缓存平台初始化缓存信息分布式锁", 5000L),
        RULE_INIT_LOCK("RULE_INIT_LOCK", 120L, "规则引擎规则加载初始化", 5000L),
        SEQUENCE_LOCK("SEQUENCE_LOCK", 120L, "序列信息月末初始化!", 5000L),
        UAA_ONLINE_NUMBER_LOCK("UAA_ONLINE_LOCK", 20L, "登录模块刷新在线人数", 5000L),
        BASE_SERVER_IDEMPOTENCE("BASE_IDEMPOTENCE_LOCK", 15L, "基础业务幂等性校验"),
        WORK_FLOW_WEB_SERVICE_LOCK("WORK_FLOW_WEB_SERVICE_LOCK", 15L, "流程webService服务可用ip地址获取锁", 5000L),
        DRAG_RULE_INIT_LOCK("DRAG_RULE_INIT_LOCK", 120L, "拖拽规则详情初始化锁", 5000L),
        DRAG_RULE_URI_LOCK("DRAG_RULE_URI_LOCK", 120L, "拖拽规则规则链初始化锁", 5000L),
        ;
    
        /**
         * 锁类型
         */
        private String lockType;
    
        /**
         * 即过期时间,单位为second
         */
        private Long expireTime;
    
        /**
         * 枷锁成功后,默认等待时间,时间应小于过期时间,单位毫秒
         */
        private Long lockedWaitTimeMiles;
    
        /**
         * 描述信息
         */
        private String lockDesc;
    
        /**
         * 构造方法
         * @param lockType 类型
         * @param lockTime 锁定时间
         * @param lockDesc 锁描述
         */
        LockTypeEnum(String lockType, Long lockTime, String lockDesc) {
            this.lockDesc = lockDesc;
            this.expireTime = lockTime;
            this.lockType = lockType;
        }
    
        /**
         * 构造方法
         * @param lockType 类型
         * @param lockTime 锁定时间
         * @param lockDesc 锁描述
         * @param lockedWaitTimeMiles 锁失效时间
         */
        LockTypeEnum(String lockType, Long lockTime, String lockDesc, Long lockedWaitTimeMiles) {
            this.lockDesc = lockDesc;
            this.expireTime = lockTime;
            this.lockType = lockType;
            this.lockedWaitTimeMiles = lockedWaitTimeMiles;
        }
    }
    
    

4. 调用方式

想使用也非常简单,如果是特定功能,可以在枚举中新增自己的加锁枚举参数,也可以使用默认的加锁枚举类型,然后只需要在controller方法上加上@ApiIdempotent注解即可
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值