Seata
簡介
分布式事務
事務是數據庫的概念,數據庫事務(ACID:原子性、一致性、隔離性和持久性);
分布式事務的產生,是由於數據庫的拆分和分布式架構(微服務)帶來的,在常規情況下,我們在一個進程中操作一個數據庫,這屬於本地事務,如果在一個進程中操作多個數據庫,或者在多個進程中操作一個或多個數據庫,就產生了分布式事務;
(1)數據庫分庫分表就產生了分布式事務;

(2)項目拆分服務化也產生了分布式事務;

Seata
Seata是一款開源的分布式事務解決方案,致力於在微服務架構下提供高性能和簡單易用的分布式事務服務;
Seata為用戶提供了AT、TCC、SAGA和XA事務模式,為用戶打造一站式的分布式解決方案;
四種事務模式中,XA模式正在開發中...,其他事務模式已經實現;
目前使用的流行度情況是:AT > TCC > Saga;
我們可以參看seata各公司使用列表:
https://github.com/seata/seata/issues/1246 大部分公司都采用的AT事務模式;
Seata已經在國內很多團隊開始落地,其中不乏有大公司;
Github:https://github.com/seata/seata
在Seata的架構中,一共有三個角色:

TC (Transaction Coordinator) - 事務協調者
維護全局和分支事務的狀態,驅動全局事務提交或回滾;
TM (Transaction Manager) - 事務管理器
定義全局事務的范圍:開始全局事務、提交或回滾全局事務;
RM (Resource Manager) - 資源管理器
管理分支事務處理的資源,與TC交互以注冊分支事務和報告分支事務的狀態,並驅動分支事務提交或回滾;
其中TC為單獨部署的 Server 服務端,TM和RM為嵌入到應用中的 Client 客戶端;
在Seata中,一個分布式事務的生命周期如下:
TM請求TC開啟一個全局事務,TC會生成一個XID作為該全局事務的編號,XID會在微服務的調用鏈路中傳播,保證將多個微服務的子事務關聯在一起;
RM請求TC將本地事務注冊為全局事務的分支事務,通過全局事務的XID進行關聯;
TM請求TC告訴XID對應的全局事務是進行提交還是回滾;
TC驅動RM將XID對應的自己的本地事務進行提交還是回滾;
TC Server(Seata)運行環境部署
我們先部署單機環境的 Seata TC Server,用於學習或測試,在生產環境中要部署集群環境;
因為TC需要進行全局事務和分支事務的記錄,所以需要對應的存儲,目前,TC有三種存儲模式( store.mode ):
file模式:適合單機模式,全局事務會話信息在內存中讀寫,並持久化本地文件 root.data,性能較高;
db模式:適合集群模式,全局事務會話信息通過 db 共享,相對性能差點;
redis模式:解決db存儲的性能問題;
我們先采用file模式,最終我們部署單機TC Server如下圖所示:

下載Seata:http://seata.io/zh-cn/blog/download.html
解壓:tar -zxvf seata-server-1.3.0.tar.gz
切換cd seata
默認seata-server.sh腳本設置的jvm內存參數2G,我們再虛擬機里面做實驗,可以改小一點;
在bin目錄下啟動:./seata-server.sh
默認配置下,Seata TC Server 啟動在 8091 端口;
因為我們沒有修改任何配置文件,默認情況seata使用的是file模式進行數據持久化,所以可以看到用於持久化的本地文件 root.data;
AT模式事務案例-單體應用多數據源分布式事務
在Spring Boot單體項目中,如果使用了多數據源,就需要考慮多個數據源的數據一致性,即產生了分布式事務的問題,我們采用Seata的AT事務模式來解決該分布式事務問題;
以電商購物下單為例:
1、准備數據庫表和數據;
其中每個庫中的undo_log表,是 Seata AT模式必須創建的表,主要用於分支事務的回滾;
2、開發一個SpringBoot單體應用;
測試:http://localhost:8080/order?userId=1&productId=1
AT模式事務案例-微服務的分布式事務
tips:異常部分如果使用try-catch進行捕獲,則Seata不會檢測到異常也就不會進行事務回滾
AT事務模式分布式事務工作機制
前提
基於支持本地 ACID 事務的關系型數據庫;(mysql、oracle)
Java 應用,通過JDBC訪問數據庫;
整體機制
就是兩階段提交協議的演變:
一階段:
“業務數據“和“回滾日志記錄“在同一個本地事務中提交,釋放本地鎖和連接資源;
二階段:
如果沒有異常異步化提交,非常快速地完成;
如果有異常回滾通過一階段的回滾日志進行反向補償;
具體舉例說明整個AT分支的工作過程:
業務表:product
*Field* *Type* *Key*
id bigint(20) PRI
name varchar(100)
since varchar(100)
AT分支事務的業務邏輯:
update product set name = 'GTS' where name = 'TXC';
一階段過程:
1、解析SQL,得到SQL的類型(UPDATE),表(product),條件(where name = 'TXC')等相關的信息;
2、查詢前鏡像:根據解析得到的條件信息,生成查詢語句,定位數據;
select id, name, since from product where name = 'TXC';
得到前鏡像:
id name since
1 TXC 2014
3、執行業務 SQL:更新這條記錄的 name 為 'GTS';
4、查詢后鏡像:根據前鏡像的結果,通過 主鍵 定位數據;
select id, name, since from product where id = 1;
得到后鏡像:
id name since
1 GTS 2014
5、插入回滾日志:把前后鏡像數據以及業務SQL相關的信息組成一條回滾日志記錄,插入到 UNDO_LOG 表中;
6、分支事務提交前,向TC注冊分支,申請product表中,主鍵值等於1的記錄的全局鎖(在當前的同一個全局事務id范圍內是可以申請到全局鎖的,不同的全局事務id才會排斥);
7、本地事務提交:業務數據的更新和前面步驟中生成的 UNDO LOG 一並提交;
8、將本地事務提交的結果上報給TC;
二階段-回滾
1、收到 TC 的分支回滾請求,開啟一個本地事務,執行如下操作;
2、通過 XID 和 Branch ID 查找到相應的 UNDO LOG 記錄;
3、數據校驗:拿 UNDO LOG 中的后鏡像與當前數據進行比較,如果有不同,說明數據被當前全局事務之外的動作做了修改,這種情況,需要人工來處理;
4、根據 UNDO LOG 中的前鏡像和業務 SQL 的相關信息生成並執行回滾的語句:
update product set name = 'TXC' where id = 1;
5、提交本地事務,並把本地事務的執行結果(即分支事務回滾的結果)上報給 TC;
二階段-提交
1、收到TC的分支提交請求,把請求放入一個異步任務的隊列中,馬上返回提交成功的結果給TC;
2、異步任務階段的分支提交請求將異步和批量地刪除相應UNDO LOG記錄;
回滾日志表:
*Field* *Type*
branch_id bigint PK
xid varchar(100)
context varchar(128)
rollback_info longblob
log_status tinyint
log_created datetime
log_modified datetime
SQL建表語句:
CREATE TABLE `undo_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`branch_id` bigint NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
AT事務模式運行機制解讀
AT 模式的前提:
1、基於支持本地 ACID 事務的關系型數據庫;
2、Java 應用,通過 JDBC 訪問數據庫;
整體機制是兩階段提交協議的演變:
一階段:
業務數據和回滾日志記錄在同一個本地事務中提交,釋放本地鎖和連接資源;(本地事務,就已經在數據庫持久化了)
二階段:
如果沒有異常提交異步化,非常快速地完成;(正常情況,那就提交了,同步一下TC Server的狀態,刪除回滾日志)
如果有異常回滾通過一階段的回滾日志進行反向補償;(比如訂單刪除,庫存加回去,余額加回去);
AT事務模式數據隔離
寫隔離
一階段本地事務提交前,需要確保先拿到全局鎖;
拿不到 全局鎖 ,不能提交本地事務;
拿 全局鎖 的嘗試被限制在一定范圍內,超出范圍將放棄,並回滾本地事務,釋放本地鎖;
以一個示例來說明:
兩個或者多個全局事務 tx1 和 tx2,分別並發對 a 表的 m 字段進行更新操作,m 的初始值 1000;
假設tx1 先開始,開啟本地事務,拿到本地鎖,更新操作 m = 1000 - 100 = 900,本地事務提交前,先拿到該記錄的 全局鎖 ,拿到了全局鎖,本地提交並釋放本地鎖;
tx2后開始,開啟本地事務,拿到本地鎖,更新操作 m = 900 - 100 = 800,本地事務提交前,嘗試拿該記錄的 全局鎖 ,tx1全局提交前,該記錄的全局鎖一直會被 tx1 持有,tx2 需要重試等待 全局鎖 ;
tx1 二階段全局提交,釋放 全局鎖 ,tx2 拿到 全局鎖 提交本地事務;
如果 tx1 的二階段全局回滾,則 tx1 需要重新獲取該數據的本地鎖,進行反向補償的更新操作,實現分支的回滾;
此時,如果 tx2 仍在等待該數據的 全局鎖,同時持有本地鎖,則 tx1 的分支回滾會失敗。分支的回滾會一直重試,直到 tx2 的 全局鎖 等鎖超時,放棄 全局鎖 並回滾本地事務釋放本地鎖,tx1 的分支回滾最終成功;
因為整個過程 全局鎖 在 tx1 結束前一直是被 tx1 持有的,所以不會發生 臟寫 的問題;
讀隔離
在數據庫本地事務隔離級別 讀已提交(Read Committed) 或以上的基礎上,Seata(AT 模式)的默認全局隔離級別是讀未提交(Read Uncommitted)
如果應用在特定場景下,必需要求全局的 讀已提交 ,目前 Seata 的方式是通過 SELECT FOR UPDATE 語句的代理;
SELECT FOR UPDATE 語句的執行會申請 全局鎖 ,如果 全局鎖 被其他事務持有,則釋放本地鎖(回滾 SELECT FOR UPDATE 語句的本地執行)並重試,這個過程中,查詢是被 block 住的,直到 全局鎖 拿到,即讀取的相關數據是 已提交 的,才返回;
出於總體性能上的考慮,Seata目前的方案並沒有對所有SELECT語句都進行代理,僅針對 FOR UPDATE 的 SELECT 語句;
Seata TC Server集群部署
生產環境下,需要部署集群 Seata TC Server,實現高可用,在集群時,多個 Seata TC Server 通過 db 數據庫或者redis實現全局事務會話信息的共享;
每個Seata TC Server注冊自己到注冊中心上,應用從注冊中心獲得Seata TC Server實例,這就是Seata TC Server的集群;
Seata TC Server 對主流的注冊中心都提供了集成,Naco作為注冊中心越來越流行,這里我們就采用Nacos;
Seata TC Server集群搭建具體步驟:
1、下載並解壓兩個seata-server-1.3.0.tar.gz;
2、初始化 Seata TC Server 的 db 數據庫,在 MySQL 中,創建 seata 數據庫,並在該庫下執行如下SQL腳本:
使用seata-1.3.0\script\server\db腳本(網盤有共享)
3、修改 seata/conf/file.conf 配置文件,修改使用 db 數據庫,實現 Seata TC Server 的全局事務會話信息的共享;
(1)mode = "db"
(2)數據庫的連接信息
driverClassName = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://39.99.163.122:3306/seata"
user = "mysql"
password = "UoT1R8[09/VsfXoO5>6YteB"
4、設置使用 Nacos 注冊中心;
修改 seata/conf/registry.conf 配置文件,設置使用 Nacos 注冊中心;
(1)type = "nacos"
(2)Nacos連接信息:
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
5、啟動數據庫和nacos;
6、啟動兩個 TC Server
執行 ./seata-server.sh -p 18091 -n 1 命令,啟動第一個TC Server;
-p:Seata TC Server 監聽的端口;
-n:Server node,在多個 TC Server 時,需區分各自節點,用於生成不同區間的 transactionId 事務編號,以免沖突;
執行 ./seata-server.sh -p 28091 -n 2 命令,啟動第二個TC Server;
7、打開Nacos注冊中心控制台,可以看到有兩個Seata TC Server 實例;
8、應用測試;
對於SpringBoot單體應用:
1、添加nacos客戶端依賴;
<!-- nacos-client -->
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.3.1</version>
</dependency>
2、配置application.properties文件
#----------------------------------------------------------# Seata**應用編號,默認為**${spring.application.name}**
seata.application-id=springcloud-order-seata
# Seata**事務組編號,用於**TC**集群名**
seata.tx-service-group=springcloud-order-seata-group
#* *虛擬組和分組的映射**
seata.service.vgroup-mapping.springcloud-order-seata-group=default
#seata-spring-boot-starter 1.1**版本少一些配置項**
seata.enabled=true
seata.registry.type=nacos
seata.registry.nacos.cluster=default
seata.registry.nacos.server-addr=192.168.172.128:8848
seata.registry.nacos.group=SEATA_GROUP
seata.registry.nacos.application=seata-server
#----------------------------------------------------------
對於Spring Cloud Alibaba微服務應用:
則不需要加nacos的jar包依賴,application.properties文件配置完全一樣;
TCC事務模式執行機制
AT模式基本上能滿足我們使用分布式事務大部分需求,但涉及非關系型數據庫與中間件的操作、跨公司服務的調用、跨語言的應用調用就需要結合TCC模式;
一個分布式的全局事務,整體是兩階段提交(Try - [Comfirm/Cancel])的模型,在Seata中,AT模式與TCC模式事實上都是基於兩階段提交,它們的區別在於:
AT模式基於支持本地ACID事務的關系型數據庫:
1、一階段prepare行為:在本地事務中,一並提交“業務數據更新“和”相應回滾日志記錄”;
2、二階段 commit 行為:馬上成功結束,自動異步批量清理回滾日志;
3、二階段 rollback 行為:通過回滾日志,自動生成補償操作,完成數據回滾;
而TCC 模式,需要我們人為編寫代碼實現提交和回滾:
1、一階段 prepare 行為:調用自定義的 prepare 邏輯;(真正要做的事情,比如插入訂單,更新庫存,更新余額)
2、二階段 commit 行為:調用自定義的 commit 邏輯;(自己寫代碼實現)
3、二階段 rollback 行為:調用自定義的 rollback 邏輯;(自己寫代碼實現)
所以TCC模式,就是把自定義的分支事務的提交和回滾並納入到全局事務管理中;
通俗來說,Seata的TCC模式就是手工版本的AT模式,它允許你自定義兩階段的處理邏輯而不需要依賴AT模式的undo_log回滾表;
TCC事務模式應用實踐
基於SpringBoot單體應用的TCC事務

@LocalTCC
public interface AccountService {
*/****
\* *** *扣除余額**
\* *** *定義兩階段提交**
\* ** name = reduceStock**為一階段**try**方法**
\* ** commitMethod = commitTcc* *為二階段確認方法**
\* ** rollbackMethod = cancel* *為二階段取消方法**
\* ** BusinessActionContextParameter**注解 可傳遞參數到二階段方法**
\* ****
\* ** @param* *userId* *用戶**ID**
\* ** @param* *money* *扣減金額**
\* ** @throws* *Exception* *失敗時拋出異常**
\* **/**
@TwoPhaseBusinessAction(name = "reduceBalance", commitMethod = "commitTcc", rollbackMethod = "cancelTcc")
void reduceBalance(@BusinessActionContextParameter(paramName = "userId") Integer userId,
@BusinessActionContextParameter(paramName = "money") BigDecimal money);
*/****
\* *** *確認方法、可以另命名,但要保證與**commitMethod**一致**
\* ** context**可以傳遞**try**方法的參數**
\* ****
\* ** @param* *context* *上下文**
\* ** @return boolean**
\* **/**
boolean commitTcc(BusinessActionContext context);
*/****
\* *** *二階段取消方法**
\* ****
\* ** @param* *context* *上下文**
\* ** @return boolean**
\* **/**
boolean cancelTcc(BusinessActionContext context);
}
@LocalTCC注解標識此TCC為本地模式,即該事務是本地調用,非RPC調用,@LocalTCC一定需要注解在接口上,此接口可以是尋常的業務接口,只要實現了TCC的兩階段提交對應方法即可;
@TwoPhaseBusinessAction,該注解標識為TCC模式,注解try方法,其中name為當前tcc方法的bean名稱,寫方法名便可(全局唯一),commitMethod指提交方法,rollbackMethod指事務回滾方法,指定好三個方法之后,Seata會根據事務的成功或失敗,通過動態代理去幫我們自動調用提交或者回滾;
@BusinessActionContextParameter 注解可以將參數傳遞到二階段(commitMethod/rollbackMethod)的方法;
BusinessActionContext 是指TCC事務上下文,攜帶了業務方法的參數;
基於Spring Cloud Alibaba的TCC分布式事務
具體代碼實現和springboot單體應用的代碼實現幾乎沒有區別,具體參考Git上提交的代碼;
由於Seata出現時間並不長,也在不斷的改進中,在實際面試中應該不會問大家比較底層的實現,同學們如果感興趣的話,基於我們已有的源碼閱讀經驗,可以看一下Seata的源碼,它如何進行事務隔離保證數據一致性,官方提供的文檔並不詳細;