美文网首页springboot
spring boot通用“幂等”处理

spring boot通用“幂等”处理

作者: staconfree | 来源:发表于2017-11-23 16:27 被阅读175次

    需求

    同样一个请求连续发两遍(请求的参数可能有细微不一样,比如时间戳,但是对后台来说这应该属于同一个请求),想达到的目的是:两个请求同时到达的时候只有一个请求在执行,另外一个请求等待第一个请求结束,并返回相同结果。这就是幂等的意思。

    实现思路

    01
    显然这里,不同的请求需要共用同一把锁,才能实现上述要求,这里我们选择使用redisLock,关于使用redis锁,网上有很多文章,主要是使用redis的setnx方法。例如: Redis Java客户端jedis工具类以及Redis实现的跨jvm的锁
    02
    第二次请求需要返回第一次请求的结果,我们选择是将第一次请求的结果保存在redis里面,第二次请求直接取该结果,这里需要让第一次请求和第二次请求都能有同一个rediskey,所以需要程序根据配置解析出用户参数,并拼接在一起当做rediskey。
    这里我们想起了spring cache 的实现:

     @Cacheable(value = "demo-citycache8", key = "caches[0].name+'_'+#id")
    

    03
    我们采用方法注解@Idempotent的形式,如果同一个流程下来有多个方法多有我们的@Idempotent注解,我们希望只有最外层的@Idempotent启作用。所以我们还需要利用线程变量ThreadLocal
    04
    为了我们幂等功能能通用,我们选择新建一个专门的spring-boot-starter。如何新建starter请自行查找。

    核心代码

    新建注解

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Idempotent {
        /**
         * 幂等key的分组值,例如:sale-check
         * @return
         */
        String value();
    
        /**
         * 参数的表达式,用来确定key值,例如:"#req.saleInfo.channelCode+'-'+#req.seqId"
         * @return
         */
        String express();
    }
    

    利用aop做切面:

    @Component
    @Aspect
    @Order(-1)
    @Slf4j
    public class IdempotentAspectConfiguration {
    
        @Autowired  
        private RedisTemplate redisTemplate;  
    
    /**
         * 定义切入点为 带有 Idempotent 注解的
         */
        @Pointcut("@annotation(com.roy.idempotent.autoconfigure.Idempotent)")
        public void idempotent() {
        }
    
    @Around("idempotent()")
        public Object aroundMethod(ProceedingJoinPoint joinPoint){
            Object result = null;
            // joinPoint获取参数名
            String[] params = ((CodeSignature) joinPoint.getStaticPart().getSignature()).getParameterNames();
            // joinPoint获取参数值
            Object[] args = joinPoint.getArgs();
            // IdempotentFlag利用ThreadLocal是否正在进行中
            if (!IdempotentFlag.isDoing()) {
                try {
                    IdempotentFlag.doing();
                    Map<String,Object> map = new HashMap<String, Object>();
                    if (params!=null) {
                        for (int i=0;i<params.length;i++) {
                            map.put(params[i], args[i]);
                        }
                    }
                    Signature signature = joinPoint.getSignature();
                    MethodSignature methodSignature = (MethodSignature) signature;
                    Method method = methodSignature.getMethod();
                    Idempotent idempotent = method.getAnnotation(Idempotent.class);
                    // 类似springcache的写法,解析出express的实际值作为key
                    String key = getIdempotentKey(map, idempotent.express());
                    if (StringUtils.isEmpty(idempotent.value()) || StringUtils.isEmpty(key)) {
                        throw new Exception("idempotent config error.");
                    } else {
                        // 利用redisLock加锁获取结果
                        result = getResult(joinPoint, idempotent.value(), key);
                    }
                } catch (Throwable e) {
                    throw new RuntimeException(e);
                } finally {
                    IdempotentFlag.done();
                }
            } else {
                try {
                    result = joinPoint.proceed(args);
                } catch (Throwable e) {
                    throw new RuntimeException(e);
                }
            }
    
            return result;
        }
    }
    

    使用redisLock获取结果

    public Object getResult(ProceedingJoinPoint joinPoint, String idemGroup, String idemKey) throws Throwable {
            Object result = redisTemplate.opsForValue().get("com:roy:idmpotent:value:"+idemGroup+":"+idemKey);
            if (result==null) {
                SimpleRedisLock lock = new SimpleRedisLock(redisTemplate, "ha.net.idmpotent:lock:"+idemGroup+":"+idemKey, 10000, 20000);
                try {
                    if (lock.lock()) {
                        result = redisTemplate.opsForValue().get("com:roy:idmpotent:value:"+idemGroup+":"+idemKey);
                        if (result!=null) {
                            return result;
                        }
                        // 需要加锁的代码
                        result = joinPoint.proceed(joinPoint.getArgs());
                        if (result!=null){
                            // 结果缓存1分钟
                        redisTemplate.opsForValue().set("com:roy:idmpotent:value:"+idemGroup+":"+idemKey, result, 60, TimeUnit.SECONDS);
                        }
                    }
                } catch (Throwable e) {
                    throw e;
                } finally {
                    // 释放锁
                    lock.unlock();
                }
            }
            return result;
        }
    

    使用方法

    @ResponseBody
    @Idempotent(value = "updateCity",express = "#reqVo.id+'-'+#reqVo.cityInfo.code")
    public String updateCityNameByBodyVo(@RequestBody @Valid UpdateCityVO reqVo){
    }
    

    如上面的controller,模拟两个请求参数同时到达:
    {"id":1,"name":"深圳","cityInfo":{"code":"shenzhen"},"timestamp":12345}

    {"id":1,"name":"深圳2","cityInfo":{"code":"shenzhen"},"timestamp":123456}
    一个是把name值改成深圳,一个是把name值改成深圳2。

    虽然timestamp不一样,但是由于express中规定id和cityInfo.code拼接当做key值,两个请求这两参数都一样,所以会被识别为同一个请求。
    当第一个请求先到,第二个请求后到的时候,数据库的name值只会被改成深圳,而不会改成深圳2

    相关文章

      网友评论

      • 无问西东2683:感觉很不错,大佬,代码在github上有吗?我想跑起来试试,但不全。。
        无问西东2683:SimpleRedisLock 这个类没有,以及 getIdempotentKey 这个方法没有,能贴出来吗
      • 6644e64e287a:很不错,IdempotentFlag 主要实现的什么功能,能否贴下代码。谢谢!
        staconfree:public class IdempotentFlag {
        private static ThreadLocal<String> idempotent = new ThreadLocal<String>();
        public static void doing() {
        idempotent.set("1");
        }
        public static boolean isDoing() {
        if ("1".equals(idempotent.get())) {
        return true;
        }
        return false;
        }
        public static void done() {
        idempotent.remove();
        }
        }
      • 0941d3ecb9c0:支持
        staconfree:@人微风来 谢谢

      本文标题:spring boot通用“幂等”处理

      本文链接:https://www.haomeiwen.com/subject/uamzvxtx.html