接口并发幂等处理方案

407 阅读2分钟

目的

为了保证数据的一致性、唯一性,最简单有效的方式就是数据库表增加唯一约束,但不是所有场景都支持,为了能够支持所有的业务场景,还是需要在接口上面做文章

场景

1,WEB页面重复提交
2,feign接口超时自动重试
3,防接口并发

并发锁

前后端都需要做防并发控制 (基本做法就是使用Redis的分布式锁)

自定义并发锁注解

/**
 * 并发锁注解
 *
 * @author xiongyan
 * @date 2020/3/14
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ConcurrentLock {

    /**
     * key
     * 
     * @return
     */
    String key();

    /**
     * 参数,多个用“,”隔开
     *
     * @return
     */
    String param() default "";

    /**
     * 锁自动释放时间(单位秒)
     * 
     * @return
     */
    int lockTime() default 5;

}

并发锁切面

/**
 * 并发锁切面
 * 
 * @author xiongyan
 * @date 2020/3/13
 */
@Aspect
@Slf4j
@Component
public class ConcurrentLockAspect {

    @Pointcut(value = "@annotation(com.xxx.xxx.common.lock.ConcurrentLock)")
    public void concurrentLock() {
    }

    @Around("concurrentLock()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        ConcurrentLock concurrentLock = signature.getMethod().getAnnotation(ConcurrentLock.class);
        if (null == concurrentLock || StrUtil.isEmpty(concurrentLock.key())) {
            return joinPoint.proceed();
        }

        // 并发锁key
        String key = signature.getDeclaringTypeName() + "_" + signature.getName() + "_" + concurrentLock.key();
        if (StrUtil.isNotEmpty(concurrentLock.param())) {
            try {
                Object[] spelValue = SpelUtil.parseToArray(concurrentLock.param().split(","), joinPoint);
                if (null != spelValue) {
                    key = MessageFormat.format(key, spelValue);
                }
            } catch (Exception e) {
                log.warn("并发锁注解解析失败, key:{}, param:{}", concurrentLock.key(), concurrentLock.param(), e);
                return joinPoint.proceed();
            }
        }

        long lockTimes = concurrentLock.lockTime();
        RLock lock = RedisUtil.getLock(Md5Util.md5(key));
        // 获取锁
        if (lock.tryLock(0, lockTimes, TimeUnit.SECONDS)) {
            try {
                // 执行业务逻辑
                return joinPoint.proceed();
            } finally {
                // 释放锁
                if (lock.isLocked()) {
                    lock.forceUnlock();
                }
            }
        } else {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            log.warn("接口触发了并发锁,uri:{},params:{}", request.getRequestURI(), JacksonUtil.toJsonString(joinPoint.getArgs()));
            throw new ScmBizException("请稍后再试");
        }
    }

}

幂等锁

如果自己实现幂等就需要加表,把请求的数据先存入表,存在则直接返回成功

实现通用的幂等组件(借助于Redis的分布式锁和Redis持久化缓存),大大降低了业务的复杂度

自定义幂等锁注解

/**
 * 幂等锁注解
 *
 * @author xiongyan
 * @date 2020/3/14
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface IdempotentLock {

    /**
     * key
     * 
     * @return
     */
    String key();

    /**
     * 参数,多个用“,”隔开
     *
     * @return
     */
    String param() default "";

    /**
     * 锁自动释放时间(天)
     * 
     * @return
     */
    int lockTime() default 365;

}

幂等锁切面

/**
 * 幂等锁切面
 * 
 * @author xiongyan
 * @date 2020/3/13
 */
@Aspect
@Slf4j
@Component
public class IdempotentLockAspect {

    @Pointcut(value = "@annotation(com.xxx.xxx.common.lock.IdempotentLock)")
    public void idempotentLock() {
    }

    @Around("idempotentLock()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        IdempotentLock idempotentLock = signature.getMethod().getAnnotation(IdempotentLock.class);
        if (null == idempotentLock || StrUtil.isEmpty(idempotentLock.key())) {
            return joinPoint.proceed();
        }

        // 幂等锁key
        String key = signature.getDeclaringTypeName() + "_" + signature.getName() + "_" + idempotentLock.key();
        if (StrUtil.isNotEmpty(idempotentLock.param())) {
            try {
                Object[] spelValue = SpelUtil.parseToArray(idempotentLock.param().split(","), joinPoint);
                if (null != spelValue) {
                    key = MessageFormat.format(key, spelValue);
                }
            } catch (Exception e) {
                log.warn("幂等锁注解解析失败, key:{}, param:{}", idempotentLock.key(), idempotentLock.param(), e);
                return joinPoint.proceed();
            }
        }

        key = Md5Util.md5(key);
        RMapCache<String, Object> cache = RedisUtil.getMapCache("idempotent");
        Object result = cache.get(key);
        if (null != result) {
            return result;
        }

        RLock lock = RedisUtil.getLock(key);
        // 获取锁 (最大锁时间5秒后自动释放)
        if (lock.tryLock(0, 5, TimeUnit.SECONDS)) {
            try {
                // 执行业务逻辑
                result = joinPoint.proceed();

                // 放入缓存
                cache.put(key, result, idempotentLock.lockTime(), TimeUnit.DAYS);
                return result;
            } finally {
                // 释放锁
                if (lock.isLocked()) {
                    lock.forceUnlock();
                }
            }
        } else {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            log.warn("接口触发了并发锁,uri:{},params:{}", request.getRequestURI(), JacksonUtil.toJsonString(joinPoint.getArgs()));
            throw new ScmBizException("请稍后再试");
        }
    }

}