• 分布式幂等1(基于一次性token) 自定义接口幂等(注解) @AvoidResubmit(isLoc = false)


    /**
    * 目的: 自定义切片,防止表单重复提交, 尤其是服务间调用,retry,防止重复提交数据
    * 1. 表单提交时 , 优先获取 一个token
    * 2. 提交表单时, 携带token 请求头 (header name: rdtc_token)
    * 3. 如果重复提交,响应: 重复提交 {"code":2500,message:"重复提交"}
    * eg:
    * 支付分为两个步骤:
    * 1.1 获取全局唯一token
    * 接口处理生成唯一标识(token) 存储到redis中,并返回给调用客户端。
    * 1.2 发起支付操作并附带token
    * 接口处理:
    * 1.2.1 获得分布式锁(处理并发情况)
    * 1.2.2 判断redis中是否存在token
    * 1.2.3 存在 执行支付业务逻辑,否则返回该订单已经支付
    * 1.2.4 释放分布式锁(自动)
    */

    主要逻辑解析:

     使用方法: 

    1.getToken  (请求前先获取token) 请求时header携带 token (  header name: rdtc_token )

        @GetMapping("getToken")
        public String getSubCode(Boolean isLoc){
            String token = ResubmitHandler.createToken(isLoc);
            return token;
        }

    2 .controller 层 接口方法上添加注解 (需要幂等的方法)

    @AvoidResubmit(isLoc = false)
    isLoc = true  单机:默认使用本地缓存实现, 
    isloc = flase  集群:则使用redis ,才是真正意义上的分布式幂等
        @PostMapping("add")
        @AvoidResubmit(isLoc = false)
        public JSONObject add(@RequestBody @Validated AgreeDO agree)
        {
            //doingreturn ResponseUtil.getResult(ResponseCode.SUCCESS.getCode(),ResponseCode.SUCCESS.getMessage(),null);
        }
     

    测试:   1.  请求前现获取一个token, 一次性token 

               请求时header携带 token (  header name: rdtc_token )   resubmit data  code  token

     curl -X POST "http://localhost:7213/agree/add" -H "accept: */*" -H "Content-Type: application/json" -H "rdtc_token:a369361156674bb496fc94ee49c84bd6_1659490855779"

    -d "{ \"avatar\": \"string\", \"remark\": \"string\", \"ts\": 0, \"type\": \"string\", \"typeId\": \"string\", \"userId\": \"string\", \"userName\": \"string\"}"

    如果再次提交: return  {"code":2500,"message":"数据重复提交"}

     

    配置: 添加如下两个类:

    AvoidResubmitHandler

    添加自定义注解

    /**
     * 自定义注解,用于是否做方重复提交数据检测   isLoc  表示是单节点,还是多个节点, 单机默认使用内存, 集群则 采用redis
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AvoidResubmit {
        boolean isLoc() default true;
    }
    添加 AvodiResubmitHandler
    /***************************
     *<pre>
     * @Project Name : sea-blog-service
     * @Package      : com.sea.xx.handler
     * @File Name    : AvoidResubmitHandler
     * @Author       :  Sea
     * @Date         : 8/5/22 10:53 AM
     * @Purpose      :
     * @History      :
     *</pre>
     ***************************/
    import com.alibaba.fastjson.JSONObject;
    import com.google.common.cache.Cache;
    import com.google.common.cache.CacheBuilder;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    import java.util.Date;
    import java.util.UUID;
    import java.util.concurrent.TimeUnit;
    /**
     * 依赖:
     *         <dependency>
     *               <groupId>org.springframework.boot</groupId>
     *               <artifactId>spring-boot-starter-data-redis</artifactId>
     *            </dependency>
     *             <dependency>
     *             <groupId>com.google.guava</groupId>
     *             <artifactId>guava</artifactId>
     *             <version>22.0</version>
     *         </dependency>
     */
    /***************************
     *<pre>
     * @Project Name : sea-blog-service
     * @Package      : com.sea.handler
     * @File Name    : ResubmitLock
     * @Author       :  Sea
     * @Date         : 8/2/22 3:25 PM
     * @Purpose      : 接口幂等
     * @History      :
     *</pre>
     ***************************/
    @Slf4j
    @Aspect
    @Component
    public class AvoidResubmitHandler{
        /**
         * @param joinPoint
         * @param avoidResubmit
         * @throws Throwable
         */
        @Around("@annotation(avoidResubmit)")
        public Object handleSeaAnnotionAOPMethod(ProceedingJoinPoint joinPoint, AvoidResubmit avoidResubmit) throws Throwable {
            ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            boolean isLoc = avoidResubmit.isLoc();
            String resubmitToken = sra.getRequest().getHeader(RESUBMIT_TOKEN_NAME);// uuid|ts
            if(!this.checkTokenExist(resubmitToken,isLoc)){
                //{"code":2500,message:"重复提交"}
                return new JSONObject(){{put("code",2500);put("message","数据重复提交");}};
            }
            Object object = joinPoint.proceed();
            //删除token
    //        delToken(resubmitToken,isLoc);
            return object;
        }
    
    
    
        // ################### base logic handler  ###################
        private static String  RESUBMIT_TOKEN_NAME = "rdtc_token";
        private static String  RESUBMIT_PREFIX =  "resubmit_";
        private static long   tokenTimeOutSec = 180;//3min
        private static Cache<String, Long> cacheLoc =  CacheBuilder.newBuilder().expireAfterWrite(tokenTimeOutSec, TimeUnit.SECONDS).build();
        //避免直接注入报错
        private static RedisTemplate<String,Object> redisTemplate ;//= getBean("redisTemplate", new RedisTemplate<String, Object>().getClass());;
        @Autowired
        public  void setRedisTemplate(RedisTemplate redisTemplate) {
            this.redisTemplate = redisTemplate;
        }
    
        /**
         *  token name [rdtc_token]  resubmit data code
         * 1.基于本地缓存 + token 实现防止重读提交
         * 2.基于redis + token
         * token 的设计  :  UUID+"|" 时间戳
         */
        public static String createToken(Boolean isLoc){
            String token =  UUID.randomUUID().toString().replaceAll("-","");
            long time = new Date().getTime();
            if(isLoc){cacheLoc.put(token,time);}
            else {
                redisTemplate.opsForValue().set(RESUBMIT_PREFIX+token,time+"",tokenTimeOutSec,TimeUnit.SECONDS);
            }
            return token+"_"+new Date().getTime();
        }
    
        /**
         * 删除token
         * @param token
         * @param isLoc
         * @return
         */
        public void delToken(String token,Boolean isLoc){
            try {
                if(token.contains("_")){token=token.split("_")[0];}
                if(isLoc){
                    cacheLoc.invalidate(token);}
                else {
                    redisTemplate.delete(RESUBMIT_PREFIX+token);
                }
            }catch (Exception e){
                e.printStackTrace();
                log.error(e+"");
            }
        }
    
    
    
        /**
         * token name [rdtc_token]  resubmit data code
         * token : UUID+时间戳 校验机制
         * 1.验证时间,if ts> 60s   return  not ok
         * 2.get token from cache, if not exist return no ok
         * @param token  uuid|ts
         * @param isLoc
         * @return  if exits return true
         */
        public Boolean checkTokenExist(String token , Boolean isLoc){
            if(StringUtils.isBlank(token)||!token.contains("_")){return false;}
            String[] split = token.split("_");
            token= split[0];
            //如果严重超时,直接返回失败
            if(new Date().getTime() - Long.valueOf(split[1])>180*1000){ return  false;}
            if(isLoc){return checkExistInLoc(token);}
            return checkExistInRedis(token);
        }
    
    
        /**
         * @param token
         * @return
         */
        private Boolean checkExistInLoc(String token){
            //类似分布式锁
            Boolean lock = MyNxLockUtils.getLock(token, "1");
            if(!lock){return  false;}
            Long value = cacheLoc.getIfPresent(token);
            if(value==null){return false;}
            return  true;
        }
    
        /**
         * @param token
         * @return
         */
        private Boolean checkExistInRedis(String token){
            try{
                Boolean lock = redisTemplate.opsForValue().setIfAbsent("lc" + token, "1", 10, TimeUnit.SECONDS);
                if(!lock){return false;}
                return redisTemplate.hasKey(RESUBMIT_PREFIX + token);
            }catch (Exception e){
                e.printStackTrace();
                log.error(e+"");
                return true;
            }
        }
    
    
        /**
         * 自定义loc类分布式锁
         */
        public static class MyNxLockUtils {
            private static  volatile String  lcPoint0="xx0", lcPoint1="xx1", lcPoint2="xx2" ,lcPoint3="xx3", lcPoint4="xx4",
                                             lcPoint5="xx5", lcPoint6="xx6", lcPoint7="xx7", lcPoint8="xx8", lcPoint9="xx9";
             // 分段枷锁,提升效率
            private static String getLcPoint(String k){
                switch (k.hashCode()%10){
                    case 1: return lcPoint1; case 2: return lcPoint2; case 3: return lcPoint3; case 4: return lcPoint4;
                    case 5: return lcPoint5; case 6: return lcPoint6; case 7: return lcPoint7; case 8: return lcPoint8;
                    case 9: return lcPoint9;default: return lcPoint0;
                }
            }
            private static Cache<String, String> lockCache = null;
            static {
                lockCache =  CacheBuilder.newBuilder().expireAfterWrite(8, TimeUnit.SECONDS).build();
            }
    
            /**
             * @param k
             * @param v
             * @return
             */
            public static Boolean getLock(String k, String v){
                if(lockCache.getIfPresent(k)==null)
                {
                    synchronized (getLcPoint(k)){
                        if(lockCache.getIfPresent(k)==null){
                            lockCache.put(k,v);
                            return true;
                        }}
                }
                return false;
            }
        }
    
    }
    
    
    


    
    
    
     
  • 相关阅读:
    前端思想实现:面向UI编程_____前端框架设计开发
    使用单体模式设计原生js插件
    QQ空间首页背景图片淡出解析与不足完善
    网页字体设置
    Asp.net MVC Session过期异常的处理
    日本设计的七个原则
    断开所有远程连接(sql server)
    Ubuntu1404+Django1.9+Apache2.4部署配置2配置文件设置
    Linux系统查找文件find命令使用(不断更新)
    ubuntu1404下Apache2.4错误日志error.log路径位置
  • 原文地址:https://www.cnblogs.com/lshan/p/16544988.html
Copyright © 2020-2023  润新知