天天看點

【防止重複下單】分布式系統接口幂等性實作方案3 怎麼解決ABA問題?4 總結

問題背景

最容易想到的使用資料庫事務。

比如建立訂單時,要同時往訂單表、訂單商品表插入資料,那這些插入資料的INSERT必須在一個資料庫事務執行,資料庫事務可保證:執行這些INSERT語句,同生共死!

訂單服務調用支付服務,不巧網絡逾時,然後訂單服務走重試機制,給你重試了一把,支付服務收到一個支付請求兩次,而且因為輪詢負載均衡算法落在不同機器!

是以一個分布式系統中的接口,要如何保證幂等性呢?

如何避免重複下單?

評論裡有同學說,前端頁面直接防止使用者重複送出表單。沒啥毛病,但網絡錯誤會導緻重傳,很多RPC架構、網關都有自動重試機制,是以重複請求無法避免。問題最終還是歸結于如何保證服務接口的幂等性。

如何判斷請求是重複的?

插入訂單前,先查一下訂單表裡面有無重複訂單?

這可不好,你很難用SQL條件定義什麼是“重複訂單”

訂單的使用者、商品、價格一樣就是重複訂單?

萬一這使用者就是連續下了倆一模一樣訂單呢!

是以保證幂等性要做到如下幾點!

每個請求須有唯一辨別

比如訂單支付請求,得包含訂單id,一個訂單id最多支付一次!

每次處理完請求後,須有記錄辨別該請求已被處理

在MySQL中記錄一個狀态字段。比如支付之前記錄一條這個訂單的支付流水。

每次接收請求判斷之前是否處理過

若有一個訂單已支付,就已經有了一條支付流水。若重複發送這個請求,則此時先插入支付流水,orderId已存在,唯一鍵限制生效,報錯插入不進去。就不會再重複扣款。

在往db插條記錄時,一般不提供主鍵,而由資料庫在插入時自動生成一個主鍵。這樣重複的請求就會導緻插入重複資料。

MySQL的主鍵自帶唯一性限制,若在一條INSERT語句提供主鍵,且該主鍵值在表中已存在,則該條INSERT會執行失敗。是以可利用db的“主鍵唯一限制”,在插資料時帶上主鍵,以此實作建立訂單接口的幂等性。

給訂單服務添加一個“orderId生成”的接口,無參,傳回值就是一個全局唯一訂單号。在使用者進入建立訂單頁面時,前端頁面先調用該orderId生成接口得到一個訂單号,在使用者送出訂單的時候,在建立訂單的請求中攜帶該訂單号。

該訂單号其實就是訂單表的主鍵,如此一來,重複請求中帶的都是同一訂單号。訂單服務在訂單表中插入資料的時候,執行的這些重複INSERT語句中的主鍵,也都是同一個訂單号。而資料庫的唯一限制可保證,隻有一次INSERT執行成功。

實際要結合業務,比如使用Redis,用orderId作為唯一鍵。隻有成功插入這個支付流水,才可執行扣款。

要求是支付一個訂單,必須插入一條支付流水,order_id建立一個唯一鍵。

你在支付一個訂單前,先插入一條支付流水,order_id就已經傳過去了。就可以寫一個辨別到Redis中,set order_id payed,當重複請求過來時,先查Redis的order_id對應的value,若為payed說明已支付,就别重複支付了!

然後再重複支付訂單時,寫嘗試插入一條支付流水,db會報唯一鍵沖突,整個事務復原即可。

儲存一個是否處理過的辨別也可以,服務的不同執行個體可以一起操作Redis。

幂等建立訂單的時序圖

【防止重複下單】分布式系統接口幂等性實作方案3 怎麼解決ABA問題?4 總結
如果因為重複訂單導緻插入訂單表失敗,訂單服務不要把這個錯誤傳回給前端頁面。否則,就可能出現使用者點選建立訂單按鈕後,頁面提示建立訂單失敗,而實際上訂單卻建立成功了。正确的做法是,遇到這種情況,訂單服務直接傳回訂單建立成功即可。

3 怎麼解決ABA問題?

3.1 什麼是 ABA?

比如訂單支付後,賣家要發貨,發貨完成後要填個快遞單号。假設說,賣家填了個666,剛填完,發現填錯了,趕緊再修改成888。對訂單服務來說,這就是2個更新訂單的請求。系統異常時666請求到了,單号更成666,接着888請求到了,單号又更新成888,但是666更新成功的響應丢了,調用方沒收到成功響應,自動重試,再次發起666請求,單号又被更新成666了,這資料顯然就錯了.

時序圖

【防止重複下單】分布式系統接口幂等性實作方案3 怎麼解決ABA問題?4 總結

3.2 解決方案

通用的解決方案

訂單主表增加一列version。每次查詢訂單的時候,版本号要随着訂單資料傳回給頁面。

頁面在更新資料的請求中,把這個版本号作為更新請求的參數,帶回給訂單更新接口。

訂單服務在更新資料的時候,需要比較訂單的版本号是否和消息中的一緻:

不一緻 拒絕更新資料

一緻 還需要再更新資料的同時,把版本号+1。“比較版本号、更新資料和版本号+1”,這個過程必須在同一個事務裡面執行。

UPDATE orders set tracking_number = 666, version = version + 1
    WHERE version = 8;      

在這條SQL的WHERE條件中,version的值需要頁面在更新的時候通過請求傳進來。

通過這個版本号,就可以保證,從我打開這條訂單記錄開始,一直到我更新這條訂單記錄成功,這個期間沒有其他人修改過這條訂單資料。因為,如果有其他人修改過,資料庫中的版本号就會改變,那我的更新操作就不會執行成功。我隻能重新查詢新版本的訂單資料,然後再嘗試更新。

有了這個版本号,前文的ABA即有兩個 case

把運單号更新為666的操作成功了,更新為888的請求帶着舊版本号,那就會更新失敗,頁面提示使用者更新888失敗

第二種情況,666更新成功後,888帶着新的版本号,888更新成功。這時候即使重試的666請求再來,因為它和上一條666請求帶着相同的版本号,上一條請求更新成功後,這個版本号已經變了,是以重試請求的更新必然失敗

無論哪種情況,資料庫中的資料與頁面上給使用者的回報都是一緻的。這樣就可以實作幂等更新并且避免了ABA問題

下圖展示case1

【防止重複下單】分布式系統接口幂等性實作方案3 怎麼解決ABA問題?4 總結

4 總結

  • 對于建立訂單服務來說,可以通過預先生成訂單号,然後利用資料庫中訂單号的唯一限制這個特性,避免重複寫入訂單,實作建立訂單服務的幂等性
  • 對于更新訂單服務,可以通過一個版本号機制,每次更新資料前校驗版本号,更新資料同時自增版本号,這樣的方式,來解決ABA問題,確定更新訂單服務的幂等性。

兩種幂等的實作方法,就可以保證,無論請求是不是重複,訂單表中的資料都是正确的。

實作訂單幂等的方法,完全可以套用在其他需要實作幂等的服務中,隻需要這個服務操作的資料儲存在資料庫中,并且有一張帶有主鍵的資料表即可