Java接⼝并发安全重复_如何处理重复请求并发请求的
利⽤唯⼀请求编号去重
你可能会想到的是,只要请求有唯⼀的请求编号,那么就能借⽤Redis做这个去重——只要这个唯⼀请求编号在redis存在,证明处理过,那么就认为是重复的
代码⼤概如下:
String KEY = "REQ12343456788";//请求唯⼀编号
long expireTime = 1000;//1000毫秒过期,1000ms内的重复请求会认为重复
long expireAt = System.currentTimeMillis() +expireTime;
String val= "expireAt@" +expireAt;//redis key还存在的话要就认为请求是重复的
Boolean firstSet = ute((RedisCallback) connection ->connection.Bytes(), Bytes(), Expiration.milliseconds(expireTime), RedisStringCommands.SetOption.SET_IF_ABSENT));final booleanisConsiderDup;if (firstSet != null && firstSet) {//第⼀次访问
isConsiderDup = false;
}else {//redis值已存在,认为是重复了
isConsiderDup = true;
}
业务参数去重
上⾯的⽅案能解决具备唯⼀请求编号的场景,例如每次写请求之前都是服务端返回⼀个唯⼀编号给客户端,客户端带着这个请求号做请求,服务端即可完成去重拦截。
但是,很多的场景下,请求并不会带这样的唯⼀编号!那么我们能否针对请求的参数作为⼀个请求的标识呢?
先考虑简单的场景,假设请求参数只有⼀个字段reqParam,我们可以利⽤以下标识去判断这个请求是否重复。⽤户ID:接⼝名:请求参数
String KEY = "dedup:U="+userId + "M=" + method + "P=" + reqParam;
jdk怎么使用
那么当同⼀个⽤户访问同⼀个接⼝,带着同样的reqParam过来,我们就能定位到他是重复的了。
但是问题是,我们的接⼝通常不是这么简单,以⽬前的主流,我们的参数通常是⼀个JSON。那么针对这种场景,我们怎么去重呢?
计算请求参数的摘要作为参数标识
假设我们把请求参数(JSON)按KEY做升序排序,排序后拼成⼀个字符串,作为KEY值呢?但这可能⾮常的长,所以我们可以考虑对这个字符串求⼀个MD5作为参数的摘要,以这个摘要去取代reqParam的位置。
String KEY = "dedup:U="+userId + "M=" + method + "P=" + reqParamMD5;
这样,请求的唯⼀标识就打上了!
注:MD5理论上可能会重复,但是去重通常是短时间窗⼝内的去重(例如⼀秒),⼀个短时间内同⼀个⽤户同样的接⼝能拼出不同的参数导致⼀样的MD5⼏乎是不可能的。
继续优化,考虑剔除部分时间因⼦
上⾯的问题其实已经是⼀个很不错的解决⽅案了,但是实际投⼊使⽤的时候可能发现有些问题:某些请求⽤户短时间内重复的点击了(例如1000毫秒发送了三次请求),但绕过了上⾯的去重判断(不同的KEY值)。
原因是这些请求参数的字段⾥⾯,是带时间字段的,这个字段标记⽤户请求的时间,服务端可以借此丢弃掉⼀些⽼的请求(例如5秒前)。如下⾯的例⼦,请求的其他参数是⼀样的,除了请求时间相差了⼀秒:
/两个请求⼀样,但是请求时间差⼀秒
String req= "{\n" +
"\"requestTime\" :\"20190101120001\",\n" +
"\"requestValue\" :\"1000\",\n" +
"\"requestKey\" :\"key\"\n" +
"}";
String req2= "{\n" +
"\"requestTime\" :\"20190101120002\",\n" +
"\"requestValue\" :\"1000\",\n" +
"\"requestKey\" :\"key\"\n" +
"}";
这种请求,我们也很可能需要挡住后⾯的重复请求。所以求业务参数摘要之前,需要剔除这类时间字段。还有类似的字段可能是GPS的经纬度字段(重复请求间可能有极⼩的差别)。
请求去重⼯具类,Java实现
public classReqDedupHelper {/***
*@paramreqJSON 请求的参数,这⾥通常是JSON
*@paramexcludeKeys 请求参数⾥⾯要去除哪些字段再求摘要
*@return去除参数的MD5摘要*/
public String dedupParamMD5(finalString reqJSON, excludeKeys) {
String decreptParam=reqJSON;
TreeMap paramTreeMap= JSON.parseObject(decreptParam, TreeMap.class);if (excludeKeys!=null) {
List dedupExcludeKeys =Arrays.asList(excludeKeys);if (!dedupExcludeKeys.isEmpty()) {for(String dedupExcludeKey : dedupExcludeKeys) {
}
}
}
String JSONString(paramTreeMap);
String md5deDupParam=jdkMD5(paramTreeMapJSON);
log.debug("md5deDupParam = {}, excludeKeys = {} {}", md5deDupParam, Arrays.deepToString(excludeKeys), paramTreeMapJSON);returnmd5deDupParam;
}private staticString jdkMD5(String src) {
String res= null;try{
MessageDigest messageDigest= Instance("MD5");byte[] mdBytes =messageDigest.Bytes());
res=DatatypeConverter.printHexBinary(mdBytes);
}catch(Exception e) {
<("",e);
}returnres;
}
}
下⾯是⼀些测试⽇志:
public static voidmain(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 = newReqDedupHelper().dedupParamMD5(req);
String dedupMD52= newReqDedupHelper().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 = ute((RedisCallback) connection ->connection.Bytes(), Bytes(), Expiration.milliseconds(expireTime),
RedisStringCommands.SetOption.SET_IF_ABSENT));final booleanisConsiderDup;if (firstSet != null &&firstSet) {
isConsiderDup= false;
}else{
isConsiderDup= true;
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论