在業務開發中,我們常會面對防止重複請求的問題。當伺服器端對於請求的響應涉及資料的修改,或狀態的變更時,可能會造成極大的危害。重複請求的後果在交易系統、售後維權,以及支付系統中尤其嚴重。
前台操作的抖動,快速操作,網路通訊或者後端響應慢,都會增加後端重複處理的概率。前台操作去抖動和防快速操作的措施,我們首先會想到在前端做一層控制。當前端觸發操作時,或彈出確認介面,或disable入口並倒計時等等,此處不細表。但前端的限制僅能解決少部分問題,且不夠徹底,後端自有的防重複處理措施必不可少,義不容辭。
在介面實現中,我們常要求介面要滿足冪等性,來保證多次重複請求時只有一次有效。
查詢類的介面幾乎總是冪等的,但在包含諸如資料插入,多模組資料更新時,達到冪等性會比較難,尤其是高並行時的冪等性要求。比如第三方支付前台回撥和後台回撥,第三方支付批次回撥,慢效能業務邏輯(如使用者提交退款申請,商家同意退貨/退款等)或慢網路環境時,是重複處理的高發場景。
這裡針對「使用者提交退款申請」的例子,說明一下嘗試過的防重複處理方法的效果。後端防重複處理的方式,我們先後嘗試了三種:
這種方式簡單直觀,從DB查詢出來的退款詳情(包括狀態)往往還可以用在後續邏輯中,沒有花額外的工作專門應對重複請求的問題。
這種查詢狀態後進行驗證的邏輯,從程式碼上線後就一直存在於所有含狀態的業務邏輯處理中,必不可少。但對於防重複處理效果並不好:在前端新增防重複提交前,每週平均在25筆;前端優化後,每週降到7筆。這個數量佔總退款申請數的3%%,一個仍然無法接受的比例。
理論上,任意次請求只要在資料狀態更新之前都完成了查詢操作,則業務邏輯的重複處理就會發生。如下圖所示。優化的方向是減少查詢到更新之間業務處理時間,可降低空檔期的併行影響。極致情況下如果查詢和更新變成了原子操作,則就不存在我們當前的問題。
Redis儲存查詢輕量快速。在request進來的時候,可以先記錄在快取中。後續進來的request每次進行驗證。整個流程處理完成,清除快取。以退款為例子:
與1)的發放相比,資料庫換成響應更快的快取。但是仍然不是原子操作。插入和讀取快取還是有時間間隔。在極致的情況下還是存在重複操作的情況。此方法優化後,每週1筆重複操作。
需要原子性操作,想到了資料庫的唯一索引。新建一個TradeLock表:
CREATE TABLE `TradeLock` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `type` int(11) NOT NULL COMMENT '鎖型別', `lockId` int(11) NOT NULL DEFAULT '0' COMMENT '業務ID', `status` int(11) NOT NULL DEFAULT '0' COMMENT '鎖狀態', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='Trade鎖機制';
● 每次request進來則往表裡面插入資料:
成功,則可以繼續操作(相當於獲取鎖); 失敗,則說明有操作在進行。
● 操作完成後,刪除此條記錄。(相當於釋放鎖)。
目前已經上線,等待下週的資料統計。
由於資料庫的操作比較消耗效能,了解到redis的計數器也是原子性操作。果斷採用計數器。既可以提高效能,還不用儲存,而且能提升qps的峰值。
還是以訂單退款為例子:
● 每次request進來則新建一個以orderId為key的計數器,然後+1。
如果>1(不能獲得鎖): 說明有操作在進行,刪除。 如果=1(獲得鎖): 可以操作。
● 操作結束(刪除鎖):刪除這個計數器。
要了解計數器,可以參考:http://www.redis.cn/commands/incr.html
PHP語言自身沒有提供進程互斥和鎖定機制。因此才有了我們上面的嘗試。網上也有檔案鎖機制,但是考慮到我們的分散式部署,建議還是用快取。在大並行的情況下,程式各種情況的發生。特別是涉及到金額操作,不能有一分一毫的差距。所以在大並行要互斥的情況下可以考慮3、4兩種方案。
愛迪生嘗試了1600多種材料選擇了鎢絲發明了燈泡,實踐出真知。遇到問題,和問題鬥爭,最後解決問題是一個最大提升自我的過程,不但加寬自己的知識廣度,更加深了自己的技能深度。達到目標之後的成就感更是不言而喻。
以上就是防訂單重複提交策略方法的詳細內容,更多請關注TW511.COM其它相關文章!