Spring Boot 優雅地處理重復請求

前 言

對于一些用戶請求,在某些情況下是可能重復發送的,如果是查詢類操作并無大礙,但其中有些是涉及寫入操作的,一旦重復了,可能會導致很嚴重的后果,例如交易的接口如果重復請求可能會重復下單。
重復的場景有可能是:

  • 黑客攔截了請求,重放
  • 前端/客戶端因為某些原因請求重復發送了,或者用戶在很短的時間內重復點擊
  • 網關重發
    本文討論的是如何在服務端優雅地統一處理這種情況,如何禁止用戶重復點擊等客戶端操作不在本文的討論范疇。

利用唯一請求編號去重

可能會想到的是,只要請求有唯一的請求編號,那么就能借用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=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;
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/212048.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/212048.shtml
英文地址,請注明出處:http://en.pswp.cn/news/212048.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Verilog基礎:$random系統函數的使用

相關閱讀 Verilog基礎?編輯https://blog.csdn.net/weixin_45791458/category_12263729.html $random系統函數語法的BNF范式如下所示&#xff0c;有關BNF范式相關內容&#xff0c;可以瀏覽以往文章Verilog基礎&#xff1a;巴科斯范式(BNF)。 $random系統函數在每次調用時返回一…

【IDEA】IntelliJ IDEA中進行Git版本控制

本篇文章主要記錄一下自己在IntelliJ IDEA上使用git的操作&#xff0c;一個新項目如何使用git進行版本控制。文章使用的IDEA版本 IntelliJ IDEA Community Edition 2023.3&#xff0c;遠程倉庫為https://gitee.com/ 1.配置Git&#xff08;File>Settings&#xff09; 2.去Git…

[gRPC實現go調用go]

1什么是RPC RPC&#xff1a;Remote Procedure Call&#xff0c;遠程過程調用。簡單來說就是兩個進程之間的數據交互。正常服務端的接口服務是提供給用戶端(在Web開發中就是瀏覽器)或者自身調用的&#xff0c;也就是本地過程調用。和本地過程調用相對的就是&#xff1a;假如兩個…

深度優先遍歷(DFS)

時間復雜度與深搜一致&#xff1b;

STM32 定時器總結

縮寫 ARR: Auto-Reload Register&#xff08;保存定時器的計數范圍&#xff09; PSC: Prescaler register&#xff08;預分頻器寄存器&#xff0c;根據設置的分頻因子N&#xff0c;計數N個定時器時鐘脈沖后&#xff0c;產生一個CNT計數&#xff0c;以此實現分頻功能&#xff0…

LeetCode 2048. 下一個更大的數值平衡數

一、題目 1、題目描述 如果整數 x 滿足&#xff1a;對于每個數位 d &#xff0c;這個數位 恰好 在 x 中出現 d 次。那么整數 x 就是一個 數值平衡數 。 給你一個整數 n &#xff0c;請你返回 嚴格大于 n 的 最小數值平衡數。 0 < n < 1e6 2、接口描述 public:int nextB…

Android渲染-AHardwareBuffer

本文主要從應用的角度介紹android的native層AHardwareBuffer創建紋理以及保存渲染數據。 HardwareBuffer 要介紹native層的AHardwareBuffer&#xff0c;就需要先從Java層的HardwareBuffer說起。Android官方對于HardwareBuffer介紹如下&#xff1a; HardwareBuffer wraps a na…

HttpURLConnection OOM問題記錄

使用HttpURLConnection 上傳大文件&#xff0c;會出現內存溢出問題&#xff1a; 觀察HttpURLConnection 源碼&#xff1a; Overridepublic synchronized OutputStream getOutputStream() throws IOException {connecting true;SocketPermission p URLtoSocketPermission(th…

【接口分享】熱門好用的API,含免費次數

語音驗證碼短信&#xff1a;撥打電話告知用戶驗證碼&#xff0c;實現信息驗證。短信驗證碼&#xff1a;可用于登錄、注冊、找回密碼、支付認證等等應用場景。支持三大運營商&#xff0c;3秒可達&#xff0c;99.99&#xff05;到達率&#xff0c;支持大容量高并發。通知短信&…

基于SSM的點餐系統的設計與實現

末尾獲取源碼 開發語言&#xff1a;Java Java開發工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 數據庫&#xff1a;MySQL5.7和Navicat管理工具結合 服務器&#xff1a;Tomcat8.5 開發軟件&#xff1a;IDEA / Eclipse 是否Maven項目&#xff1a;是 目錄…

mysql設置為密碼登錄

要設置Ubuntu上的MySQL需要密碼登錄&#xff0c;你可以使用以下步驟&#xff1a; 打開終端。 輸入以下命令登錄到 MySQL 服務器&#xff1a; sudo mysql -u root -p按Enter后&#xff0c;系統會要求輸入密碼。如果是第一次登錄&#xff0c;你可能需要直接按Enter鍵&#xff08…

【已解決】解決UbuntuKali無法進行SSH遠程連接

目錄 Ubuntu20.04配置SSH遠程連接Kali Linux配置SSH遠程連接 Ubuntu20.04配置SSH遠程連接 首先更新安裝包 sudo apt-get update 下載SSH服務 sudo apt install openssh-server 查看SSH服務 service ssh status 打開 /etc/ssh/sshd_config文件修改配置文件 將PermitRootLog…

知識筆記(五十二)———MySQL 刪除數據表

MySQL中刪除數據表是非常容易操作的&#xff0c;但是你在進行刪除表操作時要非常小心&#xff0c;因為執行刪除命令后所有數據都會消失。 語法 以下為刪除 MySQL 數據表的通用語法&#xff1a; DROP TABLE table_name ; -- 直接刪除表&#xff0c;不檢查是否存在 或 DROP…

基于Debain安裝 Docker 和 Docker Compose

一、安裝Docker # 先升級一下系統 (Ubuntu / Debian 系) sudo apt-get update sudo apt-get upgrade# 如果你是 CentOS、紅帽系列則使用&#xff1a; yum update yum upgrade# 安裝 Docker curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh二、Dock…

LeetCode 0070. 爬樓梯:動態規劃(遞推)

【LetMeFly】70.爬樓梯&#xff1a;動態規劃&#xff08;遞推&#xff09; 力扣題目鏈接&#xff1a;https://leetcode.cn/problems/climbing-stairs/ 假設你正在爬樓梯。需要 n 階你才能到達樓頂。 每次你可以爬 1 或 2 個臺階。你有多少種不同的方法可以爬到樓頂呢&#x…

NVIDIA Jetson NX ubuntu20.04刪除多余版本沖突的Boost庫

參考Ubuntu16.04 卸載舊版本Boost庫并安裝新版本 卸載 刪除/usr/local/include/boost文件夾&#xff0c;刪除/usr/local/lib中和boost有關的文件,以及/usr/local/lib/cmake/中boost的cmake文件 cd /usr/local/lib/ ls | grep boost sudo rm -rf /usr/local/include/boost su…

藍橋杯 day01 奇怪的數列 特殊日期

奇怪的數列 題目描述 奇怪的數列 從 X 星截獲一份電碼&#xff0c;是一些數字&#xff0c;如下&#xff1a; 13 1113 3113 132113 1113122113 ?? YY 博士經徹夜研究&#xff0c;發現了規律&#xff1a; 第一行的數字隨便是什么&#xff0c;以后每一行都是對上一行…

redis+springsecurity+mybtais-plus+JWT

redisspringsecuritymybtais-plusJWT 01 引入依賴 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.mysql</groupId&g…

12.視圖

目錄 1.視圖的含義與作用 2.視圖的創建與查看 1.創建視圖的語法形式 2、查看視圖&#xff1a; 1.使用DESCRIBE語句查看視圖基本信息 2.使用SHOW TABLE STATUS語查看視圖基本信息查看視圖的信息 3.使用SHOW CREATE VIEW語查看視圖詳細信息 4.在views表中查看視圖詳細信息…

【DP】Yarik and Array—CF1899C

Yarik and Array—CF1899C 一道不難的DP&#xff0c;根據代碼就能看出思路。 C o d e Code Code #include <bits/stdc.h> #define int long long #define sz(a) ((int)a.size()) #define all(a) a.begin(), a.end() using namespace std; using PII pair<int, int&…