事務是指將全部涉及到的操做放到一個不可分割的執行單元內. 一個事務內的全部操做, 要麼所有都執行, 要麼所有都不執行. 這就是事務的通俗理解.html
通常來講, 事務都是針對數據庫而言, 可是其實並非,一些消息隊列例如RocketMq, kafka等也會涉及到事務. 這些組件有個專門的術語, 叫資源管理器(Resource Manager, 即RM)java
分佈式事務,是隨着分佈式系統應用愈來愈普遍的過程當中衍生出來的一個新概念,通常是指RM在不一樣的節點上.在微服務大行其道的今天, 分佈式事務愈來愈值得被重視.mysql
本文是想介紹分佈式事務的, 可是要說分佈式事務, 本地事務又是一個繞不開的話題. 因此咱們這裏迅速過一下本地事務的相關概念.算法
ACID是事務必須具有的四個特性.其中分別是:spring
1. A是指原子性. 是指一個操做必須是一個不可分割的單元, 要麼執行要麼不執行, 不能存在指執行了一半,另一半沒執行的狀態
2. C是指一致性. 是指事務執行先後,系統都處在一個一致性的狀態.
3. I是指隔離性. 隔離性是指不一樣事務之間應該互相隔離,不受影響
4. D是持久性, 表示事務的執行應該是永久性的, 不能由於系統重啓或奔潰就丟失
複製代碼
通常地,ACID中的I又會引出另一組概念: 可見性問題和隔離等級.sql
可見性問題是指因爲一個事務內的操做在另一個事務中的可見性而帶來的問題. 通常來講,可見性越高, 帶來問題的可能性就越大.數據庫
可見性從高到低, 問題有如下幾種:編程
1. 讀未提交. 一個事務能讀到另一個事務未提交的更改.這是最嚴重的問題的,未提交的數據都是髒數據.
2. 不可重複讀. 所謂的不可重複讀是指一個事務第一次讀某條記錄
和第二次讀同一條記錄時會讀到不同的內容.
緣由是該事務在這兩次讀之間, 有另一個事務更新了這條記錄,而且提交了.
3. 幻讀. 所謂幻讀是指一個事務在兩次讀同一份數
據時, 第一次和第二次讀到的數量不同. 緣由是該事務在這兩次讀之間,
有另一個事務新增/刪除了記錄, 並提交了.
能夠看到, 其實不可重複讀和幻讀都是因爲另一個事務更改了數據形成的.
二者的差異是另一個事務的操做是update仍是insert/delete
複製代碼
隔離等級和可見性問題是遙相呼應的, 每一個隔離等級的存在都是爲了解決掉可見性問題api
隔離等級有如下幾種:bash
1. 讀未提交. 這是最低級別的隔離等級, 很明顯,什麼可見性問題都沒解決.
2. 讀已提交. 解決了"讀未提交"的問題
3. 可重複讀. 解決"不可重複讀"的問題
4. 串行化, 解決了"幻讀"的問題.
複製代碼
固然, 越高的隔離等級意味着越低的處理數據的吞吐量.
在mysql中, 能夠用 begin, commit, rollback三個指令來實現事務.
1. begin用來開始一個事務.
2. commit 用來提交一個事務
3. rollback用來回滾一個事務
複製代碼
mysql的事務是默認自動提交的.可使用
set autocommit = 0
或
set autocommit = 1
複製代碼
來關閉/開啓自動提交.
值得一提的是, mysql默認的隔離等級是"可重複讀", 可是高版本的innodb(mysl5.7)實際上是經過間隙鎖達到了"串行化"的標準了的.
spring是支持事務操做的話, 可是spring中的事務操做其實都只是個代理, 最終都是依賴數據庫的begin, commit, rollback實現的.
編程式事務是指經過transactionTemplate和TransactionManager來手動控制commit和rollback的事務.
編程式事務相對於聲明式事務而言, 靈活度更高, 例如能夠針對某個代碼段提交或回滾.
聲明式事務通俗來講就是註解事務, 經過把spring的@Transactional註解添加到方法或類上來聲明一個事務, 所以得名"聲明式事務".
@Transactional經常使用的參數有:
1. propagation, 指定事務的傳播等級
2. isolation, 指定隔離等級
3. norollback for, 指定不回滾事務的異常
4. rollback for, 指定須要回滾事務的異常
5. timeout, 指定事務的超時時間
複製代碼
聲明式事務的原理是動態代理和AOP, 簡單來講就是在具體的方法執行先後加上begin, commit, rollback的邏輯.
聲明式事務最大的好處就是簡單, 對代碼侵入性低.對應的缺點就是粒度很差控制, 最小的粒度也是要加到方法上.
事務的傳播通俗地來說, 就是多個方法的調用鏈中, 若是涉及到事務的嵌套, spring應該如何處理.
這個概念也是由聲明式事務的原理而引伸出來的. 聲明式事務的原理就是由動態代理在方法的先後加上開啓事務和提交事務的邏輯.
假設存在如下的一種場景:
@Transational
public void A(){
B();
// do something
}
@Transational
public void B(){
//dosomething
int a = 1/0
}
複製代碼
上面兩個方法都聲明開啓事務, 很明顯B是會拋出異常的, B的事務會被回滾. 那麼A會不會也被回滾呢. 這就須要用事務的傳播機制來解決了.
spring的事務傳播機制一共有7種:
1. propagation_require. 默認的傳播類型. 表示當前方法須要再一個事務中執行, 若是沒有開啓事務, 則須要開啓一個
2. propagation_support. 表示當前方法不須要事務, 可是若是事務存在,則在事務中執行
3. propagation_mandatory. 表示當前方法必需要在事務中執行, 若是不存在,則拋出異常.
4. propagation_requireNew. 表示當前方法須要在新的事務中執行.當前方法執行時, 若是已經存在一個事務, 則先掛起該事務
5. propagation_not_support. 表示當前方法不支持事務, 若是已經存在事務, 那就先掛起該事務.
6. propagation_never. 表示當前方法不該該在事務中執行, 若是存在事務, 則拋異常.
7. propagation_nested. 若是存在嵌套的事務, 那麼各個方法在各自獨立的方法裏面提交和回滾.
複製代碼
DTP(Distributed Transaction Processing)是x/open組織提出來的分佈式事務的模型.
一個DTP模型至少包含如下三個元素:
1. AP, 應用程序,用於定義事務開始和結束的邊界. 說人話就是咱們開啓事務的代碼因此的應用.
2. RM, 資源管理器. 理論上一切支持持久化的數據庫資源均可以是一個資源管理器.
3. TM, 事務管理器, 負責對事務進行協調,監控. 並負責事務的提交和回滾.
複製代碼
XA是x/open提出來的分佈式事務的規範, 它是跟語言無關的.
XA規範定義了RM和TM交互的接口. 例如TM能夠經過如下接口對RM進行管理:
1. xa_open和xa_close, 用於跟RM創建鏈接
2. xa_star和xa_end, 開始和結束一個事務
3. xa_prepare, xa_commit和xa_rollback, 用於預提交, 提交和回滾一個事務
3. xa_recover 用於回滾一個預提交的事務
複製代碼
JTA規範是能夠認爲是XA規範java語言實現版的規範.
JTA定義了一系列分佈式事務相關的接口:
1. javax.transaction.Status: 定義了事務的狀態,例如prepare, commit rollback等等等
2. javax.transaction.Synchronization:同步
3. javax.transaction.Transaction:事務
4. javax.transaction.TransactionManager:事務管理器
5. javax.transaction.UserTransaction:用於聲明一個分佈式事務
6. javax.transaction.TransactionSynchronizationRegistry:事務同步註冊
7. javax.transaction.xa.XAResource:定義RM提供給TM操做的接口
8. javax.transaction.xa.Xid:事務id
複製代碼
以上不一樣的接口由不一樣的角色(RM, RM等)來實現.
二階段提交是最簡單的分佈式事務解決方案. 它把一個事務分紅request commit和commit/rollback兩個階段組成.
第一階段是請求階段, 由協調者向因此的RM詢問事務是否能夠提交. 若是能夠提交則回覆YES,不然回覆NO.
第二階段是提交階段, 協調者根據全部的RM的響應來決定該分佈式事務是否能夠提交. 若是全部的RM都回復了YES, 則能夠提交,不然回滾該事務.
二階段提交思想雖然簡單, 可是它存在很是多的問題.
三階段提交是爲了解決二階段算法存在的問題而提出來的.它把事務的提交分紅3個階段:
1. cancommit階段, 和2PC中的請求階段相似
2. precommit階段. 若是cancommit階段不是所有響應YES或者有RM超時, 那麼回滾整個事務.
不然, 發送precommit指令, 讓各個RM執行事務操做,執行完後響應ACK.
3. docommit階段.若是precommit階段由RM沒有響應ACK或者超時, 那麼回滾整個事務.
不然發送docommit指令, 讓各個RM真正提交事務.
複製代碼
TCC是指try-comfirm-cancel.是這些年來大火的一種柔性分佈式事務解決方案.
所謂"柔性", 是針對2PC和3PC等"剛性事務"而言的. 柔性事務再也不一味追求強一致性, 只要求最終一致性.
TCC把一個分佈式事務拆成如下三個步驟:
1. try階段. 各個事務參與者檢查業務一致性, 預留系統資源.例如鎖定庫存
2. comfirm階段. 事務參與者使用try階段預留的資源,執行業務操做.
3. cancel階段. 若是try階段任意一個事務參與者try失敗, 則作cancel操做. cancel包括釋放資源和反向補償
複製代碼
其實仔細一看, T-C-C是恰好跟2PC中的request-commit-rollback一一一對應的.從這點上看, TCC本質上也是一個2PC思想的解決方案.
在TCC中還有兩個概念, 主業務服務和從業務服務.
主業務服務能夠通俗地理解成發起事務的那個服務.例如一個購買的服務, 它分別調用庫存服務和訂單服務. 那麼購買服務就能夠看作是主業務服務.
對應地, 上面所說的"庫存服務"和"訂單服務"就是從業務服務.
爲何要先區分這兩種服務呢? 由於它們的職責是不同的:
1. 從業務服務必需要提供try, comfirm, cancel方法.
2. 主業務服務須要記錄事務日誌, 並在事務管理器的協調下, 適當地調用從業務服務的tcc三個方法.
複製代碼
TCC的模型以下圖所示:
圖片來自www.tianshouzhi.com/api/tutoria…, 同時也極力推薦這個博客, 受益不淺.
利用消息隊列來實現最終一致性是另一種柔性分佈式事務的思想. 它的主要思想經過消息隊列異步完成一個分佈式事務, 結合定時任務作重試和補償, 必要的時候須要人工介入.
總結地來講, 一共有"盡最大努力通知", "本地消息表"和"MQ事務消息"三種思想.
盡最大努力通知就是主動通知方會盡最大努力把處理結果通知到接收方, 若是通知失敗,會作最多X次重試.若是最終仍是失敗, 主動方提供了查詢的接口, 能夠由接收方主動查詢.
這種思想是最簡單的, 其實應用的也是比較多的.典型的有:
1. 運營商短信發送狀態回傳
2. 微信和支付寶支付狀態回傳
複製代碼
顧名思義, 本地消息表就是利用一個本地數據庫維護事務完成的中間狀態. 在分佈式事務執行的過程當中,各方事務參與者完成操做後更新消息表的狀態,逐步完成一個總體的事務.
對於異常的狀況, 由定時調度定時檢測消息表中未完成的事務, 發起重試. 定時調度的解決方案見java中執行定時任務的6種姿式
若是最終仍是有一方未能完成事務操做,則由人工介入進行補償.
若有上面一張圖:
1. 生產者先寫本地消息表和業務數據, 用本地事務保證成功.再發送MQ消息.
2. 消費者消費數據,一樣是執行本地事務. 成功後更新本地消息表的狀態. 失敗怎麼辦呢? 能夠發送消息給生產者進行回滾, 可是那樣複雜度
就高了(要求生產者也要實現TCC, 那就還不如用TCC了). 因此更現實的方案是重現+人工補償
3. 生產者可能會寫業務數據成功, 可是發送MQ消息失敗, 這個時候本地消息表仍是會有對應未完成的事務, 那麼定時任務會掃描出來, 重試.最終仍是能完成整個分佈事務.
複製代碼
固然, 上圖也不是百分百完善的 可是本地消息表更多的只是一種思想, 具體實現可能會有所不一樣,也要結合具體的業務場景和業務要求來實現.
本地消息表的方案就在MQ廣泛都尚未實現事務消息的時候提出的. 可是如今無論是kafka仍是rocketMQ都開始支持事務消息了.
有了事務消息, 其實本地表和定時任務的工做就由MQ的事務機制來完成了.
例如www.tianshouzhi.com/api/tutoria… 裏面介紹的方案.
在實際的應用中, 分佈式事務出現的場景能夠總結爲兩種. 仍是以一個購買服務爲例, 那麼這兩種分佈式事務的場景多是:
在微服務化大行其道的今天,按業務分庫應該是大多公司搭建架構的一個基本準則了. 因此這樣來看, 貌似是第二種場景更符合實際了.
固然第一種場景確定也仍是有存在的. 例如上面"本地消息表"的解決方案中, 就有須要再同一個服務中跟多個RM交互.
分佈式事務開源框架其實市面上也挺多的,例如tcc-transactio等等, 這裏咱們來看看atomikos和seata這兩個
atomikos是一個很是有名的分佈式事務開源框架. 它有JTA/XA規範的實現, 也有TCC機制的實現方案, 前者是免費開源的, 後者是商業付費版的.
這裏介紹一下JTA/XA規範的實現.
上面JTA規範那一小節說到JTA定義了一系列的接口,那些接口是由不一樣的角色去實現的. atomikos的角色是一個事務管理器, 它實現的接口主要有:
1. javax.transaction.UserTransaction
對應的實現是com.atomikos.icatch.jta.UserTransactionImp,用戶只須要直接操做這個類就是實現一個JTA分佈式事務
2. javax.transaction.TransactionManager
對應的實現是com.atomikos.icatch.jta.UserTransactionManager, atomikos使用這個實現類來對事務進行管理
3. javax.transaction.Transaction
對應的實現是com.atomikos.icatch.jta.TransactionImp
複製代碼
應用atomikos的簡單實例(仍是來自www.tianshouzhi.com/api/tutoria…):
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions-jdbc</artifactId>
<version>4.0.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
複製代碼
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;
public class AtomikosExample {
private static AtomikosDataSourceBean createAtomikosDataSourceBean(String dbName) {
// 鏈接池基本屬性
Properties p = new Properties();
p.setProperty("url", "jdbc:mysql://localhost:3306/" + dbName);
p.setProperty("user", "root");
p.setProperty("password", "your password");
// 使用AtomikosDataSourceBean封裝com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
//atomikos要求爲每一個AtomikosDataSourceBean名稱,爲了方便記憶,這裏設置爲和dbName相同
ds.setUniqueResourceName(dbName);
ds.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
ds.setXaProperties(p);
return ds;
}
public static void main(String[] args) {
AtomikosDataSourceBean ds1 = createAtomikosDataSourceBean("db_user");
AtomikosDataSourceBean ds2 = createAtomikosDataSourceBean("db_account");
Connection conn1 = null;
Connection conn2 = null;
PreparedStatement ps1 = null;
PreparedStatement ps2 = null;
UserTransaction userTransaction = new UserTransactionImp();
try {
// 開啓事務
userTransaction.begin();
// 執行db1上的sql
conn1 = ds1.getConnection();
ps1 = conn1.prepareStatement("INSERT into user(name) VALUES (?)", Statement.RETURN_GENERATED_KEYS);
ps1.setString(1, "tianshouzhi");
ps1.executeUpdate();
ResultSet generatedKeys = ps1.getGeneratedKeys();
int userId = -1;
while (generatedKeys.next()) {
userId = generatedKeys.getInt(1);// 得到自動生成的userId
}
// 模擬異常 ,直接進入catch代碼塊,2個都不會提交
// int i=1/0;
// 執行db2上的sql
conn2 = ds2.getConnection();
ps2 = conn2.prepareStatement("INSERT into account(user_id,money) VALUES (?,?)");
ps2.setInt(1, userId);
ps2.setDouble(2, 10000000);
ps2.executeUpdate();
// 兩階段提交
userTransaction.commit();
} catch (Exception e) {
try {
e.printStackTrace();
userTransaction.rollback();
} catch (SystemException e1) {
e1.printStackTrace();
}
} finally {
try {
ps1.close();
ps2.close();
conn1.close();
conn2.close();
ds1.close();
ds2.close();
} catch (Exception ignore) {
}
}
}
}
複製代碼
很明顯, 這個例子是屬於場景1的分佈式事務. 因此若是有場景1的分佈式事務的話, 直接使用atomikos就能夠了, 簡單直接高效.
可是話又說回來了, 實際場景的分佈式事務更多的仍是屬於場景2的. 很明顯簡單的JTA事務是處理不了場景2的分佈式事務的.場景2下的分佈式事務, 還得須要像TCC或消息隊列柔性事務等解決方案去實現.
seata就是Fescar(TXC/GTC/FESCAR)和tcc-transaction整合後開源的一個分佈式事務落地解決方案框架,實現了AT, TCC, SAGA三種模式, 大有一統江湖的意思.
官網地址是seata.io/zh-cn/docs/…, 文檔方面相對來講還不夠完善, 可是做爲了解仍是足夠了. 這裏也是簡單地介紹一下.
TC - 事務協調者 維護全局和分支事務的狀態,驅動全局事務提交或回滾。
TM - 事務管理器 定義全局事務的範圍:開始全局事務、提交或回滾全局事務。
RM - 資源管理器 管理分支事務處理的資源,與TC交談以註冊分支事務和報告分支事務的狀態,並驅動分支事務提交或回滾。
AT 即Automatic Transaction, 所謂AUTO, 表示是這種模式是對業務無侵入的, 不須要業務改造.可是對業務有要求:
1. 基於支持本地 ACID 事務的關係型數據庫。
2. Java 應用,經過 JDBC 訪問數據庫。
複製代碼
AT模式整體邏輯以下圖:
AT模式採用的也是2PC的思想, 加入了補償的機制, 補償的機制跟innodb裏面的undo日誌相似.
undo日誌其實就是一個反向補償, 例如insert的語句, 事務回滾時,會執行一個對應的delete語句
用大白話翻譯了一下模式(個人理解)就是:
1. 第1階段, 先生成undo日誌, undo日誌和業務的操做在本地事務中一併提交
2. 第2階段, 在TC的協調下, 若是能夠提交則迅速提交. 須要回滾時根據回滾日誌作反向補償.
複製代碼
固然具體應用沒有那麼簡單, 更多的參考官網
TCC模式就是上面介紹的TCC的思想, SEATA的tcc模式以下圖:
TCC模式其實跟AT模式也是相似的, 也是一個2PC的演化版, 在事務協調器(TC)的協調下, 進行多個子事務的提交和回滾.
不一樣的是, AT模式回滾是在數據庫資源層面的補償(執行回滾日誌), 而TCC是調用自定義的邏輯進行回滾(執行回滾代碼邏輯).
saga是一種長事務解決方案. 在Saga模式中,業務流程中每一個參與者都提交本地事務,當出現某一個參與者失敗則補償前面已經成功的參與者,一階段正向服務和二階段補償服務都由業務開發實現
saga的思想雖然在1987年提出來了, 可是seata的saga模式是今年的8月份才正式支持的.我對它的理解也不夠深刻,因此也不在班門弄斧了.瞭解一下便可
分佈式系統歷來就不是一個簡單的概念, 分佈式系統中的分佈式事務更是如此.
也許分佈式事務的思想算是比較簡單, 可是實現起來的確有不少的細節和困難須要咱們去注意和克服.於是大多數據公司企業都會有根據本身的業務實際去作不一樣的實踐, 而不是完徹底全地照搬思想.
這一點體現出來的另一面就是, 如今市面上確實也沒有一個完善的分佈式解決方案, 能讓咱們照搬就能夠了.阿里的seata開源也不久, 但願有一天, 它真的能一統江湖, 真正的能夠一次性一站式地解決分佈式事務的問題