前言
這篇文章将給大家介紹一下對分布式事務的一些見解,并講解分布式事務處理架構 TX-LCN 的執行原理,錯誤之處望各位不吝指正。
1. 什麼情況下需要使用分布式事務?
使用的場景很多,先舉一個常見的:在微服務系統中,如果一個業務需要使用到不同的微服務,并且不同的微服務對應不同的資料庫。
打個比方:電商平台有一個客戶下訂單的業務邏輯,這個業務邏輯涉及到兩個微服務,一個是庫存服務(庫存減一),另一個是訂單服務(訂單數加一),示意圖如下:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL2YWOhhDM5QWN3UTMkVTYmZmM2cTN4MWNlVDN3EjMyIGO1UDO4ATZh9CXt92Yu4GZjlGbh5SZslmZxl3Lc9CX6MHc0RHaiojIsJye.png)
如果在執行這個業務邏輯時沒有使用分布式事務,當庫存與訂單其中一個出現故障時,就很可能出現這樣的情況:庫存資料庫的值減少了 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 再次執行該次請求事務。
最後
歡迎大家一起交流,喜歡文章記得關注我點贊轉發喲,感謝支援!