前言
這篇文章將給大家介紹一下對分布式事務的一些見解,並講解分布式事務處理框架 TX-LCN 的執行原理,錯誤之處望各位不吝指正。

1. 什么情況下需要使用分布式事務?
使用的場景很多,先舉一個常見的:在微服務系統中,如果一個業務需要使用到不同的微服務,並且不同的微服務對應不同的數據庫。
打個比方:電商平台有一個客戶下訂單的業務邏輯,這個業務邏輯涉及到兩個微服務,一個是庫存服務(庫存減一),另一個是訂單服務(訂單數加一),示意圖如下:

如果在執行這個業務邏輯時沒有使用分布式事務,當庫存與訂單其中一個出現故障時,就很可能出現這樣的情況:庫存數據庫的值減少了 1,但是訂單數據庫沒有變化;或是庫存沒變化,多了一個訂單,也就是出現了數據不一致現象。
所以在類似的場合下我們要使用分布式事務,保證數據的一致性。
2. 分布式事務的解決思路
2.1引入:MySQL 中的兩階段提交策略
在談分布式事務的解決思路之前,我們先來看看單一數據源是如何做事務處理的,我們可以從中獲取一些啟發。
我們以 MySQL 的 InnoDB 引擎為例,由於 MySQL 中有兩套日志機制,一套是存儲層的 redo log,另一套是 server 層的 binlog,每次更新數據都要對兩個日志進行更新。為了防止寫日志時只寫了其中一個而沒有寫另外一個,MySQL 使用了一個叫兩階段提交的方式保證事務的一致性。具體是這樣的:
假設創建一個這樣的數據庫:mysql> create table T(ID int primary key, c int);, 然后執行一條這樣的更新語句:mysql> update T set c=c+1 where ID=2;
這條更新語句的執行流程是這樣子的:
-
首先執行器會找引擎取 ID=2 這一行數據
-
拿到數據后會把數據進行+1 操作,然后調用引擎接口把新數據寫入
-
引擎將數據更新到內存中,並將操作記錄到 redo log 里,此時 redo log 處於 prepare 狀態。但它不會提交事務,只是通知執行器已經完成任務,可以隨時提交。
-
執行器生成這個操作的 binlog,並把 binlog 寫入磁盤
-
最后執行器調用引擎的事務接口,把 redo log 改為提交狀態,更新完成。
在上述過程中,redo log 寫完后沒有直接提交,而是處於 prepare 狀態,等通知執行器並把 binlog 寫完后,redo log 再進行提交。這個過程就是兩階段提交,這是一個精妙的設計。
可能你會問為什么要有兩階段提交?如果不采用兩階段提交的話,也就是使用一階段提交,那就相當於按順序執行寫 redo log 和 binlog,如果寫完 redo log 后系統出現了故障,那么就會只有 redo log 記錄了操作,binlog 沒有記錄,造成數據不一致;使用兩階段提交的話,假設寫完 redo log 后系統出現了故障,由於事務還沒有提交,所以可以順利回滾。
兩階段提交的設計還有什么好處?首先要奠定一個概念:一個操作執行的時間越長,這個操作就越有可能失敗。打個比方,你吃飯要用 20 分鍾,上廁所要用 1 分鍾,在吃飯的過程中收到微信消息的概率肯定比去上廁所的過程中收到微信消息的概率大。由於在數據庫中更新操作的時間要遠大於提交事務的時間,所以先把更新操作做完,等所有耗時操作都做完最后再提交事務,能夠最大程度保證事務執行成功。
2.2分布式事務的兩階段提交策略
根據上述的兩階段提交策略,分布式事務也可以采取類似的辦法完成事務。
在第一階段,我們要新增一個事務管理者的角色,通過它來協調各個數據源。還是拿開頭的訂單案例講解,在執行下訂單的邏輯時,先讓各個數據庫去執行各自的事務,比如從庫存中減 1,在訂單庫中加 1,但是完成后不提交,只是通知事務管理者已經完成了任務。

到了第二階段,由於在階段一我們已經收到了各個數據源是否就緒的信息,只要有一個數據源沒有就緒,在第二階段就通知所有數據源回滾;如果全部數據源都已經就緒,就通知所有數據源提交事務。

總結一下這個兩階段提交的過程就是:首先事務管理器通知各個數據源進行操作,並返回是否准備好的信息。等所有數據源都准備好后,再統一發送事務提交(回滾)的通知讓各個數據源提交事務。由於最后的提交操作耗時極短,所以操作失敗的可能性會很低。
那么這個兩階段提交協議可能存在什么缺點呢?很可能存在被阻塞的問題,假如其中一個數據源出現了某些問題阻塞了,既不能返回成功信息,也不能返回失敗信息,那么整個事務將被阻塞。對應的策略是添加一些倒計時的操作,或者是重新發送消息。
3. 分布式事務框架 TX-LCN
講了這么多理論的知識,下面講解一款真正應用在生產中的分布式事務框架 TX-LCN 的運行原理。(典型的分布式事務框架不止 TX-LCN,比如還有阿里的 GTS,不過 GTS 是收費的,TX-LCN 是開源的)
我們先看一下官方文檔中給出的運行原理示意圖:

思路和我們上面講的兩階段分布式事務處理流程差不多(有小不同),核心步驟分為 3 步:
-
創建事務組:在事務發起方開始執行業務代碼之前先調用 TxManager 創建事務組對象,然后拿到事務表示 GroupId 的過程。簡單來說就是對這次下訂單的操作在事務管理中心里創建一個對象,拿到一個 id。
-
加入事務組:參與方在執行完業務方法后,將該模塊的事務信息通知給 TxManager 的操作。也就是指各個數據源(各個服務)完成操作后,和事務管理中心說一聲,注冊一下自己。
-
通知事務組:發起方執行業務代碼后,將發起方執行結果狀態通知給 TxManager,TxManager 將根據事務最終狀態和事務組的信息來通知相應的參與模塊提交或回滾事務,並返回結果給事務發起方。和客戶打交道的下訂單服務會收到減庫存和加訂單是否成功消息,它會把這兩個消息通知給事務管理者,事務管理者根據情況通知兩個庫存服務提交事務或回滾事務。
目前發現網上有一篇不錯的 TX-LCN 執行源碼分析文章: https://blog.csdn.net/cgj296645438/article/details/93860384
文章中跟着源碼走一遍會發現和上面的流程圖差不多,落實到代碼中有一些精彩的地方,比如:
public Object runTransaction(DTXInfo dtxInfo, BusinessCallback business) throws Throwable { if (Objects.isNull(DTXLocalContext.cur())) { DTXLocalContext.getOrNew(); } else { return business.call(); } log.debug("<---- TxLcn start ---->"); DTXLocalContext dtxLocalContext = DTXLocalContext.getOrNew(); TxContext txContext; // ---------- 保證每個模塊在一個DTX下只會有一個TxContext ---------- // if (globalContext.hasTxContext()) { // 有事務上下文的獲取父上下文 txContext = globalContext.txContext(); dtxLocalContext.setInGroup(true); log.debug("Unit[{}] used parent's TxContext[{}].", dtxInfo.getUnitId(), txContext.getGroupId()); } else { // 沒有的開啟本地事務上下文 txContext = globalContext.startTx(); } //...... }
這段代碼保證了每個模塊下只會有一個 TxContext,換個說法就是假設一個業務邏輯不是操作不同的數據源,而是對同一個數據源執行多次相同的操作,那么該數據源對應的模塊在 DTX 下會只有一個 TxContext

LCN 的事務協調機制
LCN 的口號是:LCN 並不生產事務,LCN 只是本地事務的協調工。大家肯定會有個疑問,它不生產事務,那么它是怎么控制各個模塊在完成事務的邏輯操作之后不馬上提交,而是等到 TxManager 最后一起通知各模塊提交的呢?
因為每個模塊都是一個 TxClient,每個 TxClient 下都有一個連接池,是框架自定義的連接池,對 Connection 使用靜態代理的方式進行包裝。
public class LcnConnectionProxy implements Connection { private Connection connection; public LcnConnectionProxy(Connection connection) { this.connection = connection; } /** * notify connection * * @param state transactionState * @return RpcResponseState RpcResponseState */ public RpcResponseState notify(int state) { try { if (state == 1) { log.debug("commit transaction type[lcn] proxy connection:{}.", this); connection.commit(); } else { log.debug("rollback transaction type[lcn] proxy connection:{}.", this); connection.rollback(); } connection.close(); log.debug("transaction type[lcn] proxy connection:{} closed.", this); return RpcResponseState.success; } catch (Exception e) { log.error(e.getLocalizedMessage(), e); return RpcResponseState.fail; } } @Override public void setAutoCommit(boolean autoCommit) throws SQLException { connection.setAutoCommit(false); } //...... }
連接池在沒有接收到通知事務之前會一直占有着這次分布式事務的連接資源。等到最后 TxManager 通知 TxClient 時,TxClient 才會去執行相應的提交或回滾。所以 LCN 的事務協調機制相當於是攔截了一下連接池,控制了連接的事務提交。

LCN 的事務補償機制
由於我們不能保證事務每次都正常執行,如果在執行某個業務方法時,本應該執行成功的操作卻因為服務器掛機或網絡抖動等問題導致事務沒有正常提交,這種場景就需要通過補償來完成事務。
在這種情況下 TxManager 會做一個標示;然后返回給發起方。告訴他本次事務有存在沒有通知到的情況,然后 TxClient 再次執行該次請求事務。
最后
歡迎大家關注我的公眾號【程序員追風】,文章都會在里面更新,整理的資料也會放在里面。