一些用户请求在某些情况下是可能重复发送的,如果是查询类操作并无大碍,但其中有些涉及写入操作,一旦重复了,可能会导致很严重的后果。例如交易接口如果重复请求,可能会重复下单。
   
   
    
重复的场景有可能是:
    
    
    
 
     
     
     String KEY = "REQ12343456788";//请求唯一编号
    long expireTime = 1000;// 1000毫秒过期,1000ms内的重复请求会认为重复
    long expireAt = System.currentTimeMillis() + expireTime;
    String val = "expireAt@" + expireAt;
    //redis key还存在的话要就认为请求是重复的
    Boolean firstSet = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(KEY.getBytes(), val.getBytes(), Expiration.milliseconds(expireTime), RedisStringCommands.SetOption.SET_IF_ABSENT));
    final boolean isConsiderDup;
    if (firstSet != null && firstSet) {// 第一次访问
        isConsiderDup = false;
    } else {// redis值已存在,认为是重复了
        isConsiderDup = true;
    
    
     
String KEY = "dedup:U="+userId + "M=" + method + "P=" + reqParam;
1、计算请求参数的摘要作为参数标识
    
    
    
 
     
     
     String KEY = 
 
     
     
     "dedup:U="+userId + 
 
     
     
     "M=" + method + 
 
     
     
     "P=" + reqParamMD5;
    
    
     
2、继续优化,考虑剔除部分时间因子
    
    
    
 
     
     
     //两个请求一样,但是请求时间差一秒
 
     
     
     
    
 
     
     
     String req = 
 
     
     
     "{\n" +
 
     
     
     
            
 
     
     
     "\"requestTime\" :\"20190101120001\",\n" +
 
     
     
     
            
 
     
     
     "\"requestValue\" :\"1000\",\n" +
 
     
     
     
            
 
     
     
     "\"requestKey\" :\"key\"\n" +
 
     
     
     
            
 
     
     
     "}";
 
     
     
     
 
     
     
     
    
 
     
     
     String req2 = 
 
     
     
     "{\n" +
 
     
     
     
            
 
     
     
     "\"requestTime\" :\"20190101120002\",\n" +
 
     
     
     
            
 
     
     
     "\"requestValue\" :\"1000\",\n" +
 
     
     
     
            
 
     
     
     "\"requestKey\" :\"key\"\n" +
 
     
     
     
            
 
     
     
     "}";
    
    
     
         
         
         
 
          
          
          public class ReqDedupHelper {
    /**
     *
     * @param reqJSON 请求的参数,这里通常是JSON
     * @param excludeKeys 请求参数里面要去除哪些字段再求摘要
     * @return 去除参数的MD5摘要
     */
    public String dedupParamMD5(final String reqJSON, String... excludeKeys) {
        String decreptParam = reqJSON;
        TreeMap paramTreeMap = JSON.parseObject(decreptParam, TreeMap.class);
        if (excludeKeys!=null) {
            List<String> dedupExcludeKeys = Arrays.asList(excludeKeys);
            if (!dedupExcludeKeys.isEmpty()) {
                for (String dedupExcludeKey : dedupExcludeKeys) {
                    paramTreeMap.remove(dedupExcludeKey);
                }
            }
        }
        String paramTreeMapJSON = JSON.toJSONString(paramTreeMap);
        String md5deDupParam = jdkMD5(paramTreeMapJSON);
        log.debug("md5deDupParam = {}, excludeKeys = {} {}", md5deDupParam, Arrays.deepToString(excludeKeys), paramTreeMapJSON);
        return md5deDupParam;
    }
    private static String jdkMD5(String src) {
        String res = null;
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            byte[] mdBytes = messageDigest.digest(src.getBytes());
            res = DatatypeConverter.printHexBinary(mdBytes);
        } catch (Exception e) {
            log.error("",e);
        }
        return res;
    }
}
         
         
          
    
    
    
 
     
     
     public static void main(String[] args) {
    //两个请求一样,但是请求时间差一秒
    String req = "{\n" +
            "\"requestTime\" :\"20190101120001\",\n" +
            "\"requestValue\" :\"1000\",\n" +
            "\"requestKey\" :\"key\"\n" +
            "}";
    String req2 = "{\n" +
            "\"requestTime\" :\"20190101120002\",\n" +
            "\"requestValue\" :\"1000\",\n" +
            "\"requestKey\" :\"key\"\n" +
            "}";
    //全参数比对,所以两个参数MD5不同
    String dedupMD5 = new ReqDedupHelper().dedupParamMD5(req);
    String dedupMD52 = new ReqDedupHelper().dedupParamMD5(req2);
    System.out.println("req1MD5 = "+ dedupMD5+" , req2MD5="+dedupMD52);
    //去除时间参数比对,MD5相同
    String dedupMD53 = new ReqDedupHelper().dedupParamMD5(req,"requestTime");
    String dedupMD54 = new ReqDedupHelper().dedupParamMD5(req2,"requestTime");
    System.out.println("req1MD5 = "+ dedupMD53+" , req2MD5="+dedupMD54);
}
    
    
     
    
    
    
 
     
     
     req1MD5 = 9E054D36439EBDD0604C5E65EB5C8267 , req2MD5=A2D20BAC78551C4CA09BEF97FE468A3F
req1MD5 = C2A36FED15128E9E878583CAAAFEFDE9 , req2MD5=C2A36FED15128E9E878583CAAAFEFDE9
    
    
     
- 
一开始两个参数由于 requestTime 是不同的,所以求去重参数摘要的时候可以发现两个值是不一样的; 
 - 
第二次调用的时候,去除了 requestTime 再求摘要(第二个参数中传入了”requestTime”),则发现两个摘要是一样的,符合预期。 
 
    
    
    
 
     
     
     String userId= "12345678";//用户
String method = "pay";//接口名
String dedupMD5 = new ReqDedupHelper().dedupParamMD5(req,"requestTime");//计算请求参数摘要,其中剔除里面请求时间的干扰
String KEY = "dedup:U=" + userId + "M=" + method + "P=" + dedupMD5;
long expireTime = 1000;// 1000毫秒过期,1000ms内的重复请求会认为重复
long expireAt = System.currentTimeMillis() + expireTime;
String val = "expireAt@" + expireAt;
// NOTE:直接SETNX不支持带过期时间,所以设置+过期不是原子操作,极端情况下可能设置了就不过期了,后面相同请求可能会误以为需要去重,所以这里使用底层API,保证SETNX+过期时间是原子操作
Boolean firstSet = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(KEY.getBytes(), val.getBytes(), Expiration.milliseconds(expireTime),
        RedisStringCommands.SetOption.SET_IF_ABSENT));
final boolean isConsiderDup;
if (firstSet != null && firstSet) {
    isConsiderDup = false;
} else {
    isConsiderDup = true;
}
    
    
     
作者:Jaskey Lam 
来源: 
http://jaskey.github.io/blog/2020/05/19/handle-duplicate-request/ 
        
           
           
           
         
            
            
            
          
             
             
             
           
              
              
              
          
             
             
              
          
             
             
             
           
              
              
              推荐阅读
          
             
             
              
          
             
             
             1. GitHub 上有什么好玩的项目?
          
             
             
             2. Linux 运维必备 150 个命令汇总
          
             
             
             3. SpringSecurity + JWT 实现单点登录
          
             
             
             4. 100 道 Linux 常见面试题
          
             
             
             
         
            
            
             
        
           
           
            
本文分享自微信公众号 - Java后端(web_resource)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
相关文章
暂无评论...

