1.引言
這篇博文打算分兩篇來闡述:
第一篇介紹優化背景和優化思路;
第二篇對支撐我們改造的跨語言TCC中間件dtm進行講解。
另外,我們項目使用的是.net core開發的微服務項目,使用的語言是C#。
2.現狀
2.1訂單創建流程
為了說明問題,將下單流程極簡化:
本地驗證優惠券是否被使用->根據前端傳遞的參數構造訂單。
優惠券的驗證
使用本地驗證,看是否已與已購買的訂單進行了綁定,如果綁定說明被使用不允許下單。
庫存沒有處理
現狀是下單時不處理庫存,支付成功后才通過消息異步通知商品中心對庫存進行扣減,如果庫存不足,則進行退款處理,這種補償機制肯定體驗不好。
2.2問題
優惠券的驗證
在高並發情況下,優惠券的驗證不准確,應該去促銷中心驗證,這種本地驗證的問題就是,只驗證了已被使用的優惠券,如果優惠券也正在被其他下單請求使用和校驗的話,會因為對資源的並發訪問而導致均驗證通過。
會發生超賣的情況
在高訪問量情況下,必然會出現同一個商品多人同時購買的情況發生,在下單階段沒有對購買的sku的庫存進行凍結,會導致超賣的發生。
3.解決思路
3.1 對於優惠券
下單過程中,向促銷中心發送對優惠券的使用申請,占用該優惠券,如果已經被占用,則終止當前下單操作。如果未占用,則鎖定該優惠券,等下單成功,更改優惠券的使用狀態為已使用。
3.2 對於庫存
也是類似的思路,比如我要買2個商品A,向商品中心發送對所購買商品庫存的申請,占用2個客戶要購買的商品庫存:
- 如果庫存不足,終止下單
- 如果庫存充足,此時,商品中心的占用庫存+2,可用庫存-2。繼續下單
下單成功后,商品中心的占用庫存-2,可用庫存不變。
3.3 引出TCC理論
為什么上述過程可以避免因為並發而造成的共享資源數據不准確?
上述過程將共享資源,分為“預留資源”和“可用資源”,在業務執行的時候,只操作預留資源,幾乎不會涉及到鎖和資源的爭用,所以它具有很高的性能潛力。即使加鎖,也只是在對庫存進行加減計算那很短的時間加鎖,鎖的范圍很小。
分布式事務解決方案之TCC
上述其實是TCC分布式事務的思想,也是當前使用較廣泛的解決類似“超售”分布式事務問題的技術方案。
可靠消息隊列,是我們項目目前使用的分布式事務方案,可靠消息隊列的局限性是,隔離性差,在並發情況下,共享資源的處理會混亂,會導致優惠券或庫存的處理不准確。
對於微服務的分布式事務的解決:可靠消息隊列和TCC以及saga的組合,能解決大部分分布式事務問題,目前TCC是我們的微服務技術中缺失的比較重要的一塊,已經到了無法滿足業務需求的程度了,需要進行技術突破。
3.4 TCC改造后的下單時序圖
- 第一步,用戶向訂單服務發送下單請求。
- 第二步,創建事務,生成事務 ID(主和子事務),記錄在活動日志中,進入 Try 階段:
- 促銷中心:檢查優惠券的占用狀態,如果未占用,通知下一步進入 Confirm 階段;不可用的話,通知下一步進入 Cancel 階段。
- 商品中心:檢查庫存,庫存充足,則增加占用庫存,減少可用庫存,通知下一步進入 Confirm 階段;庫存不足,通知下一步進入 Cancel 階段。
- 第三步,如果第二步中所有業務都反饋業務可行,就將活動日志中的狀態記錄為 Confirm,進入 Confirm 階段:
- 促銷中心:修改占用優惠券狀態為'已使用'
- 商品中心:被占用庫存的商品狀態改為‘待出貨’,減去占用庫存,可用庫存不變
- 第四步,如果第三步的操作全部完成了,事務就會宣告正常結束。而如果第三步中的任何一方出現了異常,不論是業務異常還是網絡異常,都將會根據活動日志中的記錄,來重復執行該服務的 Confirm 操作,即進行“最大努力交付”。
- 第五步,如果是在第二步,有任意一方反饋業務不可行,或是任意一方出現了超時,就將活動日志的狀態記錄為 Cancel,進入 Cancel 階段:
- 促銷中心:恢復優惠券狀態為'未使用'
- 商品中心:恢復占用庫存
- 第六步,如果第五步全部完成了,事務就會宣告以失敗回滾結束。而如果第五步中的任何一方出現了異常,不論是業務異常還是網絡異常,也都將會根據活動日志中的記錄,來重復執行該服務的 Cancel 操作,即進行“最大努力交付”。
3.5 改造的可行性
由於 TCC 的業務侵入性比較高,需要開發編碼配合,在一定程度上增加了不少工作量,需要投入一定的開發成本,比如更換事務實現方案的替換成本。
所以,通常並不會完全靠裸編碼來實現 TCC,而是會基於某些分布式事務中間件(如阿里開源的Seata)來完成,以盡量減輕一些編碼工作量。
但Seata有語言限制,僅支持java,對.net core來說,適合的TCC中間件有:https://github.com/yedf/dtm,是用go語言開發的支持大部分主流的開發語言,其中針對.net core有相應的sdk可供使用(是張善友大佬提供的)。
4.其他常用分布式事務的適用場景
4.1 可靠消息隊列
可靠消息隊列的實現原理,雖然它也能保證最終的結果是相對可靠的,過程也足夠簡單(相對於 TCC 來說),但可靠消息隊列的整個實現過程完全沒有任何隔離性可言。
假如訂單創建成功,創建之前已經檢測了庫存正好夠,於是乎發送消息通知倉庫服務扣減庫存准備發貨,這是正常無並發情況的邏輯。但由於該商品熱賣,有大量購買該商品的請求並發,而碰巧在我檢測庫存之后創建訂單發送消息時(還未來得及扣減庫存),正好已經有交易先於我完成了(它搶先一步扣減了庫存),這樣就導致此刻倉庫發現現在已經沒有庫存可被扣除了,正常情況是無法發貨,訂單應該作廢的。但因為base理論要求只要我發出去成功的消息后,我就當整個創建訂單是成功的了,剩下的就交給倉庫服務去解決它的問題了,哪怕倉庫服務由於並發的出現而沒有庫存了,也得讓整個過程達到最終一致,所以消息不斷重發,倉庫服務不斷的嘗試扣減庫存,直至操作成功(比如補充了庫存),或者被人工介入為止。
可靠消息隊列屬於base理論的技術實現手段之一,但它僅適合對資源隔離性要求不高的場景,或者說是共享資源並發低到不會發生沖突的場景。但對下單扣減商品庫存這種並發場景無法避免的情況下,缺乏隔離性,就會導致類似“超售”的問題。
如果這件事情是發生在剛性事務且隔離級別足夠的情況下,其實是可以完全避免的。比如mysql“可重復讀”(Repeatable Read)的隔離級別,可以保證后面提交的事務會因為無法獲得鎖而導致失敗。但用可靠消息隊列就無法保證這一點了。
然而鎖定庫存,顯然系統的性能會極地,在促銷活動的場景,服務甚至不可用,所以分布式事務要避免粗粒度的鎖的使用。
4.2 saga事務
SAGA 的意思是“長篇故事、長篇記敘、一長串事件”,它的思想是基於數據補償代替回滾的解決思路。回滾意味着相當於沒發生,而補償意味着發生了,但我可以彌補你。比如訂單創建成功了,但我突然發現沒有庫存了,那我再把訂單取消掉,這樣給客戶不友好,前一秒還給我提示成功了,后一面又提示庫存不足,取消訂單。
saga最初提出了一種如何提升“長時間事務”(Long Lived Transaction)運作效率的方法,大致思路是把一個大事務分解為可以交錯運行的一系列子事務的集合。原本提出 SAGA 的目的,是為了避免大事務長時間鎖定數據庫的資源,后來才逐漸發展成將一個分布式環境中的大事務,分解為一系列本地事務的設計模式。
saga介紹相關文章:https://www.jianshu.com/p/e4b662407c66