程序员过关斩将--重复的请求并不好过滤
為什么要做重復請求的過濾呢?不過濾不行嗎?
過濾重復請求很難嗎?加一個請求ID不就好了嗎?
每個技術難點的話題,肯定是由一個產品需求引發(fā)的,俗話說:如果沒有產品經理,程序員將不需要聽診器,但是會失業(yè)!!
產生背景
重復請求能夠對系統(tǒng)造成傷害是架構中很難避免的一個設計問題,一般情況下,讀請求很少會造成致命性的故障,主要是系統(tǒng)的寫請求,很多時候一個重復寫的動作,會是我們程序員加班的緣由。比如:用戶使用積分兌換物品,重復的請求會造成用戶積分的重復扣減,而作為線上系統(tǒng),如果日志等輔助打的不好的話,排查原因其實需要很多時間。
一般的產品經理設計系統(tǒng)的時候并不會涉及到這類異常情況,但是一旦出現問題,產品經理就會找到程序員罵娘,多么悲哀的故事,人家付出5分精力設計的系統(tǒng),我們卻要花費10分的精力去編碼和維護。
重復的業(yè)務請求,有的時候對系統(tǒng)造成的影響很大,所以程序員在設計的時候尤其要注意,產生的原因有很多:
黑客進行了攔截,人為的重放了請求
客戶端因為某些原因,用戶在很短的時間內重放了請求
一些中間件(比如網關)重放了請求
未知的其他情況
道理很簡單,用一張圖表達的會更清爽一些
image抽象出來是不是很簡單?但是落地卻并非像這張圖一樣簡單!!
從這張圖上一眼就可以看到,整個過程的重點難點在于過濾器這個邏輯設計部分,這部分可以和業(yè)務代碼融合在一起,有的時候也可以相分離,比如:有的網關可以內嵌腳本(比如:lua),就完全可以做到和業(yè)務無關,但是通常情況下,落地的代碼卻和業(yè)務息息相關。
客戶端處理
客戶端處理重復請求是一種可以有效過濾正常請求的手段,為什么這么說呢?當一個用戶正常操作的時候,客戶端完全可以利用loading的方式或者其他過濾重復手段來達到目的,比如:當用戶點擊一個按鈕的時候,彈出loading窗口方式用戶再次操作。
再比如:客戶端可以設置一個類似于布隆過濾的數據結構,配合對應的過濾算法也可以達到過濾重復請求的效果。
不過,客戶端的任何解決方案也只是治標不治本,畢竟,客戶端在整個系統(tǒng)架構中,是最不可靠的終端。
請求標識
重復請求過濾的關鍵在于過濾器的邏輯設計,目前最常用,落地最多當屬使用請求ID的方式。大體流程如下:
客戶端發(fā)送請求的時候,會生成隨機的請求ID,隨著業(yè)務參數一起傳送到服務端
服務端會根據傳送上來的請求ID做是否重復的判斷
服務器的判斷邏輯其實有很多落地方案了,比如最常見的利用redis來存儲請求ID,以下是偽代碼(NetCore):
public?class?Para {public?string?ReqId{get?;set?;}??//其他業(yè)務參數 }public?bool?IsExsit(Para?p) {//利用redis來判斷當前的key是否存在bool?isExsit=redisMethond(p.ReqId);//如果存在,則說明是重復請求,如果不存在說明不是重復請求,并且添加到redisif(!isExsit){AddRedis(p.ReqId);}return?isExsit;}一般網上的文章都到此為止了,這種方案有沒有問題呢?答案:有
問題1
正常的客戶端重復請求,一般情況下真的會根據我們寫的代碼過濾掉重復請求,為什么說一般情況呢?那是因為分布式的原因,極限情況下也會導致重復的請求到業(yè)務處理端,比如以下情況:
請求被路由到了A服務器,A服務器會去請求Redis,判斷是否有相同的請求ID存在,如果是第一次請求,Redis會返回不存在
同樣的時間,客戶端或者黑客重放了同樣的請求,這個請求被路由到了B服務器,B服務器同樣會請求Redis來判斷是否存在,這個時候由于A服務器還沒回寫Redis,所以B服務器得到的結果也是不存在該請求
這樣就導致了業(yè)務端收到了兩次同樣的請求,會導致業(yè)務不可預期的結果
可見,一個小小重復過濾請求,可能還需要分布式鎖的出場才可以
問題2
即便請求中加了唯一的請求ID,但是這個ID并沒有安全保證,或者說,這個ID是可以篡改的。當黑客攔截到請求,隨便改一下請求ID,在重放就搞定你了。所以,加的請求ID,還需要一個安全機制來保證安全,不然這個參數其實意義不大。
業(yè)務簽名
由于單純添加請求ID,并不能解決問題,所以我們需要一種保證請求ID的機制,目前來看,普遍的落地方案是根據業(yè)務參數生成摘要,也就是所謂的加簽操作。加簽操作可以有效的防止參數被篡改。如果你做過微信相關的開發(fā),你會發(fā)現和微信服務器的交互也是基于加簽操作的。而生成的簽名可以作為請求ID,以下是偽代碼:
????//客戶端生成簽名string?sigh=MD5($"參數1=值1&參數2=值2&time=當前時間戳")以上只是例子,雖然MD5算法有產生重復數據的可能性,但是對于當前這個業(yè)務場景來說足夠了。細心的同學會發(fā)現,參數當中加了一個時間戳的參數,這個是我故意加的,這個時間戳在這個場景下會出現問題,什么問題呢?
時間戳問題
當前的請求場景是要過濾重復的請求,什么樣的請求算是重復請求呢?關鍵是這個定義要明確,我看了很多重復過濾請求的文章,重復請求這個概念其實定義的不好,這個是和具體業(yè)務場景相關的。舉個栗子:當用戶一秒內重復點擊某個按鈕算是重復請求,那10秒內重復點擊呢?用戶一秒之內對同一個商品下單算重復請求,那10秒內呢?
這個定義就涉及到了上面所說的時間戳參數的問題,時間戳是否要參與生成簽名,要根據具體的業(yè)務場景來定義,不過,我還是要建議,請求的參數中帶上時間戳,無論它參不參與簽名,至于為什么這么做,當時間長了你就知道了
寫在最后
過濾重復請求這個需求,并沒有像想象中那么容易,并非只要加上一個請求ID就完事了,它涉及到安全以及分布式的問題,在某些場景下(比如:秒殺)還會涉及到性能以及高可用等非功能性問題,所以那些說:只需要一個請求ID就能過濾的同學,請不要再誤導別人了,技術是神圣不可侵犯的。
還是那句話:具體的業(yè)務影響到具體的代碼實現,脫離業(yè)務講架構其實就是耍流氓
總結
以上是生活随笔為你收集整理的程序员过关斩将--重复的请求并不好过滤的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JWT 介绍 - Step by Ste
- 下一篇: .NET团队送给.NET开发人员的云原生