如何优雅处理重复请求/并发请求?
Java后端技术
共 10754字,需浏览 22分钟
· 2021-06-05
往期热门文章:
1、不用到2038年,MySQL的TIMESTAMP就能把我们系统搞崩!
2、翻车!在项目中用了Arrays.asList、ArrayList的subList,被公开批评
前言
黑客拦截了请求,重放; 前端/客户端因为某些原因请求重复发送了,或者用户在很短的时间内重复点击了; 网关重发; ……
利用唯一请求编号去重
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、计算请求参数的摘要作为参数标识
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、《历史文章分类导读列表!精选优秀博文都在这里了!》
2、为什么不建议你用a.equals(b)判断对象相等 3、为什么 Java 后端开发没有大规模采用 Kotlin? 4、为什么不推荐使用BeanUtils属性转换工具 5、Top 16 的 Java 工具类,你用过几个? 6、分享几个酷炫的 IDEA 主题 7、Intellij IDEA 这样配置注释模板,让你瞬间高出一个逼格!
8、【建议收藏】面试官会问的位运算奇淫技巧 9、到底可不可以用 kill -9 关闭程序? 10、IDEA 2021首个大版本发布,新增了这几个超实用功能!
评论
什么是以太坊期货ETF?它们如何运作?
作者:Jagjit Singh,CoinTelegraph;编译:陶朱,金色财经一、以太坊期货 ETF 介绍以太坊期货 ETF 是追踪以太坊期货合约而非以太坊本身的投资基金。考虑一份承诺在未来某个时间以特定价格购买以太坊的合约。这些衍生品合约通过以太坊 (ETH) 期货交易所交易基金 (ETF) 进
区块链头条
0
字节面试:如何解决MQ消息积压问题?
面试题大全:www.javacn.siteMQ(Message Queue)消息积压问题指的是在消息队列中累积了大量未处理的消息,导致消息队列中的消息积压严重,超出系统处理能力,影响系统性能和稳定性的现象。1.消息积压是哪个环节的问题?MQ 执行有三大阶段:消息生产阶段。消息存储阶段。消息消费阶段。
Java中文社群
0
如何动手做出一个 CPU,很简单
将Python客栈设为“星标⭐”第一时间收到最新资讯来源:无聊的闪客纯手工打造一个 CPU 这个事儿。在电子专业的同学眼里,很容易。在计算机专业的同学眼里,稍稍有点复杂,有的专业课的实验课可能会带着同学做一个,或者用 Logisim 这样的仿真软件去模拟实现一个。在非计算机专业的同学眼里,就有点不敢
Python客栈
0
面试官:MySQL 上亿大表,如何深度优化?
来源:cnblogs.com/YangJiaXin/p/10828244.html背景分析测试实施索引优化后delete大表优化为小批量删除总结前段时间刚入职一家公司,就遇上这事!背景XX实例(一主一从)xxx告警中每天凌晨在报SLA报警,该报警的意思是存在一定的主从延迟(若在此时发生主从切换,需要
好好学java
0
文末送书 | 大模型时代下如何学习云原生
《containerd 原理剖析与实战》新书内购中,点击阅读原文,限时 69.9 元购买。文末免费赠书大模型与云原生近年来,大语言模型的热度可谓是愈发高涨,尤其是今年年初 Sora 的出现,更是让全球再次看到了AIGC 的巨大威力。Sora 生成实例视频---几头巨大的长毛猛犸踏着积雪的草地而来在当
云原生实验室
10
图像处理基础知识
点击上方“小白学视觉”,选择加"星标"或“置顶”重磅干货,第一时间送达图像1、模拟图像模拟图像,又称连续图像,是指在二维坐标系中连续变化的图像,即图像的像点是无限稠密的,同时具有灰度值(即图像从暗到亮的变化值)。2、数字图像数字图像,又称数码图像或数位图像,是二维图像用有限数字数值像素的表示。数字图
小白学视觉
10
如何使用 Python比较两张图像并获得准确度?
点击上方“小白学视觉”,选择加"星标"或“置顶”重磅干货,第一时间送达本文,将带你了解如何使用 Python、OpenCV 和人脸识别模块比较两张图像并获得这些图像之间的准确度水平。首先,你需要了解我们是如何比较两个图像的。我们正在使用Face Recognition python 模块来获取两张图
小白学视觉
10
如何解释“有你这写脚本时间,我早就一个一个做完了”?
点击下方“JavaEdge”,选择“设为星标”第一时间关注技术干货!免责声明~任何文章不要过度深思!万事万物都经不起审视,因为世上没有同样的成长环境,也没有同样的认知水平,更「没有适用于所有人的解决方案」;不要急着评判文章列出的观点,只需代入其中,适度审视一番自己即可,能「跳脱出来从外人的角度看看现
JavaEdge
0