10w qps 高併發,如何防止重複下單?

基礎知識:電商訂單支付核心流程

首先,來看看 訂單支付的業務流程和交互流程。

圖解:訂單支付的業務流程和交互流程

結合下圖來看看 訂單支付的業務流程和交互流程。

訂單支付流程, 分爲 大致 的 6 個步驟 :

1. 下單 / 結算:

下單作爲支付的入口,但並非起點,

支付相關的金額等信息全部來至結算,此時訂單處於 未支付 狀態。

2. 申請支付:

用戶開始申請支付,客戶端調用支付服務,

此時在支付系統內產生一筆訂單支付流水,這筆支付流水處於 未支付 狀態。

3. 發起支付:

支付服務調用 第三方支付平臺,

通常, 第三方支付平臺 是 錢包類的支付方式,

在發起支付這一步驟,支付平臺會響應一些支付的鏈接,客戶端會對鏈接進行相應的處理。

4. 錢包支付:

用戶進行支付,

用戶 APP 端直接拉去錢包進行支付。

5. 支付回調:

用戶完成支付之後,三方支付平臺會回調 商戶的支付服務 接口,通知支付結果。

6. 更新訂單狀態:

支付服務 確認訂單支付完成後,會向 訂單服務同步 支付的結果。

訂單服務變更服務的狀態:未支付變更爲  待發貨

客戶端通過輪詢、長輪詢,或者服務端主動推送的方式,在界面上變更訂單狀態。

圖解:支付狀態的變化

如下圖,從支付流水角度來分析一下支付狀態的變化:

  1. 從未支付,到有支付結果的終態,中間還有一箇中間狀態:支付中

  2. 戶通過打開錢包 --》完成支付 --》支付回調,這段時間的支付流水就處於:支付中

重複下單的定義、危害、應對策略

什麼是重複下單

現在問題來了, 什麼是重複下單?

用戶在下單頁面進行下單時,由於用戶點擊下單按鈕 多次 、或者 重試策略 導致在訂單服務中接收到了 兩次同樣 的下單請求。

重複下單帶來的危害

重複下單場景,第 N 次的下單會對數據進行打亂,導致系統整體數據異常

重複下單場景,第 N 次的下單需要等第一次下單操作完成

重複下單帶來的危害, 總結起來,有以下幾點:

1. 系統資源佔用與性能下降
2. 訂單處理複雜性增加
3. 財務結算與對賬難度增大
4. 用戶體驗受損
5. 數據異常與決策誤導
6. 售後服務與退換貨問題
7. 安全風險與欺詐行爲

重複下單問題,主要解決辦法就是做好冪等,因爲在分佈式系統中,我們是沒有辦法保證用戶一定不會快速點擊兩次下單。

Order 服務調用 Pay 服務,剛好網絡超時,然後 Order 服務開始重試機制,於是 Pay 服務對同一支付請求,就接收到了兩次,而且因爲輪詢負載均衡算法,請求落在了不同業務服務節點,所以一個分佈式系統服務,須保證冪等性。

什麼場景下回發生重複下單?

場景 1:客戶端 bug

用戶短時間內多次點擊下單按鈕,或瀏覽器刷新按鈕導致。

比如下單的按鍵在點按之後,在沒有收到服務器請求之前,按鍵的狀態沒有設爲已禁用狀態,還可以繼續點擊。又或者,在觸摸屏下,用戶手指的點按可能被手機操作系統識別爲多次點擊。

場景 2:超時重試

Nginx 或 Spring Cloud Gateway 網關層、RPC 通信重試或業務層重試,進行超時重試導致的。

用戶的設備與服務器之間,可能是不穩定的網路。這樣一個下單請求過去,服務器不一定及時返回結果。

超時最大的問題:從用戶的角度,他無法確定下單的請求是否達到服務器,還是已經到了服務器但是返回結果時數據丟失了。所以用戶無法區分到底這個訂單是否下單成功。

場景 3:用戶 APP 強退 / 閃退之後重新下單

心急的用戶可能會重啓流程 / 重啓 App / 重啓手機。在這種強制的手段下,任何技術手段都會失效。

場景 4:黑客或惡意用戶

黑客或惡意用戶使用 postman 等網絡工具,重複惡意提交訂單。

重複下單問題與冪等性問題

重複下單問題,本質上,就是下單操作的冪等性問題

說到底,“下單防重”的問題是屬於 “接口冪等性” 的問題範疇。

什麼是冪等性問題?

所謂冪等性,就是一次操作和多次操作同一個資源,所產生的影響均與一次操作的影響相同。

" 冪等(idempotent、idempotence)是一個數學與計算機學概念,常見於抽象代數中。

冪等函數,或冪等方法,是指可以使用相同參數重複執行,並能獲得相同結果的函數。

冪等性,用數學語言表達就是:

f(x)=f(f(x))

維基百科的冪等性定義如下:

冪等(idempotent、idempotence)是一個數學與計算機學概念,常見於抽象代數中。

冪等函數,或冪等方法,是指可以使用相同參數重複執行,並能獲得相同結果的函數。

這些函數不會影響系統狀態,也不用擔心重複執行會對系統造成改變。

例如,“setTrue()”函數就是一個冪等函數, 無論多次執行,其結果都是一樣的,更復雜的操作冪等保證是利用唯一交易號 (流水號) 實現.

通俗點說:

一個接口如果冪等,不管被調多少次,只要參數不變,結果也不變。

冪等性是對於寫操作來說的,一個寫操作,一般都需要保證:

上述內容選自尼恩這篇硬核文章:

最系統的冪等性方案:一鎖二判三更新

如何解決接口冪等問題

接口接口冪等問題,只需記住一句口令:一鎖、二判、三更新。只需嚴格按照這個過程,那麼就可以解決接口冪等問題,總結如下:

  1. 一鎖:先加鎖,可以加分佈式鎖、悲觀鎖都可以,但是一定是一個互斥鎖。

  2. 二判:進行冪等性判斷,可以基於狀態機、業務流水錶、數據庫唯一索引等,進行重複操作的判斷。

  3. 三更新:對數據進行更新,將數據進行持久化。

關於冪等性方案,請參見尼恩這篇硬核文章:

最系統的冪等性方案:一鎖二判三更新 

如何解決重複下單問題?

方案一:提交訂單按鈕置灰

防止用戶提交,最常規的做法,就是客戶端點擊下單之後,在收到服務端響應之前,按鈕置灰。

前端頁面直接防止用戶重複提交表單,但網絡錯誤會導致重傳,很多 RPC 框架、網關都有自動重試機制,所以重複請求在前端側無法完全避免。

當然,這種方案也不是真的沒有價值。

這種方案可以在高併發場景下,從瀏覽器端去攔住一部分請求,減少後端服務器的處理壓力,達到過濾流量的效果。

**方案一優點:**簡單。基本可以防止重複點擊提交按鈕造成的重複提交問題。

**方案一缺點:**前進後退操作,或者 F5 刷新頁面等問題並不能得到解決。 

方案二:請求唯一 ID + 數據庫唯一索引約束

首先來向大家介紹一種最簡單的、成本最低的解決方案。

防重是第一步,需要識別是否重複請求,

所以,需要客戶端在請求下單接口的時候,需要生成一個唯一的請求號:requestId,服務端拿這個請求號,判斷是否重複請求。

核心流程圖:

實現的邏輯,流程如下:

  1. 當用戶進入訂單提交界面的時候,調用後端獲取請求唯一 ID,並將唯一 ID 值埋點在頁面裏面。

  2. 當用戶點擊提交按鈕時,後端檢查這個唯一 ID 是否用過,如果沒有用過,繼續後續邏輯;如果用過,就提示重複提交。

  3. 最關鍵的一步操作,就是把這個唯一 ID 存入業務表中,同時設置這個字段爲唯一索引類型,從數據庫層面做防止重複提交。

對於下單流量不算高的系統,可以採用這種 請求唯一 ID + 數據表增加唯一索引約束 ` 的方式,來防止接口重複提交

但是這個併發量太低,10wqps 高併發, 這個根本沒法滿足。

方案三:reids 分佈式鎖 + 請求唯一 ID

在上一個方案中,我們詳細的介紹了對於下單流量不算高的系統,可以通過 請求唯一 ID + 數據表增加唯一索引約束 ` 這種方案來實現防止接口重複提交

隨着業務的快速增長,每一秒的下單請求次數,可能從幾十上升到幾百甚至幾萬。

面對這種下單流量越來越高的場景,此時數據庫的訪問壓力會急劇上升,數據庫會成爲整個下單流程的瓶頸。

對於這樣的場景,我們可以選擇引入緩存中間件來緩解數據庫高併發場景下的壓力,

下面,我們以引入redis緩存中間件,向大家介紹具體的解決方案。

流程如下:

  1. 當用戶進入訂單提交界面的時候,調用後端獲取請求唯一 ID,同時後端將請求唯一 ID 存儲到redis中再返回給前端,前端將唯一 ID 值埋點在頁面裏面。

  2. 當用戶點擊提交按鈕時,後端檢查這個請求唯一 ID 是否存在,如果不存在,提示錯誤信息;如果存在,繼續後續檢查流程。

  3. 使用redis的分佈式鎖服務,對請求 ID 在限定的時間內進行加鎖,如果加鎖成功,繼續後續流程;如果加鎖失敗,提示說明:服務正在處理,請勿重複提交。

  4. 最後一步,如果加鎖成功後,需要將鎖手動釋放掉,以免再次請求時,提示同樣的信息;同時如果任務執行成功,需要將redis中的請求唯一 ID 清理掉。

至於數據庫是否需要增加字段唯一索引,理論上可以不用加,如果加了更保險。

這個通過擴展,可以滿足 10wqps 高併發要求。

具體的擴展方案, 即將在 《尼恩 Java 面試寶典》 配套視頻 發佈。

方案四:reids 分佈式鎖 + token

在上一個方案中,每次提交訂單的時候,都需要調用服務端獲取請求唯一 ID:requestId,然後才能提交,這裏面存在以下問題:

下單鏈路中,多了的一次請求, 這一次請求專門用於請求 request id。這次請求是否可以減少呢?

當然是可以的,比如, 可以用戶的請求的特徵數據,根據特定規則生成 token,來替代 那個專用的 request id。

而不用專門去來減少一次客戶端與服務端之間的交互次數,提高下單流程效率。

特定規則生成 token, 比如說,可以組合一些核心參數,去生成 token, 核心參數包括:
應用名+接口名+方法名+請求參數簽名(請求header、body參數,取SHA1值)

組合一些核心參數,去生成 token ,大致 流程如下:

  1. 用戶點擊提交按鈕,服務端接受到請求後,通過規則計算出本次請求唯一 ID 值

  2. 使用redis的分佈式鎖服務,對請求 ID 在限定的時間內嘗試進行加鎖,如果加鎖成功,繼續後續流程;如果加鎖失敗,說明服務正在處理,請勿重複提交。

  3. 最後一步,如果加鎖成功後,需要將鎖手動釋放掉,以免再次請求時,提示同樣的信息。

方案四和方式三的最大不同,在於 唯一請求 ID 的生成 環節,

方案四 放在服務端通過組合來實現 唯一請求 ID 的生成 ,在保證防止接口重複提交的效果同時,也可以顯著的降低接口測試複雜度!

方案四的性能,比方案三更高。

方案五:技術 + 產品 + 運營支持

如果經過上述方案處理,還是會有用戶誤操作,直到收到兩份商品才發現下重了。

在實際設計中,無論多麼好的技術,也不可能 100% 的攔截所有的可能性,必須依靠 技術+產品設計+運營支持 的綜合手段才能解決這類問題。

此時就得依靠運營 / 客服的支持了。

所以即便京東這一類電商等也是配合運營手段進行處理。

實操:reids 分佈式鎖 + token 解決重複下單的問題

只講理論,是耍流氓

40 歲老架構師一直強調, 實操,實操,實操纔是王道

比如咱們社羣的 k8s 實操:

比如咱們社羣的 AT+TCC 模式混合事務實操 ):

此實操即將配合 《尼恩 Java 面試寶典視頻》發佈

接下來,咱們開始 reids 分佈式鎖 + token 解決重複下單的問題的實操

此實操即將配合 《尼恩 Java 面試寶典視頻》發佈

實操 step1:使用 AOP 進行 BizToken 的無入侵生成

定義一個註解 BizToken

在業務層或者 控制層,進行 BizToken 的使用

實操 step2:編寫服務驗證邏輯,通過 aop 代理方式實現

此 aop 切面的 具體的實操演示,請參見 《尼恩 Java 面試寶典》 視頻

實操 step3:使用 redission 分佈式鎖保證冪等

在 BizToken 校驗邏輯用到了redis分佈式鎖保證冪等,

redission 分佈式鎖 具體實現邏輯如下:

通過封裝 redission 的分佈式鎖來實現 鎖的功能:

具體的實現,委託到 redission 的分佈式鎖來實現

具體的實操演示,請參見 《尼恩 Java 面試寶典》 視頻

10wqps 高併發,防止重複下單總結

防止重複下單,本質上就是先做重複判斷,然後服務端做好冪等性控制,結合實際業務場景選擇相應的方案。

實現冪等性需要先理解自身業務需求,根據業務邏輯來實現這樣才合理,處理好其中的每一個結點細節,完善整體的業務流程設計,才能更好的保證系統正常運行。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/QZ4z_I5Gl6FNz6jcyGkknA