当前位置: 首页 > news >正文

Java实现10万,并发去重,优雅地处理重复请求!

对于一些用户请求,在某些情况下是可能重复发送的,如果是查询类操作并无大碍,但其中有些是涉及写入操作的,一旦重复了,可能会导致很严重的后果,例如交易的接口如果重复请求可能会重复下单。

重复的场景有可能是:

  • 黑客拦截了请求,重放

  • 前端/客户端因为某些原因请求重复发送了,或者用户在很短的时间内重复点击

  • 网关重发

  • ….

本文讨论的是如何在服务端优雅地统一处理这种情况,如何禁止用户重复点击等客户端操作不在本文的讨论范畴。

利用唯一请求编号去重

可能会想到的是,只要请求有唯一的请求编号,那么就能借用Redis做这个去重——只要这个唯一请求编号在redis存在,证明处理过,那么就认为是重复的

代码大概如下:

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;    }

业务参数去重

上面的方案能解决具备唯一请求编号的场景,例如每次写请求之前都是服务端返回一个唯一编号给客户端,客户端带着这个请求号做请求,服务端即可完成去重拦截。

但是,很多的场景下,请求并不会带这样的唯一编号!那么我们能否针对请求的参数作为一个请求的标识呢?

先考虑简单的场景,假设请求参数只有一个字段reqParam,我们可以利用以下标识去判断这个请求是否重复。用户ID:接口名:请求参数

String KEY = "dedup:U="+userId + "M=" + method + "P=" + reqParam;

那么当同一个用户访问同一个接口,带着同样的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 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=A2D20BAC78551C4CA09BEF97FE468A3Freq1MD5 = C2A36FED15128E9E878583CAAAFEFDE9 , req2MD5=C2A36FED15128E9E878583CAAAFEFDE9

日志说明:

  • 一开始两个参数由于requestTime是不同的,所以求去重参数摘要的时候可以发现两个值是不一样的。

  • 另外,关于Java并发问题,公众号Java精选,回复java面试,获取更多面试题资料,支持在线随时随地刷题。

  • 第二次调用的时候,去除了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;}

相关文章:

Java实现10万,并发去重,优雅地处理重复请求!

对于一些用户请求&#xff0c;在某些情况下是可能重复发送的&#xff0c;如果是查询类操作并无大碍&#xff0c;但其中有些是涉及写入操作的&#xff0c;一旦重复了&#xff0c;可能会导致很严重的后果&#xff0c;例如交易的接口如果重复请求可能会重复下单。 重复的场景有可…...

《深入解析 C#》—— C# 3 部分

文章目录 第三章 C#3&#xff1a;LINQ及相关特性3.1 自动实现属性&#xff08;*&#xff09;3.2 隐式类型 var&#xff08;*&#xff09;3.3 对象和集合初始化3.3.1 对象初始化器3.3.2 集合初始化器 3.4 匿名类型3.4.1 基本语法和行为3.4.2 编译器生成类型3.4.3 匿名类型的局限…...

Redis 的5种数据类型的基本命令

目录 String的基本命令 1. SET 2. GET 3. GETSET 4. STRLEN 5. APPEND 6. SETRANGE 7. GETRANGE 8. SETEX 9. SETNX 10. MSET 11. MGET 12. INCR 13. DECR 14. INCRBY 15. DECRBY 16. INCRBYFLOAT Map的基本命令 1. HSET 2. HGET 3. HMSET 4. HMGET 5. …...

【Liunx-后端开发软件安装】Liunx安装nginx

【Liunx-后端开发软件安装】Liunx安装nginx 使用安装包安装 一、简介 nginx&#xff0c;这个家伙可不是你厨房里的那位大厨&#xff0c;它可是互联网世界的“煎饼果子摊主”。想象一下&#xff0c;在熙熙攘攘的网络大街上&#xff0c;nginx挥舞着它的锅铲——哦不&#xff0c;是…...

力扣Lc20--- 202.快乐数(java版)-2024年3月20日

1.题目 2.知识点 &#xff08;1&#xff09;while (seen.contains(n) false) { // 循环体 } 与 !seen.contains(n) 等同 &#xff08;2&#xff09; 当传入数字 19 给 isHappy(19) 方法时&#xff0c;下面是每一行代码的执行过程&#xff1a; 初始化一个空的 HashSet&#…...

机器学习----交叉熵(Cross Entropy)如何做损失函数

目录 一.概念引入 1.损失函数 2.均值平方差损失函数 3.交叉熵损失函数 3.1信息量 3.2信息熵 3.3相对熵 二.交叉熵损失函数的原理及推导过程 表达式 二分类 联立 取对数 补充 三.交叉熵函数的代码实现 一.概念引入 1.损失函数 损失函数是指一种将一个事件&#x…...

Linux docker3--数据卷-nginx配置示例

一、因为docker部署服务都是以最小的代价部署&#xff0c;所以通常在容器内部很多依赖和命令无法执行。进入容器修改配置的操作也比较麻烦。本例介绍的数据卷作用就是将容器内的配置和宿主机文件打通&#xff0c;之后修改宿主机的配置文件就相当于修改了docker进程的配置文件&a…...

力扣454. 四数相加 II

思路&#xff1a;把四个数组拆成两对&#xff0c;两个分别相加&#xff0c;记录第一对的相加结果进map里&#xff0c;再把第二对数组 0-nums2-nums4 去map里面找出现了几次&#xff0c;这题不用对重复的四元组去重&#xff0c;所以出现多次都有效。 class Solution {public int…...

vulnstack1 渗透分析 红日靶场(一)

环境搭建 ip段设置 kali (coleak)&#xff1a;192.168.145.139 Windows 7 (stu1)&#xff1a;192.168.10.181、192.168.145.140 Winserver 2008 (owa)&#xff1a;192.168.10.180 Win2k3 (root-tvi862ubeh)&#xff1a;192.168.10.182复制 kali可以访问win7&#xff0c;但不能…...

外包干了6天,技术明显进步。。。

我是一名大专生&#xff0c;自19年通过校招进入湖南某软件公司以来&#xff0c;便扎根于功能测试岗位&#xff0c;一晃便是近四年的光阴。今年8月&#xff0c;我如梦初醒&#xff0c;意识到长时间待在舒适的环境中&#xff0c;已让我变得不思进取&#xff0c;技术停滞不前。更令…...

比较好的知识点

2023年Java超全面试题及答案解析---https://blog.csdn.net/qq_42301302/article/details/128785274 7分钟带你细致解析4个Java算法必刷题---https://blog.csdn.net/hcxy2022/article/details/127963797 50道JAVA基础算法编程题【内含分析、程序答案】---https://blog.csdn.net/…...

抖音开放平台的订单类API接口调用测试指南(内含详细步骤)

一、什么是抖音开放平台 抖音开放平台基于抖音母体&#xff0c;提供抖音服务基础设施和创新行业解决方案的平台。同时满足各类各类机构、创作者及服务商对于内容获取、分享的个性化需求&#xff0c;我们诚邀各个行业、不同阶段的合作伙伴与我们一起&#xff0c;共建内容良性生…...

HiveSQL一本通 - 案例实操

文章目录 0.HiveSQL一本通使用说明6.综合案例练习之基础查询6.1 环境准备创建数据表数据准备加载数据 6.2 简单查询练习1.查询姓名中带“山”的学生名单2.查询姓“王”老师的个数3.检索课程编号为“04”且分数小于60的学生的分数信息&#xff0c;结果按分数降序排列4.查询数学成…...

Axure RP 8中文---快速原型设计工具,一站式解决方案

Axure RP 8是一款专业的快速原型设计工具&#xff0c;以其直观易用的界面和丰富的功能受到广大用户的青睐。它支持用户通过拖放操作快速创建交互式原型&#xff0c;包括线框图、流程图等&#xff0c;并具备高保真度的设计能力。Axure RP 8还提供了团队协作和共享功能&#xff0…...

Available platform plugins are: minimal, offscreen, webgl, windows.

我在运行pyqt5开发的代码时&#xff0c;报错&#xff1a; This application failed to start because no Qt platform plugin could be initialized, Reinstalling the application may fix this problem. Available platform plugins are: minimal, offscreen, webgl, windows…...

创意无限,风险有度:2024愚人节海外网红营销策略解析

2024年愚人节即将到来&#xff0c;这个充满趣味与惊喜的节日&#xff0c;既是人们展示幽默与创意的舞台&#xff0c;也是品牌进行营销活动的绝佳时机。在这个特殊的日子里&#xff0c;通过海外网红营销来推广品牌或产品&#xff0c;无疑是一种富有创意的营销策略&#xff0c;但…...

深入理解 Session、Cookie 和 Token:网络安全和身份验证的重要概念

深入理解 Session、Cookie 和 Token&#xff1a;网络安全和身份验证的重要概念 在当今数字化的世界中&#xff0c;网络安全和身份验证是至关重要的议题。为了实现这些目标&#xff0c;我们常常使用诸如 Session、Cookie 和 Token 等概念。这些概念在 Web 开发、网络通信和安全…...

镜像站汇总

软件镜像站 查看linux版本&#xff0c;常见有centos, ubuntu, Debian cat /etc/os-release去清华软件源帮助页面&#xff0c;查找对应源设置方案(需要结合具体的系统版本)&#xff0c;常用&#xff1a; Debian https://mirrors.tuna.tsinghua.edu.cn/help/debian/ 需要选则系…...

设计模式之抽象工厂模式解析

抽象工厂模式 1&#xff09;问题 工厂方法模式中的每个工厂只生产一类产品&#xff0c;会导致系统中存在大量的工厂类&#xff0c;增加系统的开销。 2&#xff09;概述 a&#xff09;产品族 和 产品等级结构 产品等级结构&#xff1a;产品的继承结构&#xff1b; 产品族&…...

【毕设级项目】基于ESP8266的家庭灯光与火情智能监测系统——文末源码及PPT

目录 系统介绍 硬件配置 硬件连接图 系统分析与总体设计 系统硬件设计 ESP8266 WIFI开发板 人体红外传感器模块 光敏电阻传感器模块 火焰传感器模块 可燃气体传感器模块 温湿度传感器模块 OLED显示屏模块 系统软件设计 温湿度检测模块 报警模块 OLED显示模块 …...

VB.net复制Ntag213卡写入UID

本示例使用的发卡器&#xff1a;https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...

STM32+rt-thread判断是否联网

一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...

Qt Widget类解析与代码注释

#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }//解释这串代码&#xff0c;写上注释 当然可以&#xff01;这段代码是 Qt …...

Linux简单的操作

ls ls 查看当前目录 ll 查看详细内容 ls -a 查看所有的内容 ls --help 查看方法文档 pwd pwd 查看当前路径 cd cd 转路径 cd .. 转上一级路径 cd 名 转换路径 …...

抖音增长新引擎:品融电商,一站式全案代运营领跑者

抖音增长新引擎&#xff1a;品融电商&#xff0c;一站式全案代运营领跑者 在抖音这个日活超7亿的流量汪洋中&#xff0c;品牌如何破浪前行&#xff1f;自建团队成本高、效果难控&#xff1b;碎片化运营又难成合力——这正是许多企业面临的增长困局。品融电商以「抖音全案代运营…...

Linux云原生安全:零信任架构与机密计算

Linux云原生安全&#xff1a;零信任架构与机密计算 构建坚不可摧的云原生防御体系 引言&#xff1a;云原生安全的范式革命 随着云原生技术的普及&#xff0c;安全边界正在从传统的网络边界向工作负载内部转移。Gartner预测&#xff0c;到2025年&#xff0c;零信任架构将成为超…...

数据库分批入库

今天在工作中&#xff0c;遇到一个问题&#xff0c;就是分批查询的时候&#xff0c;由于批次过大导致出现了一些问题&#xff0c;一下是问题描述和解决方案&#xff1a; 示例&#xff1a; // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...

Java入门学习详细版(一)

大家好&#xff0c;Java 学习是一个系统学习的过程&#xff0c;核心原则就是“理论 实践 坚持”&#xff0c;并且需循序渐进&#xff0c;不可过于着急&#xff0c;本篇文章推出的这份详细入门学习资料将带大家从零基础开始&#xff0c;逐步掌握 Java 的核心概念和编程技能。 …...

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中&#xff0c;新增了一个本地验证码接口 /code&#xff0c;使用函数式路由&#xff08;RouterFunction&#xff09;和 Hutool 的 Circle…...

学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”

2025年#高考 将在近日拉开帷幕&#xff0c;#AI 监考一度冲上热搜。当AI深度融入高考&#xff0c;#时间同步 不再是辅助功能&#xff0c;而是决定AI监考系统成败的“生命线”。 AI亮相2025高考&#xff0c;40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕&#xff0c;江西、…...