最近作項目使用到了分佈式事務,下面這篇文章將給你們介紹一下對分佈式事務的一些看法,並講解分佈式事務處理框架 TX-LCN 的執行原理,初學入門,錯誤之處望各位不吝指正。
mysql
1. 什麼狀況下須要使用分佈式事務?
使用的場景不少,先舉一個常見的:在微服務系統中,若是一個業務須要使用到不一樣的微服務,而且不一樣的微服務對應不一樣的數據庫。web
打個比方:電商平臺有一個客戶下訂單的業務邏輯,這個業務邏輯涉及到兩個微服務,一個是庫存服務(庫存減一),另外一個是訂單服務(訂單數加一),示意圖以下:sql
若是在執行這個業務邏輯時沒有使用分佈式事務,當庫存與訂單其中一個出現故障時,就極可能出現這樣的狀況:庫存數據庫的值減小了 1,可是訂單數據庫沒有變化;或是庫存沒變化,多了一個訂單,也就是出現了數據不一致現象。
數據庫
因此在相似的場合下咱們要使用分佈式事務,保證數據的一致性。服務器
2. 分佈式事務的解決思路
引入: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;
app
這條更新語句的執行流程是這樣子的:框架
首先執行器會找引擎取 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 分鐘,在吃飯的過程當中收到微信消息的機率確定比去上廁所的過程當中收到微信消息的機率大。因爲在數據庫中更新操做的時間要遠大於提交事務的時間,因此先把更新操做作完,等全部耗時操做都作完最後再提交事務,可以最大程度保證事務執行成功。
分佈式事務的兩階段提交策略
根據上述的兩階段提交策略,分佈式事務也能夠採起相似的辦法完成事務。
在第一階段,咱們要新增一個事務管理者的角色,經過它來協調各個數據源。仍是拿開頭的訂單案例講解,在執行下訂單的邏輯時,先讓各個數據庫去執行各自的事務,好比從庫存中減 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 再次執行該次請求事務。
參考資料:極客時間丁奇 Mysql 實戰與尚學堂視頻配套資料
歡迎加入咱們的知識星球,一塊兒成長,交流經驗。加入方式,長按下方二維碼噢
最後,我想重複一句話:選擇和一羣優秀的人一塊兒成長,你成長的速度絕對會不同!
本文分享自微信公衆號 - Java極客技術(Javageektech)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。