如何處理重複請求 併發請求的

2022-06-23 10:39:32 字數 4022 閱讀 6902

你可能會想到的是,只要請求有唯一的請求編號,那麼就能借用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) connection ->connection.set(key.getbytes(), val.getbytes(), expiration.milliseconds(expiretime), redisstringcommands.setoption.set_if_absent));

final

boolean

isconsiderdup;

if (firstset != null && firstset)

else

上面的方案能解決具備唯一請求編號的場景,例如每次寫請求之前都是服務端返回乙個唯一編號給客戶端,客戶端帶著這個請求號做請求,服務端即可完成去重攔截。

但是,很多的場景下,請求並不會帶這樣的唯一編號!那麼我們能否針對請求的引數作為乙個請求的標識呢?

先考慮簡單的場景,假設請求引數只有乙個欄位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 = "";

string req2 = "";

這種請求,我們也很可能需要擋住後面的重複請求。所以求業務引數摘要之前,需要剔除這類時間字段。還有類似的字段可能是gps的經緯度字段(重複請求間可能有極小的差別)。

public

class

reqdeduphelper }}

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)

catch

(exception e)

return

res;

}}

下面是一些測試日誌:

public

static

void

main(string args) ";

string req2 = "";

//全引數比對,所以兩個引數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

至此,我們可以得到完整的去重解決方案,如下:

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) connection ->connection.set(key.getbytes(), val.getbytes(), expiration.milliseconds(expiretime),

redisstringcommands.setoption.set_if_absent));

final

boolean

isconsiderdup;

if (firstset != null &&firstset)

else

Nginx是如何處理請求的

基於命名的虛擬伺服器 name based virtual servers nginx首先要確定由哪個伺服器 server 來處理這個請求,如下面的簡單的例子中,一共有3個虛擬主機,分別是 server 其中 是區分大小寫的正規表示式 為不區分大小寫的正規表示式 為了提高效率,實行嚴格匹配,如果找到...

nginx是如何處理請求的

一 nginx如何選擇適當的虛擬伺服器來處理請求?server匹配 1 基於主機名匹配來選擇合適的虛擬伺服器 對比請求頭中的host欄位與server中的server name,選擇匹配的伺服器。如果都不匹配,則選擇該埠所對應預設的伺服器。如果沒有指定預設伺服器,默默為伺服器列表中的第乙個,可以通過...

Servlet容器如何處理多個請求??

tomcat是一種servlet容器,是servlet執行的載體,兩者相互依存著存在。對多個請求的處理主要有servlet容器的執行緒池來處理。如何處理 1 執行緒池中包含工作者執行緒 即實際執行任務的執行緒 和排程執行緒 即對執行緒進行排程 2 tomcat的執行緒使用單例項多執行緒的機制執行,這...