架構師必備的那些分布式事務解決方案!!


為了保證分布式環境下數據強一致性,需要引入分布式事務,而分布式事務由於網絡環境的不確定性,天生就很難實現。具體可以見上一篇。

分布式下,我想要強一致性

為了保證分布式事務的正確性,目前互聯網領域有幾種流行的解決方案,但是大部分都沒有像XA事務一樣形成標准的工業規范。但是這些方案在某些特定的行業或者業務場景下卻得到了越來越多的開發者的認可。

避免分布式事務

此方案提倡盡量避免分布式事務,不僅僅是因為分布式事務的難度,更是因為實現分布式事務需要更多的高級人才。如果一個操作設計到事務操作,而這些事務操作可以利用單機事務來解決,推薦首選單機事務。

當然,是否可以避免分布式事務還要看具體業務,在微服務盛行的當下,更多的還要看領域的划分標准,如果兩個微服務可以合並成一個微服務,一定程度上在領域划分標准接受范圍之內,可以考慮利用合並的方式來避免分布式服務。

舉一個很簡單的栗子:一個用戶基本信息服務和用戶資產服務(比如:用戶經驗值),當用戶修改資料的時候給用戶加貢獻值這個業務場景下,因為涉及到用戶資料修改和加貢獻值兩個不同服務的操作,這個時候就可以考慮將兩個服務合並為一個服務,用單機的數據庫事務來代替分布式事務。

在可以避免分布式事務的情況下,首選避免分布式事務

二階段提交

二階段(2PC)提交方案是基於X/OpenDTP標准規范的,最大的缺點在於它在第一階段需要鎖定資源,會大大降低系統的性能,大型的互聯網應用並不推薦這種方案,那種對性能不敏感的企業級應用可以嘗試使用。

在asp.net中,微軟已經提供了分布式事務的管理類型:TransactionScope,它依賴DTC(Distributed Transaction Coordinator)服務完成事務一致性。當它包裹的代碼中如果設計到多個不同物理位置的數據庫的時候,它會自動升級為分布式事務,使用起來非常方便。

using (TransactionScope ts = new TransactionScope())
            {
                數據庫A操作();
                數據庫B操作();
                數據庫C操作();
                ts.Complete();
            }

TCC

TCC本質上是一種編程模型,它提倡的是補償操作,所以一般情況下它會有重試機制,它約定參與事務的每個業務方都需要提供三個接口,具體情況請查看上一篇文章。由於TCC的接口重試特性,所以提供的提交和取消接口必須實現冪等性。

2PC主要是針對數據庫操作,而TCC主要是針對業務層面來進行操作,這在性能上比2PC要高很多,例如一個提交訂單的場景,商品服務需要扣除庫存,而訂單系統需要創建訂單,代碼類似以下,請不要糾結命名和參數:

//訂單服務
public interface IOrderService
{
     //創建一個不可見的訂單,返回訂單號
    Task<string> CreateOrder();
    //根據訂單號提交訂單,使訂單可見
    Task<int> SubmitOrder(string orderNo);
    //根據訂單號取消訂單
    Task<int> CancleOrder(string orderNo);
} 
//商品服務
public interface IProductService
{
    //根據商品id,鎖定庫存,返回鎖定的id
    Task<int> LockProductStock(int productId);
    //根據鎖定的庫存id,提交事務,扣除商品庫存
    Task<int> SubmitLockStock(int lockId);
    //根據鎖定的庫存id,取消事務,商品庫存回滾
    Task<int> CancleLockStock(int lockId);
}

其實TCC實現過程中,還有很多細節。比如:當提交事務階段,有一個節點由於網絡原因或者down機提交失敗,該怎么辦呢?這個時候我們要在本地引入本地消息機制,或者叫做業務活動管理器,把每個業務參與分布式事務的每個操作都記錄下來,當某個過程的某個節點操作失敗,無論是自動發起重試,還是手動重試都可以達到最終數據的一致性。

image

基於消息的事務

基於消息的分布式事務實現的是最終一致性,它是基於BASE理論的一個解決方案,最早由eBay提出並實施,它采用了消息隊列來輔助實現事務控制流程,核心思想是將需要分布式處理的任務通過MQ分發給每個業務去異步執行,如果任務失敗,則可以發起系統自動重試或者人工重試的糾正流程。

還是以上邊的創建訂單和扣減庫存為栗子:

  1. 首先調用訂單服務的創建訂單接口創建訂單,如果創建成功,則發送需要扣減庫存的消息(也可以看做創建訂單成功的消息)到MQ。
  2. 商品服務監聽扣減庫存消息隊列,如果收到扣減庫存消息,則執行扣減庫存操作,如果操作成功,則回復MQ刪除該消息。如果沒有操作成功,則准備接收同樣消息的下次投遞。

image

這個流程看似很完美,其實有很多漏洞。

  • 創建訂單是第一步操作,可以看做是單純的單機操作,這個並沒有問題,但是接着發送MQ消息這一步需要和創建訂單保證事務性,因為會發生創建訂單成功,發送mq消息失敗的情況。如果不能用技術手段來保證這兩步的事務,也可以采用引入本地消息的方案,在創建訂單的時候,用訂單數據庫來保證訂單創建成功和創建訂單消息表的一致性。然后發送mq成功之后,修改訂單消息表的狀態為發送成功,如果發送mq消息失敗,則啟用另外一個線程或者進程進行重試。
  • 商品服務扣減庫存類似,扣減庫存這個操作和回復mq消息這兩個操作也可以利用本地消息表的方式來解決一致性問題。當收到扣減庫存消息的時候,扣減庫存和添加消息成功處理記錄可以利用數據庫的事務來保證一致性,如果回復消息隊列ack失敗,就算是有重復消息,也可以根據本地的消費消息表來過濾重復消息

基於消息的分布式解決方案還有一個劣勢,如果一個事務的業務參與方非常多,消息的發送可能會非常復雜,需要非常謹慎的設計。比如以上訂單的栗子,現在引入了優惠券服務,在訂單創建成功,需要同時扣減庫存和優惠券,如果優惠券扣減失敗,需要同時回滾庫存和取消訂單,這也只是三個業務參與方,如果是四個,五個呢?當然這在業務中也許並不常見。

基於消息的分布式事務解決方案,由於引入了重試機制,也需要接口在實現的時候支持冪等性。但從開發的角度,這種方案要比tcc以及2pc都要有優勢,把每個系統之間的耦合度降到了最低,而且每個業務方的實現技術可以非常靈活,無論是采用java還是c#活着是golang都無所謂。

當然市面上基於消息的分布式解決方案各式各樣,但總體來說都屬於最終一致性方案。如果引入消息通道MQ的不穩定性,那還需要在各個業務方引入查詢機制來確保消息的ack機制。舉個栗子:如果商品服務已經正常扣減庫存,由於mq問題,始終不能正常ack。這個時候訂單服務是否會主動查詢商品服務是否已經正常扣庫存?這個時候整個架構可能就非現在這個樣子了,這個要是扯起來又是一篇文章了

點擊這里查看更多精彩內容


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM