大廠都怎么防止重復下單?
1.問題背景
最簡單的:DB事務。如創建訂單時,同時往訂單表、訂單商品表插數據,這些Insert須在同一事務執行。
Order服務調用Pay服務,剛好網絡超時,然后Order服務開始重試機制,于是Pay服務對同一支付請求,就接收到了兩次,而且因為輪詢負載均衡算法,落在了不同業務節點!所以一個分布式系統接口,須保證冪等性。
2.如何避免重復下單?
前端頁面也可直接防止用戶重復提交表單,但網絡錯誤會導致重傳,很多RPC框架、網關都有自動重試機制,所以重復請求在前端側無法完全避免!問題最后還是如何保證服務接口的冪等性。
2.1 如何判斷請求是重復的?
- 插入訂單前,先查一下訂單表,有無重復訂單? 不優:難以用SQL條件定義到底什么是“重復訂單”
- 訂單的用戶、商品、價格一樣就是重復訂單? 萬一這用戶就是連續下了倆一模一樣訂單呢?
所以保證冪等性要做到:
2.1.1 每個請求須有唯一標識
比如訂單支付請求,得包含訂單id,一個訂單id最多只能成功支付一次。
2.1.2 每次處理完請求后,須有記錄標識該請求已被處理
在MySQL中記錄一個狀態字段。如支付之前記錄一條這個訂單的支付流水。
2.1.3 每次接收請求時,判斷之前是否處理過
若有一個訂單已支付,就肯定已有一條支付流水。若重復發送這個請求,則此時先插入支付流水,發現orderId已存在,唯一約束生效,報錯重復Key。就不會再重復扣款。
在往DB插記錄時,一般不提供主鍵,而由DB在插入時自動生成。這樣重復的請求就會導致插入重復的數據。MySQL的主鍵自帶唯一性約束,若在一條INSERT語句提供主鍵,且該主鍵值在表中已存在,則該條INSERT會執行失敗。因此可利用DB的“主鍵唯一約束”,在插數據時帶上主鍵,以此實現創建訂單接口的冪等性。
給Order服務添加一個“orderId生成”的接口,無參,返回值就是一個【全局唯一】訂單號。在用戶進入創建訂單頁面時,前端頁面先調用該orderId生成接口得到一個訂單號,在用戶提交訂單時,在創建訂單的請求中攜帶該訂單號。
該訂單號其實就是訂單表的主鍵,于是,重復請求中帶的都是同一訂單號。訂單服務在訂單表中插入數據的時候,執行的這些重復INSERT語句中的主鍵,也都是同一個訂單號。而DB唯一約束保證,只有一次INSERT執行成功。
實際要結合業務,如使用Redis,用orderId作為唯一K。只有成功插入這個支付流水,才可執行扣款。
要求是支付一個訂單,須插入一條支付流水,order_id建立一個唯一鍵。你在支付一個訂單前,先插入一條支付流水,order_id就已經傳過去了。就能寫一個標識到Redis中,set order_id payed,當重復請求過來時,先查Redis的order_id對應的value,若為payed說明已支付,就別再重復支付!
然后再重復支付訂單時,寫嘗試插入一條支付流水,DB會報唯一鍵沖突,整個事務回滾。保存一個是否處理過的標識也可以,服務的不同實例可以一起操作Redis。

若因重復訂單導致插入 t_order 失敗,則Order服務不要把該錯誤返給前端頁面。否則,就可能出現用戶點擊創建訂單按鈕后,頁面提示創建訂單失敗,而實際上訂單創建成功了。
正確做法:這種case,訂單服務直接返回訂單創建成功。
3.解決ABA
3.1 什么是ABA
如訂單支付后,seller要發貨,發貨完成后要填個快遞單號。假設seller填個666,剛填完,發現填錯了,趕緊再修改成888。對訂單服務,這就是2個更新訂單的請求。系統異常時666請求到了,單號更成666,接著888請求到了,單號又更新成888,但是666更新成功的響應丟了,調用方沒收到成功響應,自動重試,再次發起666請求,單號又被更新成666了,這數據顯然就錯了!

3.2 解決方案
訂單主表增加version列。每次查詢訂單時,版本號要隨著訂單數據返回給頁面。頁面在更新數據的請求中,把這個版本號作為更新請求的參數,帶回給訂單更新接口。
訂單服務在更新數據的時候,需要比較訂單的版本號是否和消息中的一致:
- 不一致拒絕更新數據
- 一致還需再更新數據的同時,將version+1。“比較版本號、更新數據和版本號+1”的過程須在同一事務執行
UPDATE orders set tracking_number = 666,
version = version + 1
WHERE version = 8;
在這條SQL的WHERE條件中,version值需要頁面在更新的時候通過請求傳進來。
通過該版本號,就能保證,從我打開這條訂單記錄開始,一直到我更新這條訂單記錄成功,期間沒有其他人修改過該訂單數據。若有,則DB中的version就會改變,那我的更新操作就會執行失敗。我就只能重新查詢新版本的訂單數據,再嘗試更新。
有了這個版本號,前文的ABA即有兩個case:
- 把運單號更新為666成功,更新為888的請求帶著舊版本號,就更新失敗,頁面提示用戶更新888失敗
- 666更新成功后,888帶著新版本號,888更新成功。這時即使重試的666請求再來,因為它和上一條666請求帶相同版本號,上一條請求更新成功后,這個版本號已經變了,所以重試請求的更新必然失敗
無論哪種情況,DB中的數據與頁面上給用戶的反饋都是一致的。這就實現了冪等更新且避免ABA。

4.總結
- 創建訂單服務,可通過預生成訂單號,然后利用DB的訂單號唯一約束,避免重復寫入訂單,實現創建訂單服務的冪等性
- 更新訂單服務,通過一個版本號機制,每次更新數據前校驗版本號,更新數據同時自增版本號,這樣的方式,來解決ABA問題,確保更新訂單服務的冪等性
兩種冪等的實現方法,就可以保證,無論請求是不是重復,訂單表中的數據都是正確的。
實現訂單冪等的方法,完全可以套用在其他需要實現冪等的服務中,只需要這個服務操作的數據保存在數據庫中,并且有一張帶有主鍵的數據表即可。
































