Spring通过一个注解实现接口幂等

Spring通过一个注解实现接口幂等

    • 适用场景
    • 使用方式
    • 测试方式
    • 源码分析
      • 核心代码
    • 源码地址

适用场景

一个用户操作同一个接口请求参数一致的情况下只允许处理一次

使用方式

在需要做幂等的接口上添加@AccessLimit注解

测试方式

通过 jmeter 开一个线程组,多个线程同时调用同一个接口,分析结果是否处理了多次

源码分析

@AccessLimit 中现有的字段属性:
seconds 锁多少秒
retryCount 最大重试次数
needLogin 是否需要账号登陆

核心代码

    @Around("@annotation(com.youshang.annotation.AccessLimit)")
    public Object preHandle(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            RequestAttributes ra = RequestContextHolder.getRequestAttributes();
            ServletRequestAttributes sra = (ServletRequestAttributes) ra;
            HttpServletRequest request = sra.getRequest();
            String url = request.getRequestURL().toString();

            //获取当前注解信息
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method method = methodSignature.getMethod();
            AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);

            //获取注解自定义的参数信息
            int seconds = accessLimit.seconds();
            int concurrencyCount = accessLimit.retryCount();
            boolean login = accessLimit.needLogin();
            if (seconds <= 0) {
                throw new RuntimeException("加载时间不允许小于0");
            }

            //获取请求参数 并制作hashCode
            String queryString = "";
            if (null != request.getQueryString()) {
                queryString = request.getQueryString();
            }
            Object[] args = joinPoint.getArgs();
            String jsonStr = "";
            if (args != null && args.length > 0) {
                jsonStr = JSONObject.toJSONString(args);
            }
            int requestHashCode = jsonStr.concat(queryString).hashCode();

            //获取用户ID
            String userId = "";

            //key = 请求路径 + 操作用户ID + 请求参数的转String的hashCode值
            //key(String) 调用 intern 之后当前锁才会生效
            String key = (url + userId + requestHashCode).intern();
            synchronized (key) {
                Object value = redisTemplate.opsForValue().get(key);
                log.info("校验接口幂等:当前用户id信息:{},当前接口信息:{},当前key:{},当前redis-value:{}", userId, url, key, value);
                if (null == value) {
                    //当用户第一次访问时,存储当前用户信息+请求路径
                    redisTemplate.opsForValue().set(key, 0, seconds, TimeUnit.SECONDS);
                } else {
                    int accessCount = (int) value;
                    if (concurrencyCount > accessCount) {
                        log.info("当前幂等接口允许并发操作:key:{},最大并发量:{},目前并发量:{}", key, concurrencyCount, accessCount);
                        redisTemplate.opsForValue().increment(key, 1);
                    } else {
                        Long expire = redisTemplate.opsForValue().getOperations().getExpire(key);
                        throw new RuntimeException("当前操作正在进行中,请稍等 " + expire + " 秒后再试");
                    }
                }
            }
        }catch (RuntimeException e){
            throw new RuntimeException(e.getMessage());
        }catch (Exception e){
            log.error("接口幂等拦截失败,异常信息:{}", Throwables.getStackTraceAsString(e));
        }
        return joinPoint.proceed(joinPoint.getArgs());
    }

源码地址

https://gitee.com/liaojuhui/spring-learning.git
项目:spring-boot-idempotent

你可能感兴趣的:(spring,java,后端)