对于某些用户请求,在某些情况下可能会重复发送。如果是查询操作是没有问题的,但是其中有一些涉及到写操作。一旦重复,可能会造成非常严重的后果,比如重复请求,交易接口可能会重复下单。重复的场景可能是:1.黑客拦截请求并重放2.前端/客户端请求由于某种原因重复发送,或者用户短时间内重复点击3.网关重新发送4.……本文讨论如何在服务器端优雅统一地处理这种情况。如何禁止用户重复点击等客户端操作不在本文讨论范围之内。使用唯一请求号去重可能会认为只要请求有唯一请求号,那么就可以用Redis来做这个去重——只要唯一请求号在redis中存在,并且证明有处理完毕,则认为是重复代码如下:StringKEY="REQ12343456788";//请求一个唯一的数字longexpireTime=1000;//1000毫秒后过期,1000ms内重复请求会被认为是重复longexpireAt=System.currentTimeMillis()+expireTime;Stringval="expireAt@"+expireAt;//如果rediskey仍然存在,则将请求视为重复的Boolean(过期时间),RedisStringCommands.SetOption.SET_IF_ABSENT));最终布尔值isConsiderDup;if(firstSet!=null&&firstSet){//第一次访问isConsiderDup=false;}else{//redis值已经存在,我认为是isConsiderDup=true的重复;}更多Java面试题和Java学习资料免费领取业务参数去重。上述方案可以解决请求编号唯一的场景。例如,服务端在每次写请求前返回一个唯一的编号给客户端,客户端用这个请求号发出请求,服务端就可以完成去重拦截。但是在很多场景下,请求中并没有携带这样的唯一编号!那么我们是否可以将请求参数识别为请求呢?先考虑一个简单的场景,假设请求参数只有一个字段reqParam,我们可以通过下面的flags来判断请求是否重复。用户ID:接口名称:请求参数StringKEY="dedup:U="+userId+"M="+method+"P="+reqParam;那么当同一个用户访问同一个界面时,他们带着相同的reqParam,我们可以定位到他是重复的。但问题是我们的界面通常不是那么简单。在目前的主流中,我们的参数通常是一个JSON。那么对于这种场景,我们该如何淡化呢?计算请求参数的汇总作为参数标识假设我们将请求参数(JSON)按KEY升序排列,然后拼装成字符串作为KEY值?但是这个可能会很长,所以我们可以考虑求一个这个字符串的MD5摘要作为参数,用这个摘要来代替reqParam的位置。StringKEY="dedup:U="+userId+"M="+method+"P="+reqParamMD5;这样就标记了请求的唯一标识!注意:MD5理论上可以重复,但去重通常是在很短的时间窗口(比如一秒)内去重。同一用户的同一界面,短时间内拼出不同的参数,导致MD5相同,几乎是不可能的。可能的。继续优化,考虑消除上面一些时间因素的问题其实是一个很好的解决方案,但是实际投入使用的时候可能会发现一些问题:一些请求用户在短时间内重复点击(例如,1000毫秒发送了三个请求),但是绕过了上面的去重判断(不同的KEY值)。原因是这些请求参数的字段有时间字段。这个字段标记了用户请求的时间,服务器可以利用这个来丢弃一些旧的请求(比如5秒前)。如下例,请求的其他参数相同,只是请求时间相差一秒://两次请求相同,但请求时间相差一秒Stringreq="{\n"+"\"requestTime\":\"20190101120001\",\n"+"\"requestValue\":\"1000\",\n"+"\"requestKey\":\"key\"\n"+"}";Stringreq2="{\n"+"\"requestTime\":\"20190101120002\",\n"+"\"requestValue\":\"1000\",\n"+"\"requestKey\":\"键\"\n"+"}";对于这种请求,我们也很可能需要屏蔽后续的重复请求。因此,在获取业务参数摘要之前,需要去除此类时间字段。类似的字段可能是GPS的经纬度字段(重复请求可能会有非常小的差异)。请求去重工具类,Java实现publicclassReqDedupHelper{/****@paramreqJSON请求的参数,这里一般是JSON*@paramexcludeKeys请求参数中去掉哪些字段,然后请求汇总*@return去掉参数的MD5摘要*/publicStringdedupParamMD5(finalStringreqJSON,String...excludeKeys){StringdecreptParam=reqJSON;树图paramTreeMap=JSON.parseObject(decreptParam,TreeMap.class);if(excludeKeys!=null){ListdedupExcludeKeys=Arrays.asList(excludeKeys);if(!dedupExcludeKeys.isEmpty()){for(StringdedupExcludeKey:dedupExcludeKeys){paramTreeMap.remove(dedupExcludeKey);}}}字符串paramTreeMapJSON=JSON.toJSONString(paramTreeMap);字符串md5deDupParam=jdkparamTreeMapJSON);log.debug("md5deDupParam={},excludeKeys={}{}",md5deDupParam,Arrays.deepToString(excludeKeys),paramTreeMapJSON);返回md5deDup参数;}privatestaticStringjdkMD5(Stringsrc){Stringres=null;尝试{MessageDigestmessageDigest=MessageDigest.getInstance("MD5");byte[]mdBytes=messageDigest.digest(src.getBytes());res=DatatypeConverter.printHexBinary(mdBytes);}catch(Exceptione){log.error("",e);}返回资源;}}下面是一些测试日志:publicstaticvoidmain(String[]args){//两次请求相同,但请求时间相差一秒Stringreq="{\n"+"\"requestTime\":\"20190101120001\",\n"+"\"requestValue\":\"1000\",\n"+"\"requestKey\":\"key\"\n"+"}";Stringreq2="{\n"+"\"requestTime\":\"20190101120002\",\n"+"\"requestValue\":\"1000\",\n"+"\"requestKey\":\"key\"\n"+"}";//Compareallparameters,sotheMD5ofthetwoparametersaredifferentStringdedupMD5=newReqDedupHelper().dedupParamMD5(req);StringdedupMD52=newReqDedupHelper().dedupParamMD5(req2);System.out.println("req1MD5="+dedupMD5+",req2MD5="+dedupMD52);//Removetimeparametercomparison,MD5isthesameStringdedupMD53=newReqDedupHelper().dedupParamMD5(req,"requestTime");StringdedupMD54=newReqDedupHelper().dedupParamMD5(req2,"requestTime");System.out.println("req1MD5="+dedupMD53+",req2MD5="+dedupMD54);}log输出:req1MD5=9E054D36439EBDD0604C5E65EB5C8267,req2MD5=A2D20BAC78551C4CA09BEF97FE468A3Freq1MD5=C2A36FED15128E9E878583CAAAFEFDE9,req2MD5=C2A36FED15128E9E878583CAAAFEFDE9日志说明:一开始两个参数由于requestTime是不同的,所以求去重参数摘要的时候可以发现两个值是不一样的第二次调用WhenremovingtherequestTimeandthenseekingthesummary("requestTime"ispassedinthesecondparameter),itisfoundthatthetwosummariesarethesame,asexpected.综上所述,我们可以得到一个完整的去重方案,如下:StringuserId="12345678";//UserStringmethod="pay";//接口名StringdedupMD5=newReqDedupHelper().dedupParamMD5(req,"requestTime");//计算请求参数的汇总,去除了里面请求时间的干扰StringKEY="dedup:U="+userId+"M="+method+"P="+dedupMD5;longexpireTime=1000;//1000毫秒过期,在1000ms内重复请求会被认为是重复setting+expiration不是原子操作,极端情况下设置后可能不会过期,以后同一个请求可能会被误认为是去重,所以这里使用底层API来保证SETNX+expirationtime是原子操作.BooleanfirstSet=stringRedisTemplate.execute((RedisCallback)连接->connection.set(KEY.getBytes(),val.getBytes(),Expiration.milliseconds(expireTime),RedisStringCommands.SetOption.SET_IF_ABSENT));finalbooleanisConsiderDup;if(firstSet!=null&&firstSet){isConsiderDup=false;}else{isConsiderDup=true;}更多Java面试题和Java学习资料免费获取