為了保證分散式環境下資料強一致性,需要引入分散式事務,而分散式事務由於網路環境的不確定性,天生就很難實現。具體可以見上一篇。
為了保證分散式事務的正確性,目前網際網路領域有幾種流行的解決方案,但是大部分都沒有像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機提交失敗,該怎麼辦呢?這個時候我們要在本地引入本地訊息機制,或者叫做業務活動管理器,把每個業務參與分散式事務的每個操作都記錄下來,當某個過程的某個節點操作失敗,無論是自動發起重試,還是手動重試都可以達到最終資料的一致性。
基於訊息的事務
基於訊息的分散式事務實現的是最終一致性,它是基於BASE理論的一個解決方案,最早由eBay提出並實施,它採用了訊息佇列來輔助實現事務控制流程,核心思想是將需要分散式處理的任務通過MQ分發給每個業務去非同步執行,如果任務失敗,則可以發起系統自動重試或者人工重試的糾正流程。
還是以上邊的建立訂單和扣減庫存為栗子:
- 首先呼叫訂單服務的建立訂單介面建立訂單,如果建立成功,則傳送需要扣減庫存的訊息(也可以看做建立訂單成功的訊息)到MQ。
- 商品服務監聽扣減庫存訊息佇列,如果收到扣減庫存訊息,則執行扣減庫存操作,如果操作成功,則回覆MQ刪除該訊息。如果沒有操作成功,則準備接收同樣訊息的下次投遞。
這個流程看似很完美,其實有很多漏洞。
- 建立訂單是第一步操作,可以看做是單純的單機操作,這個並沒有問題,但是接著傳送MQ訊息這一步需要和建立訂單保證事務性,因為會發生建立訂單成功,傳送mq訊息失敗的情況。如果不能用技術手段來保證這兩步的事務,也可以採用引入本地訊息的方案,在建立訂單的時候,用訂單資料庫來保證訂單建立成功和建立訂單訊息表的一致性。然後傳送mq成功之後,修改訂單訊息表的狀態為傳送成功,如果傳送mq訊息失敗,則啟用另外一個執行緒或者程式進行重試。
- 商品服務扣減庫存類似,扣減庫存這個操作和回覆mq訊息這兩個操作也可以利用本地訊息表的方式來解決一致性問題。當收到扣減庫存訊息的時候,扣減庫存和新增訊息成功處理記錄可以利用資料庫的事務來保證一致性,如果回覆訊息佇列ack失敗,就算是有重複訊息,也可以根據本地的消費訊息表來過濾重複訊息
基於訊息的分散式解決方案還有一個劣勢,如果一個事務的業務參與方非常多,訊息的傳送可能會非常複雜,需要非常謹慎的設計。比如以上訂單的栗子,現在引入了優惠券服務,在訂單建立成功,需要同時扣減庫存和優惠券,如果優惠券扣減失敗,需要同時回滾庫存和取消訂單,這也只是三個業務參與方,如果是四個,五個呢?當然這在業務中也許並不常見。
基於訊息的分散式事務解決方案,由於引入了重試機制,也需要介面在實現的時候支援冪等性。但從開發的角度,這種方案要比tcc以及2pc都要有優勢,把每個系統之間的耦合度降到了最低,而且每個業務方的實現技術可以非常靈活,無論是採用java還是c#活著是golang都無所謂。
當然市面上基於訊息的分散式解決方案各式各樣,但總體來說都屬於最終一致性方案。如果引入訊息通道MQ的不穩定性,那還需要在各個業務方引入查詢機制來確保訊息的ack機制。舉個例子:如果商品服務已經正常扣減庫存,由於mq問題,始終不能正常ack。這個時候訂單服務是否會主動查詢商品服務是否已經正常扣庫存?這個時候整個架構可能就非現在這個樣子了,這個要是扯起來又是一篇文章了