訂單相關問題

從下單開始、支付、發貨,收貨,每一個環節,都少不了更新訂單,每一次更新又需要同時更新好幾張表。
這些操作可能被隨機分佈到很多臺服務器上執行,服務器有可能故障,網絡有可能出問題。

那麼如何才能保證訂單服務的數據一致性呢?

正確使用數據庫的事務
eg.創建訂單時,要同時往訂單表和訂單商品表中插入數據,那這些插入數據的INSERT必須在一個數據庫事務中執行,數據庫的事務可以確保:執行這些INSERT語句,共赴生死!
但還有很多難以發現的坑存在

1 基本功能和數據表
任何系統的訂單服務都是獨一無二的,基於不同業務,有很多個人限定.不過核心都大同小異,讓我們研究其共同點.

基本功能
創建訂單
隨着購物流程更新訂單狀態
查詢訂單,包括用訂單數據生成各種報表
數據表
訂單主表:也叫訂單表,保存訂單的基本信息
訂單主表和後面的幾個子表都是一對多,關聯的外鍵就是訂單主表的主鍵,也就是訂單號
訂單商品表:保存訂單中的商品信息
訂單支付表:保存訂單的支付和退款信息
訂單優惠表:保存訂單使用的所有優惠信息。
2 如何避免重複下單?
用戶在瀏覽器頁面上點擊“提交訂單”按鈕的時候,瀏覽器就會給訂單系統發一個創建訂單的請求,訂單系統的後端服務,在收到請求之後,往數據庫的訂單表插入一條訂單數據,創建訂單成功.

假如用戶點擊“創建訂單”的按鈕時手抖了,點了兩下,結果是什麼?創建了兩條一模一樣的訂單.這可咋辦呢?

有人說,前端頁面上應該防止用戶重複提交表單.沒啥毛病,但是,網絡錯誤會導致重傳,很多RPC框架、網關都會有自動重試機制,所以對於訂單服務來說,重複請求這個事兒,你是沒辦法完全避免的.

所以問題的本質其實是如何保證訂單服務的冪等性.
簡單來說就是一個冪等的方法,使用同樣的參數,對它進行調用多次和調用一次,對系統產生的影響是一樣的.
所以只有冪等的方法才能做到防重.
一個冪等的創建訂單請求,不管發送多少次,結果都是數據庫只有一條新創建的訂單記錄。

2.1 怎麼判斷請求是否重複
插入訂單數據前,先查一下訂單表裏面有沒有重複的訂單?
這可不太行,因爲你很難用SQL的條件來定義“重複的訂單”
訂單用戶一樣、商品一樣、價格一樣,就是重複訂單?萬一這搞笑用戶就是連續下了倆一模一樣訂單?

2.2 最佳實踐
在往數據庫插入一條記錄時,一般不提供主鍵,而由數據庫在插入時自動生成一個主鍵。這樣重複的請求就會導致插入重複數據。

表的主鍵自帶唯一約束,如果我們在一條INSERT語句中提供了主鍵,並且這個主鍵的值在表中已經存在,那這條INSERT會執行失敗.
因此可以利用數據庫的這種“主鍵唯一約束”特性,在插入數據的時候帶上主鍵,以此實現創建訂單接口的冪等性.

給訂單服務添加一個“訂單號生成”的接口,無參,返回值就是一個全局唯一的訂單號。在用戶進入創建訂單的頁面時,前端頁面先調用這個生成訂單號接口得到一個訂單號,在用戶提交訂單的時候,在創建訂單的請求中帶着這個訂單號。

這個訂單號也就是訂單表的主鍵,如此這些重複請求中帶的都是同一個訂單號。訂單服務在訂單表中插入數據的時候,執行的這些重複INSERT語句中的主鍵,也都是同一個訂單號。數據庫的唯一約束就可以保證,只有一次INSERT語句是執行成功的

冪等創建訂單的時序圖

如果因爲重複訂單導致插入訂單表失敗,訂單服務不要把這個錯誤返回給前端頁面.
否則,就可能出現用戶點擊創建訂單按鈕後,頁面提示創建訂單失敗,而實際上訂單卻創建成功了.
正確的做法是,遇到這種情況,訂單服務直接返回訂單創建成功即可.

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

時序圖

3.2 解決方案
通用的解決方案
訂單主表增加一列version。每次查詢訂單的時候,版本號要隨着訂單數據返回給頁面。
頁面在更新數據的請求中,把這個版本號作爲更新請求的參數,帶回給訂單更新接口。

訂單服務在更新數據的時候,需要比較訂單的版本號是否和消息中的一致:

不一致 拒絕更新數據
一致 還需要再更新數據的同時,把版本號+1。“比較版本號、更新數據和版本號+1”,這個過程必須在同一個事務裏面執行。
UPDATE orders set tracking_number = 666, version = version + 1
WHERE version = 8;
1
2
在這條SQL的WHERE條件中,version的值需要頁面在更新的時候通過請求傳進來。

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

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

把運單號更新爲666的操作成功了,更新爲888的請求帶着舊版本號,那就會更新失敗,頁面提示用戶更新888失敗
第二種情況,666更新成功後,888帶着新的版本號,888更新成功。這時候即使重試的666請求再來,因爲它和上一條666請求帶着相同的版本號,上一條請求更新成功後,這個版本號已經變了,所以重試請求的更新必然失敗
無論哪種情況,數據庫中的數據與頁面上給用戶的反饋都是一致的。這樣就可以實現冪等更新並且避免了ABA問題

下圖展示case1

4 總結
對於創建訂單服務來說,可以通過預先生成訂單號,然後利用數據庫中訂單號的唯一約束這個特性,避免重複寫入訂單,實現創建訂單服務的冪等性
對於更新訂單服務,可以通過一個版本號機制,每次更新數據前校驗版本號,更新數據同時自增版本號,這樣的方式,來解決ABA問題,確保更新訂單服務的冪等性。
兩種冪等的實現方法,就可以保證,無論請求是不是重複,訂單表中的數據都是正確的。

實現訂單冪等的方法,你完全可以套用在其他需要實現冪等的服務中,只需要這個服務操作的數據保存在數據庫中,並且有一張帶有主鍵的數據表即可

原文鏈接:https://blog.csdn.net/qq_33589510/article/details/104954137

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章