10w qps 高併發,如何防止重複下單?
基礎知識:電商訂單支付核心流程
首先,來看看 訂單支付的業務流程和交互流程。
圖解:訂單支付的業務流程和交互流程
結合下圖來看看 訂單支付的業務流程和交互流程。
訂單支付流程, 分爲 大致 的 6 個步驟 :
1. 下單 / 結算:
下單作爲支付的入口,但並非起點,
支付相關的金額等信息全部來至結算,此時訂單處於 未支付 狀態。
2. 申請支付:
用戶開始申請支付,客戶端調用支付服務,
此時在支付系統內產生一筆訂單支付流水,這筆支付流水處於 未支付 狀態。
3. 發起支付:
支付服務調用 第三方支付平臺,
通常, 第三方支付平臺 是 錢包類的支付方式,
在發起支付這一步驟,支付平臺會響應一些支付的鏈接,客戶端會對鏈接進行相應的處理。
4. 錢包支付:
用戶進行支付,
用戶 APP 端直接拉去錢包進行支付。
5. 支付回調:
用戶完成支付之後,三方支付平臺會回調 商戶的支付服務 接口,通知支付結果。
6. 更新訂單狀態:
支付服務 確認訂單支付完成後,會向 訂單服務同步 支付的結果。
訂單服務變更服務的狀態:未支付
變更爲 待發貨
。
客戶端通過輪詢、長輪詢,或者服務端主動推送的方式,在界面上變更訂單狀態。
圖解:支付狀態的變化
如下圖,從支付流水角度來分析一下支付狀態的變化:
-
從未支付,到有支付結果的終態,中間還有一箇中間狀態:
支付中
-
戶通過打開錢包 --》完成支付 --》支付回調,這段時間的支付流水就處於:
支付中
重複下單的定義、危害、應對策略
什麼是重複下單
現在問題來了, 什麼是重複下單?
用戶在下單頁面進行下單時,由於用戶點擊下單按鈕 多次 、或者 重試策略 導致在訂單服務中接收到了 兩次同樣 的下單請求。
重複下單帶來的危害
重複下單場景,第 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()”函數就是一個冪等函數, 無論多次執行,其結果都是一樣的,更復雜的操作冪等保證是利用唯一交易號 (流水號) 實現.
通俗點說:
一個接口如果冪等,不管被調多少次,只要參數不變,結果也不變。
冪等性是對於寫操作來說的,一個寫操作,一般都需要保證:
-
冪等性
-
可用性
-
ACID 事務屬性。
上述內容選自尼恩這篇硬核文章:
如何解決接口冪等問題
接口接口冪等問題,只需記住一句口令:一鎖、二判、三更新。只需嚴格按照這個過程,那麼就可以解決接口冪等問題,總結如下:
一鎖:先加鎖,可以加分佈式鎖、悲觀鎖都可以,但是一定是一個互斥鎖。
二判:進行冪等性判斷,可以基於狀態機、業務流水錶、數據庫唯一索引等,進行重複操作的判斷。
三更新:對數據進行更新,將數據進行持久化。
關於冪等性方案,請參見尼恩這篇硬核文章:
如何解決重複下單問題?
方案一:提交訂單按鈕置灰
防止用戶提交,最常規的做法,就是客戶端點擊下單之後,在收到服務端響應之前,按鈕置灰。
前端頁面直接防止用戶重複提交表單,但網絡錯誤會導致重傳,很多 RPC 框架、網關都有自動重試機制,所以重複請求在前端側無法完全避免。
當然,這種方案也不是真的沒有價值。
這種方案可以在高併發場景下,從瀏覽器端去攔住一部分請求,減少後端服務器的處理壓力,達到過濾流量的效果。
**方案一優點:**簡單。基本可以防止重複點擊提交按鈕造成的重複提交問題。
**方案一缺點:**前進後退操作,或者 F5 刷新頁面等問題並不能得到解決。
方案二:請求唯一 ID + 數據庫唯一索引約束
首先來向大家介紹一種最簡單的、成本最低的解決方案。
防重是第一步,需要識別是否重複請求,
所以,需要客戶端在請求下單接口的時候,需要生成一個唯一的請求號:requestId
,服務端拿這個請求號,判斷是否重複請求。
核心流程圖:
實現的邏輯,流程如下:
-
當用戶進入訂單提交界面的時候,調用後端獲取請求唯一 ID,並將唯一 ID 值埋點在頁面裏面。
-
當用戶點擊提交按鈕時,後端檢查這個唯一 ID 是否用過,如果沒有用過,繼續後續邏輯;如果用過,就提示重複提交。
-
最關鍵的一步操作,就是把這個唯一 ID 存入業務表中,同時設置這個字段爲唯一索引類型,從數據庫層面做防止重複提交。
對於下單流量不算高的系統,可以採用這種 請求唯一 ID + 數據表增加唯一索引約束 ` 的方式,來防止接口重複提交!
但是這個併發量太低,10wqps 高併發, 這個根本沒法滿足。
方案三:reids 分佈式鎖 + 請求唯一 ID
在上一個方案中,我們詳細的介紹了對於下單流量不算高的系統,可以通過 請求唯一 ID + 數據表增加唯一索引約束 ` 這種方案來實現防止接口重複提交!
隨着業務的快速增長,每一秒的下單請求次數,可能從幾十上升到幾百甚至幾萬。
面對這種下單流量越來越高的場景,此時數據庫的訪問壓力會急劇上升,數據庫會成爲整個下單流程的瓶頸。
對於這樣的場景,我們可以選擇引入緩存中間件來緩解數據庫高併發場景下的壓力,
下面,我們以引入redis
緩存中間件,向大家介紹具體的解決方案。
流程如下:
-
當用戶進入訂單提交界面的時候,調用後端獲取請求唯一 ID,同時後端將請求唯一 ID 存儲到
redis
中再返回給前端,前端將唯一 ID 值埋點在頁面裏面。 -
當用戶點擊提交按鈕時,後端檢查這個請求唯一 ID 是否存在,如果不存在,提示錯誤信息;如果存在,繼續後續檢查流程。
-
使用
redis
的分佈式鎖服務,對請求 ID 在限定的時間內進行加鎖,如果加鎖成功,繼續後續流程;如果加鎖失敗,提示說明:服務正在處理,請勿重複提交。 -
最後一步,如果加鎖成功後,需要將鎖手動釋放掉,以免再次請求時,提示同樣的信息;同時如果任務執行成功,需要將
redis
中的請求唯一 ID 清理掉。
至於數據庫是否需要增加字段唯一索引,理論上可以不用加,如果加了更保險。
這個通過擴展,可以滿足 10wqps 高併發要求。
具體的擴展方案, 即將在 《尼恩 Java 面試寶典》 配套視頻 發佈。
方案四:reids 分佈式鎖 + token
在上一個方案中,每次提交訂單的時候,都需要調用服務端獲取請求唯一 ID:requestId,然後才能提交,這裏面存在以下問題:
下單鏈路中,多了的一次請求, 這一次請求專門用於請求 request id。這次請求是否可以減少呢?
當然是可以的,比如, 可以用戶的請求的特徵數據,根據特定規則生成 token,來替代 那個專用的 request id。
而不用專門去來減少一次客戶端與服務端之間的交互次數,提高下單流程效率。
特定規則生成 token, 比如說,可以組合一些核心參數,去生成 token, 核心參數包括:
應用名+接口名+方法名+請求參數簽名(請求header、body參數,取SHA1值)
組合一些核心參數,去生成 token ,大致 流程如下:
-
用戶點擊提交按鈕,服務端接受到請求後,通過規則計算出本次請求唯一 ID 值
-
使用
redis
的分佈式鎖服務,對請求 ID 在限定的時間內嘗試進行加鎖,如果加鎖成功,繼續後續流程;如果加鎖失敗,說明服務正在處理,請勿重複提交。 -
最後一步,如果加鎖成功後,需要將鎖手動釋放掉,以免再次請求時,提示同樣的信息。
方案四和方式三的最大不同,在於 唯一請求 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