/**
* 目的: 自定义切片,防止表单重复提交, 尤其是服务间调用,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;
        }
    }

}

 




 

 

 

posted on 2022-08-02 19:51  lshan  阅读(132)  评论(0编辑  收藏  举报