目錄
- 第一章 Seata的介紹
- 1.1、分布式事務
- 1.2、Seata是什么
- 1.3、Seata的術語
- 第二章 Seata單機版部署:TC Server
- 2.1、下載Seata
- 2.2、解壓Seata
- 2.3、運行Seata
- 第三章 Seata的常用模式:AT
- 3.1、使用前提
- 3.2、整體機制
- 3.3、讀寫隔離
- 3.3.1、寫隔離
- 3.3.2、讀隔離
- 3.4、作業機制
- 3.4.1、一階段
- 3.4.2、二階段-回滾
- 3.4.3、二階段-提交
- 3.5、附錄章節
- 第四章 單體版多資料源事務管理:AT
- 4.1、匯入資料
- 4.1.1、創建賬戶資料庫
- 4.1.2、創建庫存資料庫
- 4.1.3、創建訂單資料庫
- 4.2、匯入工程
- 4.3、正常測驗
- 4.4、例外測驗
- 4.5、添加新表
- 4.6、添加依賴
- 4.7、添加配置
- 4.8、添加注解
- 4.9、例外測驗
- 第五章 分布式單資料源事務管理:AT
- 5.1、匯入資料
- 5.1.1、重置賬戶資料庫
- 5.1.2、重置庫存資料庫
- 5.1.3、重置訂單資料庫
- 5.2、匯入工程
- 5.3、正常測驗
- 5.4、例外測驗
- 5.5、添加新表
- 5.6、添加依賴
- 5.7、添加配置
- 5.8、添加注解
- 5.9、例外測驗
- 第六章 Seata集群版部署:TC Cluster
- 6.1、關閉單機版
- 6.2、創建資料庫
- 6.3、修改存盤模式
- 6.4、修改注冊中心
- 6.5、啟動兩個實體
- 6.6、查看注冊中心
- 第七章 Seata的常用模式:TCC
- 第八章 單體版多資料源事務管理:TCC
- 8.1、匯入資料
- 8.1.1、重置賬戶資料庫
- 8.1.2、重置庫存資料庫
- 8.1.3、重置訂單資料庫
- 8.2、匯入工程
- 8.3、正常測驗
- 8.4、例外測驗
- 8.5、添加新表
- 8.6、添加依賴
- 8.7、添加配置
- 8.8、添加注解
- 8.9、賬戶服務
- 8.10、例外測驗
- 第九章 分布式單資料源事務管理:TCC
- 9.1、匯入資料
- 9.1.1、重置賬戶資料庫
- 9.1.2、重置庫存資料庫
- 9.1.3、重置訂單資料庫
- 9.2、匯入工程
- 9.3、正常測驗
- 9.4、例外測驗
- 9.5、添加新表
- 9.6、添加依賴
- 9.7、添加配置
- 9.8、添加注解
- 9.9、賬戶服務
- 9.10、例外測驗
配套資料,免費下載
鏈接:https://pan.baidu.com/s/1-eRFozbFIShqbqNRFD9KDw
提取碼:rt3w
復制這段內容后打開百度網盤手機App,操作更方便哦
第一章 Seata的介紹
1.1、分布式事務
事務是資料庫的概念,資料庫事務(ACID:原子性、一致性、隔離性和持久性),
分布式事務的產生,是由于資料庫的拆分和分布式架構(微服務)帶來的,在常規情況下,我們在一個行程中操作一個資料庫,這屬于本地事務,如果在一個行程中操作多個資料庫,或者在多個行程中操作一個或多個資料庫,就產生了分布式事務;
(1)資料庫分庫分表就產生了分布式事務;

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

1.2、Seata是什么
Seata是一款開源的分布式事務解決方案,致力于在微服務架構下提供高性能和簡單易用的分布式事務服務,
Seata為用戶提供了AT、TCC、SAGA和XA事務模式,為用戶打造一站式的分布式解決方案,
四種事務模式中,目前使用的流行度情況是:AT > TCC > Saga、XA,我們可以參看Seata各公司使用串列:https://github.com/seata/seata/issues/1246
因此,我們教學的重點和學習的重點將會放到AT模式和TCC模式的講解上,Seata默認就是AT模式,簡單的一句話來說這兩種模式的區別(后邊會深入講解):
- AT模式:可以對資料源是mysql、oracle等關系型資料庫的情況進行失敗回滾,
- TCC模式:不僅可以對資料源是mysql、oracle等關系型資料庫的情況進行失敗回滾,還可以對訊息中間件、非關系型資料庫如:redis、mongodb等資料庫進行失敗回滾,
從上邊直觀的來看,感覺TCC模式更厲害一點,實際上,Seata默認的AT模式,事務失敗回滾并不用程式員自己來做,而是由Seata框架本身來完成的,而TCC模式的事務失敗回滾等操作,全部需要手動實作,因此,AT模式在實際生產環境中用的更多一點,也更方便一點,除了特定場景下的特殊需要,AT模式基本都能滿足,
當然了,這里提前說一下,我們接下來會學習Seata的單機版部署和高可用集群版部署,而正好,我們要學習AT和TCC兩種模式,我們在學習AT模式的時候,使用Seata單機版環境、而在學習TCC模式的時候,使用Seata的高可用集群版的環境,一定注意,這么安排存粹是為了教學方便,實際上AT也能用高可用集群版環境,
1.3、Seata的術語
官方檔案:http://seata.io/zh-cn/
在Seata的架構中,一共有三個角色:

TC (Transaction Coordinator) - 事務協調者
維護全域和分支事務的狀態,驅動全域事務提交或回滾,
TM (Transaction Manager) - 事務管理器
定義全域事務的范圍:開始全域事務、提交或回滾全域事務,
RM (Resource Manager) - 資源管理器
管理分支事務處理的資源,與TC交談以注冊分支事務和報告分支事務的狀態,并驅動分支事務提交或回滾,
其中TC為單獨部署的 Server 服務端,TM和RM為嵌入到應用中的 Client 客戶端,除了以上三種角色外,還有一個全域事務id:Transaction ID XID ,
在Seata中,一個分布式事務的生命周期如下:

-
TM 向 TC 申請開啟一個全域事務,全域事務創建成功并生成一個全域唯一的 XID;
-
XID 在微服務呼叫鏈路的背景關系中傳播;
-
RM 向 TC 注冊分支事務,將其納入 XID 對應全域事務的管轄;
-
TM 向 TC 發起針對 XID 的全域提交或回滾決議;
-
TC 調度 XID 下管轄的全部分支事務完成提交或回滾請求,
第二章 Seata單機版部署:TC Server
2.1、下載Seata
我們先部署單機環境的 Seata TC Server,用于學習或測驗,在生產環境中要部署集群環境;
因為TC需要進行全域事務和分支事務的記錄,所以需要對應的存盤,目前,TC有三種存盤模式( store.mode ):
- file模式:適合單機模式,全域事務會話資訊在記憶體中讀寫,并持久化本地檔案 root.data,性能較高;( 默認 )
- db模式:適合集群模式,全域事務會話資訊通過 db 共享,相對性能差點;
- redis模式:解決db存盤的性能問題;
我們先采用file模式,最終我們部署單機TC Server如下圖所示:

截止到2021年2月24日,官方最新發布的版本為1.4.1,但是我們不能下載最新的這個版本來進行學習,因為在引入spring-cloud-starter-alibaba-seata依賴以后,內置自帶的版本為1.3.0,因此,我們需要保持版本一致,所以,我們需要下載1.3.0,
下載地址:https://github.com/seata/seata/releases/download/v1.3.0/seata-server-1.3.0.zip
2.2、解壓Seata

2.3、運行Seata
雙擊運行:bin\seata-server.bat
第三章 Seata的常用模式:AT
3.1、使用前提
- 基于支持本地 ACID 事務的關系型資料庫,例如:mysql、oracle
- Java 應用,通過 JDBC 訪問資料庫,
3.2、整體機制
兩階段提交協議的演變:
- 一階段:業務資料和回滾日志記錄(一張單獨的資料表)在同一個本地事務中提交,釋放本地鎖和連接資源,
- 二階段:
- 提交異步化,非常快速地完成,
- 回滾通過一階段的回滾日志進行反向補償,
反向補償:簡單說就是給某一個欄位加了10,反向補償就減去10,這樣資料保持不變,新增一條記錄,反向補償就洗掉以前新增的那條記錄,
3.3、讀寫隔離
3.3.1、寫隔離
- 一階段本地事務提交前,需要確保先拿到 全域鎖 ,
- 拿不到 全域鎖 ,不能提交本地事務,
- 拿 全域鎖 的嘗試被限制在一定范圍內,超出范圍將放棄,并回滾本地事務,釋放本地鎖,
以一個示例來說明:
兩個全域事務 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 持有的,所以不會發生 臟寫 的問題,
3.3.2、讀隔離
如果不考慮事務的隔離性,可能會引發讀安全性問題:
- 臟讀:一個事務讀到了另一個事務未提交的資料
- 不可重復讀:一個事務讀到了另一個事務已經提交的 update 的資料,導致多次查詢結果不一致
- 幻讀 / 虛讀:一個事務讀到了另一個事務已經提交的 insert 的資料,導致多次查詢結果不一致
| 隔離級別 | 中文說明 | 說明 |
|---|---|---|
| READ UNCOMMITTED | 讀未提交 | 不能解決以上所有讀問題,效率最高,安全性最低,一般不用 |
| READ COMMITTED | 讀已提交 | 避免臟讀,不可重復讀和幻讀有可能發生,Oracle默認的隔離級別 |
| REPEATABLE READ | 可重復讀 | 避免臟讀、不可重復讀,幻讀有可能發生,MySQL默認的隔離級別 |
| SERIALIZABLE | 串行化 | 可以解決以上所有讀問題,效率最差,安全性最高,一般不用 |
在資料庫本地事務隔離級別 讀已提交(Read Committed) 或以上的基礎上,Seata(AT 模式)的默認全域隔離級別是 讀未提交(Read Uncommitted) ,
如果應用在特定場景下,必需要求全域的 讀已提交 ,目前 Seata 的方式是通過 SELECT FOR UPDATE 陳述句的代理,

SELECT FOR UPDATE 陳述句的執行會申請 全域鎖 ,如果 全域鎖 被其他事務持有,則釋放本地鎖(回滾 SELECT FOR UPDATE 陳述句的本地執行)并重試,這個程序中,查詢是被 block 住的,直到 全域鎖 拿到,即讀取的相關資料是 已提交 的,才回傳,
出于總體性能上的考慮,Seata 目前的方案并沒有對所有 SELECT 陳述句都進行代理,僅針對 FOR UPDATE 的 SELECT 陳述句,
3.4、作業機制
以一個示例來說明整個 AT 分支的作業程序,
業務表:product
| Field | Type | Key |
|---|---|---|
| id | bigint(20) | PRI |
| name | varchar(100) | |
| since | varchar(100) |
AT 分支事務的業務邏輯:
update product set name = 'GTS' where name = 'TXC';
3.4.1、一階段
程序:
- 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表中,
{
"branchId": 641789253,
"undoItems": [{
"afterImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"beforeImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"sqlType": "UPDATE"
}],
"xid": "xid:xxx"
}
- 6、提交前,向 TC 注冊分支:申請
product表中,主鍵值等于 1 的記錄的 全域鎖 , - 7、本地事務提交:業務資料的更新和前面步驟中生成的 UNDO LOG 一并提交,
- 8、將本地事務提交的結果上報給 TC,
3.4.2、二階段-回滾
- 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,
3.4.3、二階段-提交
- 1、收到 TC 的分支提交請求,把請求放入一個異步任務的佇列中,馬上回傳提交成功的結果給 TC,
- 2、異步任務階段的分支提交請求將異步和批量地洗掉相應 UNDO LOG 記錄,
3.5、附錄章節
回滾日志表
UNDO_LOG Table:不同資料庫在型別上會略有差別,
以 MySQL 為例:
| Field | Type |
|---|---|
| branch_id | bigint PK |
| xid | varchar(100) |
| context | varchar(128) |
| rollback_info | longblob |
| log_status | tinyint |
| log_created | datetime |
| log_modified | datetime |
-- 注意此處0.7.0+ 增加欄位 context
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=1 DEFAULT CHARSET=utf8;
第四章 單體版多資料源事務管理:AT
4.1、匯入資料
4.1.1、創建賬戶資料庫
資料庫環境為mysql 5.7.33,請重新匯入運行以下sql陳述句:
DROP DATABASE IF EXISTS `seata_account`;
CREATE DATABASE `seata_account`;
USE `seata_account`;
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用戶id',
`total` decimal(10,0) DEFAULT NULL COMMENT '總額度',
`used` decimal(10,0) DEFAULT NULL COMMENT '已用余額',
`residue` decimal(10,0) DEFAULT '0' COMMENT '剩余額度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='賬戶表';
insert into `t_account`(`id`,`user_id`,`total`,`used`,`residue`) values (1,1,'1000','0','1000');
SELECT * FROM `seata_account`.`t_account` LIMIT 0, 1000;

4.1.2、創建庫存資料庫
資料庫環境為mysql 5.7.33,請重新匯入運行以下sql陳述句:
DROP DATABASE IF EXISTS `seata_storage`;
CREATE DATABASE `seata_storage`;
USE `seata_storage`;
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
`total` int(11) DEFAULT NULL COMMENT '總庫存',
`used` int(11) DEFAULT NULL COMMENT '已用庫存',
`residue` int(11) DEFAULT NULL COMMENT '剩余庫存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='庫存表';
insert into `t_storage`(`id`,`product_id`,`total`,`used`,`residue`) values (1,1,100,0,100);
SELECT * FROM `seata_storage`.`t_storage` LIMIT 0, 1000;

4.1.3、創建訂單資料庫
資料庫環境為mysql 5.7.33,請重新匯入運行以下sql陳述句:
DROP DATABASE `seata_order`;
CREATE DATABASE `seata_order`;
USE `seata_order`;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用戶id',
`product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
`count` int(11) DEFAULT NULL COMMENT '數量',
`money` decimal(11,0) DEFAULT NULL COMMENT '金額',
`status` int(1) DEFAULT NULL COMMENT '狀態',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='訂單表';
SELECT * FROM `seata_order`.`t_order` LIMIT 0, 1000;

4.2、匯入工程
在桌面重新創建一個檔案夾,名字無所謂,拷貝配套資料中的單體版\single-app-seata-at-study到此檔案夾,然后使用idea打開即可,

single-app-seata-at-study已經實作了基本的代碼流程,非常簡單,相信你一定能看懂:

single-app-seata-at-study是一個純粹的Spring Boot單體應用,連接著三個資料源:

唯一需要你注意的是,打開application.yaml,查看連接資料源的賬戶密碼是否正確,如下:

4.3、正常測驗
請啟動當前工程,然后輸入下單地址測驗:http://localhost:9000/order/create?userId=1&productId=1&count=10&money=100&status=1

請打開資料庫表,查看下單之后資料的變化,依次是:訂單資料庫表、賬戶資料庫表、庫存資料庫表



4.4、例外測驗
修改com.caochenlei.service.impl.AccountServiceImpl代碼添加例外,代碼如下:
@Override
public void decrease(Long userId, BigDecimal money) {
int i = 1 / 0; //模擬例外出錯
accountMapper.decrease(userId, money);
}
請重啟當前工程,然后輸入下單地址測驗:http://localhost:9000/order/create?userId=1&productId=1&count=10&money=100&status=1

請打開資料庫表,查看下單之后資料的變化,依次是:訂單資料庫表、賬戶資料庫表、庫存資料庫表



我們發現訂單下單失敗,訂單資料庫中多了一條訂單記錄,實際上這條記錄不應該有,而且賬戶對應的余額和庫存并沒有減少,這個問題是十分可怕的,因為沒有事務的支持,不能做到要不全部執行,要不全部失敗,
4.5、添加新表
在賬戶資料庫添加回滾日志表,sql陳述句如下:
USE `seata_account`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=1 DEFAULT CHARSET=utf8;
在庫存資料庫添加回滾日志表,sql陳述句如下:
USE `seata_storage`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=1 DEFAULT CHARSET=utf8;
在訂單資料庫添加回滾日志表,sql陳述句如下:
USE `seata_order`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=1 DEFAULT CHARSET=utf8;
最終完成以后的效果如下圖:

4.6、添加依賴
在pom.xml中新增依賴:
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
4.7、添加配置
在application.yaml中新增配置:
server:
port: 9000
management:
endpoints:
web:
exposure:
include: '*'
spring:
application:
name: single-app-seata-at-study
datasource:
dynamic:
primary: ordere-ds
datasource:
#賬戶資料源(account-ds自定義的)
account-ds:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useUnicode=true&useSSL=false
username: root #資料庫用戶,請根據實際填寫
password: 123456 #資料庫密碼,請根據實際填寫
#庫存資料源(storage-ds自定義的)
storage-ds:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useUnicode=true&useSSL=false
username: root #資料庫用戶,請根據實際填寫
password: 123456 #資料庫密碼,請根據實際填寫
#訂單資料源(ordere-ds自定義的)
ordere-ds:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useUnicode=true&useSSL=false
username: root #資料庫用戶,請根據實際填寫
password: 123456 #資料庫密碼,請根據實際填寫
seata: true #開啟seata支持
seata-mode: at #選擇seata模式:at、xa
#seata配置
seata:
application-id: myapp #應用的編號
tx-service-group: myapp-group #事務組名稱
service:
vgroup-mapping:
myapp-group: guangzhou #當前事務組使用 guangzhou 機房的seata服務
group-list:
guangzhou: 127.0.0.1:8091 #主機房,部署seata tc server/seata tc cluster
shanghai: 127.0.0.1:8091 #備用機房,部署seata tc server/seata tc cluster
config:
type: file #組態檔使用檔案存盤方式
registry:
type: file #注冊中心使用檔案存盤方式
事務分組與高可用,最佳實踐1:TC的異地多機房容災如下,更多最佳實踐請訪問:http://seata.io/zh-cn/docs/user/txgroup/transaction-group-and-ha.html
- 假定TC集群部署在兩個機房:guangzhou機房(主)和shanghai機房(備)各兩個實體
- 一整套微服務架構專案:projectA
- projectA內有微服務:serviceA、serviceB、serviceC 和 serviceD
其中,projectA所有微服務的事務分組tx-transaction-group設定為:projectA,projectA正常情況下使用guangzhou的TC集群(主)
那么正常情況下,client端的配置如下所示:
seata.tx-service-group=projectA
seata.service.vgroup-mapping.projectA=Guangzhou

假如此時guangzhou集群分組整個down掉,或者因為網路原因projectA暫時無法與Guangzhou機房通訊,那么我們將配置中心中的Guangzhou集群分組改為Shanghai,如下:
seata.service.vgroup-mapping.projectA=Shanghai
并推送到各個微服務,便完成了對整個projectA專案的TC集群動態切換,

4.8、添加注解
修改com.caochenlei.service.impl.OrderServiceImpl開啟全域事務管理,代碼如下:
@DS("order-ds")
@Slf4j
@Service
@GlobalTransactional //開啟seata全域事務注解,支持放在類上、方法上
public class OrderServiceImpl implements OrderService {
...
...
}
4.9、例外測驗
請重啟當前工程,然后輸入下單地址測驗:http://localhost:9000/order/create?userId=1&productId=1&count=10&money=100&status=1

請打開資料庫表,查看下單之后資料的變化,依次是:訂單資料庫表、賬戶資料庫表、庫存資料庫表



我們發現雖然下單失敗了,但是并沒有往訂單資料庫插入訂單資訊,賬戶余額和庫存也沒有減少,這符合我們的業務邏輯,多資料源的事務管理完美解決,
注意:測驗完畢,請關閉當前工程,防止影響其他專案,
第五章 分布式單資料源事務管理:AT
5.1、匯入資料
5.1.1、重置賬戶資料庫
資料庫環境為mysql 5.7.33,請重新匯入運行以下sql陳述句:
DROP DATABASE IF EXISTS `seata_account`;
CREATE DATABASE `seata_account`;
USE `seata_account`;
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用戶id',
`total` decimal(10,0) DEFAULT NULL COMMENT '總額度',
`used` decimal(10,0) DEFAULT NULL COMMENT '已用余額',
`residue` decimal(10,0) DEFAULT '0' COMMENT '剩余額度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='賬戶表';
insert into `t_account`(`id`,`user_id`,`total`,`used`,`residue`) values (1,1,'1000','0','1000');
SELECT * FROM `seata_account`.`t_account` LIMIT 0, 1000;

5.1.2、重置庫存資料庫
資料庫環境為mysql 5.7.33,請重新匯入運行以下sql陳述句:
DROP DATABASE IF EXISTS `seata_storage`;
CREATE DATABASE `seata_storage`;
USE `seata_storage`;
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
`total` int(11) DEFAULT NULL COMMENT '總庫存',
`used` int(11) DEFAULT NULL COMMENT '已用庫存',
`residue` int(11) DEFAULT NULL COMMENT '剩余庫存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='庫存表';
insert into `t_storage`(`id`,`product_id`,`total`,`used`,`residue`) values (1,1,100,0,100);
SELECT * FROM `seata_storage`.`t_storage` LIMIT 0, 1000;

5.1.3、重置訂單資料庫
資料庫環境為mysql 5.7.33,請重新匯入運行以下sql陳述句:
DROP DATABASE `seata_order`;
CREATE DATABASE `seata_order`;
USE `seata_order`;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用戶id',
`product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
`count` int(11) DEFAULT NULL COMMENT '數量',
`money` decimal(11,0) DEFAULT NULL COMMENT '金額',
`status` int(1) DEFAULT NULL COMMENT '狀態',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='訂單表';
SELECT * FROM `seata_order`.`t_order` LIMIT 0, 1000;

5.2、匯入工程
在桌面重新創建一個檔案夾,名字無所謂,拷貝配套資料中的分布式\distributed-seata-at-study到此檔案夾,然后使用idea打開即可,

distributed-seata-at-study已經實作了基本的代碼流程,非常簡單,相信你一定能看懂:

distributed-seata-at-study是一個典型的微服務應用,一共有三個服務,每個服務都實作了基本的代碼邏輯,并且都對應一個資料源,架構如下圖:

唯一需要你注意的是,打開application.yaml,查看連接資料源的賬戶密碼是否正確,并且我們需要你啟動nacos服務注冊中心,如下:
service-account

service-storage

service-order

5.3、正常測驗
確保啟動nacos注冊中心,
請啟動當前工程,然后輸入下單地址測驗:http://localhost:9003/order/create?userId=1&productId=1&count=10&money=100&status=1


請打開資料庫表,查看下單之后資料的變化,依次是:訂單資料庫表、賬戶資料庫表、庫存資料庫表



5.4、例外測驗
打開工程service-account修改com.caochenlei.service.impl.AccountServiceImpl代碼添加例外,代碼如下:
@Override
public void decrease(Long userId, BigDecimal money) {
int i = 1 / 0; //模擬例外出錯
accountMapper.decrease(userId, money);
}
請重啟當前工程,然后輸入下單地址測驗:http://localhost:9003/order/create?userId=1&productId=1&count=10&money=100&status=1

請打開資料庫表,查看下單之后資料的變化,依次是:訂單資料庫表、賬戶資料庫表、庫存資料庫表



我們發現訂單下單失敗,訂單資料庫中多了一條訂單記錄,實際上這條記錄不應該有,而且賬戶對應的余額和庫存并沒有減少,這個問題是十分可怕的,因為沒有事務的支持,不能做到要不全部執行,要不全部失敗,
5.5、添加新表
在賬戶資料庫添加回滾日志表,sql陳述句如下:
USE `seata_account`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=1 DEFAULT CHARSET=utf8;
在庫存資料庫添加回滾日志表,sql陳述句如下:
USE `seata_storage`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=1 DEFAULT CHARSET=utf8;
在訂單資料庫添加回滾日志表,sql陳述句如下:
USE `seata_order`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=1 DEFAULT CHARSET=utf8;
最終完成以后的效果如下圖:

5.6、添加依賴
在service-order的pom.xml中新增依賴:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
5.7、添加配置
在service-order的application.yaml中新增配置:
server:
port: 9003
management:
endpoints:
web:
exposure:
include: '*'
spring:
application:
name: service-order
cloud:
nacos:
discovery:
server-addr: http://localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useUnicode=true&useSSL=false
username: root #資料庫用戶,請根據實際填寫
password: 123456 #資料庫密碼,請根據實際填寫
#防止feign呼叫超時對測驗結果有影響
feign:
client:
config:
default:
connect-timeout: 5000
read-timeout: 5000
#seata配置
seata:
application-id: myapp #應用的編號
tx-service-group: myapp-group #事務組名稱
service:
vgroup-mapping:
myapp-group: guangzhou #當前事務組使用 guangzhou 機房的seata服務
group-list:
guangzhou: 127.0.0.1:8091 #主機房,部署seata tc server/seata tc cluster
shanghai: 127.0.0.1:8091 #備用機房,部署seata tc server/seata tc cluster
config:
type: file #組態檔使用檔案存盤方式
registry:
type: file #注冊中心使用檔案存盤方式
5.8、添加注解
打開service-order修改com.caochenlei.service.impl.OrderServiceImpl開啟全域事務管理,代碼如下:
@Slf4j
@Service
@GlobalTransactional //開啟seata全域事務注解,支持放在類上、方法上
public class OrderServiceImpl implements OrderService {
...
...
}
5.9、例外測驗
請重啟當前工程,然后輸入下單地址測驗:http://localhost:9003/order/create?userId=1&productId=1&count=10&money=100&status=1

請打開資料庫表,查看下單之后資料的變化,依次是:訂單資料庫表、賬戶資料庫表、庫存資料庫表



我們發現雖然下單失敗了,但是并沒有往訂單資料庫插入訂單資訊,賬戶余額和庫存也沒有減少,這符合我們的業務邏輯,分布式下的事務管理完美解決,
注意:測驗完畢,請關閉當前工程,防止影響其他專案,
第六章 Seata集群版部署:TC Cluster
6.1、關閉單機版
關閉單機版的命令列視窗,
6.2、創建資料庫
首先初始化資料庫:CREATE DATABASE seata; USE seata;
獲取運行腳本地址:https://github.com/seata/seata/tree/develop/script/server/db
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
6.3、修改存盤模式
找到seata\conf\file.conf,把存盤模式修改為db并修改資料源連接,修改后保存,如下:
## transaction log store, only used in seata-server
store {
## store mode: file、db、redis
mode = "db"
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata?characterEncoding=utf8&useUnicode=true&useSSL=false"
user = "root"
password = "123456"
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
## redis store property
redis {
host = "127.0.0.1"
port = "6379"
password = ""
database = "0"
minConn = 1
maxConn = 10
queryLimit = 100
}
}
6.4、修改注冊中心
找到seata\conf\registry.conf,把注冊中心修改為nacos并修改登錄配置,修改后保存,如下:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = "public"
cluster = "default"
username = "nacos"
password = "nacos"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = 0
password = ""
cluster = "default"
timeout = 0
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
...
...
...
...
6.5、啟動兩個實體
啟動第一個實體:C:\DevTools\seata\bin>seata-server.bat -p 18901 -n 1
啟動第二個實體:C:\DevTools\seata\bin>seata-server.bat -p 28901 -n 2
- -p:Seata TC Server 監聽的埠;
- -n:Server node,在多個 TC Server 時,需區分各自節點,用于生成不同區間的 transactionId 事務編號,以免沖突;
6.6、查看注冊中心
打開注冊中心:http://localhost:8848/nacos/,登錄賬戶:nacos,登錄密碼:nacos

第七章 Seata的常用模式:TCC
回顧總覽中的描述:一個分布式的全域事務,整體是 兩階段提交 的模型,全域事務是由若干分支事務組成的,分支事務要滿足 兩階段提交 的模型要求,即需要每個分支事務都具備自己的:
- 一階段 prepare 行為
- 二階段 commit 或 rollback 行為

根據兩階段行為模式的不同,我們將分支事務劃分為 Automatic (Branch) Transaction Mode 和 TCC (Branch) Transaction Mode.
AT 模式(參考鏈接 TBD)基于 支持本地 ACID 事務 的 關系型資料庫:
- 一階段 prepare 行為:在本地事務中,一并提交業務資料更新和相應回滾日志記錄,
- 二階段 commit 行為:馬上成功結束,自動 異步批量清理回滾日志,
- 二階段 rollback 行為:通過回滾日志,自動 生成補償操作,完成資料回滾,
相應的,TCC 模式,不依賴于底層資料資源的事務支持:
- 一階段 prepare 行為:呼叫 自定義 的 prepare 邏輯,
- 二階段 commit 行為:呼叫 自定義 的 commit 邏輯,
- 二階段 rollback 行為:呼叫 自定義 的 rollback 邏輯,
所謂 TCC 模式,是指支持把 自定義 的分支事務納入到全域事務的管理中,
我們其實可以理解,TCC模式所有階段的代碼都是自己實作的,所以它能夠更加靈活的回滾各種關系型資料庫、非關系型資料庫、訊息中間件等,
第八章 單體版多資料源事務管理:TCC
8.1、匯入資料
8.1.1、重置賬戶資料庫
資料庫環境為mysql 5.7.33,請重新匯入運行以下sql陳述句:
DROP DATABASE IF EXISTS `seata_account`;
CREATE DATABASE `seata_account`;
USE `seata_account`;
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用戶id',
`total` decimal(10,0) DEFAULT NULL COMMENT '總額度',
`used` decimal(10,0) DEFAULT NULL COMMENT '已用余額',
`residue` decimal(10,0) DEFAULT '0' COMMENT '剩余額度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='賬戶表';
insert into `t_account`(`id`,`user_id`,`total`,`used`,`residue`) values (1,1,'1000','0','1000');
SELECT * FROM `seata_account`.`t_account` LIMIT 0, 1000;

8.1.2、重置庫存資料庫
資料庫環境為mysql 5.7.33,請重新匯入運行以下sql陳述句:
DROP DATABASE IF EXISTS `seata_storage`;
CREATE DATABASE `seata_storage`;
USE `seata_storage`;
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
`total` int(11) DEFAULT NULL COMMENT '總庫存',
`used` int(11) DEFAULT NULL COMMENT '已用庫存',
`residue` int(11) DEFAULT NULL COMMENT '剩余庫存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='庫存表';
insert into `t_storage`(`id`,`product_id`,`total`,`used`,`residue`) values (1,1,100,0,100);
SELECT * FROM `seata_storage`.`t_storage` LIMIT 0, 1000;

8.1.3、重置訂單資料庫
資料庫環境為mysql 5.7.33,請重新匯入運行以下sql陳述句:
DROP DATABASE `seata_order`;
CREATE DATABASE `seata_order`;
USE `seata_order`;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用戶id',
`product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
`count` int(11) DEFAULT NULL COMMENT '數量',
`money` decimal(11,0) DEFAULT NULL COMMENT '金額',
`status` int(1) DEFAULT NULL COMMENT '狀態',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='訂單表';
SELECT * FROM `seata_order`.`t_order` LIMIT 0, 1000;

8.2、匯入工程
在桌面重新創建一個檔案夾,名字無所謂,拷貝配套資料中的單體版\single-app-seata-tcc-study到此檔案夾,然后使用idea打開即可,

single-app-seata-tcc-study已經實作了基本的代碼流程,非常簡單,相信你一定能看懂:

single-app-seata-tcc-study是一個純粹的Spring Boot單體應用,連接著三個資料源:

唯一需要你注意的是,打開application.yaml,查看連接資料源的賬戶密碼是否正確,如下:

8.3、正常測驗
請啟動當前工程,然后輸入下單地址測驗:http://localhost:9000/order/create?userId=1&productId=1&count=10&money=100&status=1

請打開資料庫表,查看下單之后資料的變化,依次是:訂單資料庫表、賬戶資料庫表、庫存資料庫表



8.4、例外測驗
修改com.caochenlei.service.impl.OrderServiceImpl代碼添加例外,代碼如下:
@DS("order-ds")
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private AccountService accountService;
@Resource
private StorageService storageService;
@Override
public void create(Order order) {
// 1、新建訂單
log.info("----->新建訂單開始");
orderMapper.create(order);
log.info("----->新建訂單結束");
// 2、扣減余額
log.info("----->扣減余額開始");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("----->扣減余額結束");
int i = 1 / 0; //模擬例外出錯
// 3、扣減庫存
log.info("----->扣減庫存開始");
storageService.decrease(order.getProductId(), order.getCount());
log.info("----->扣減庫存結束");
}
}
請重啟當前工程,然后輸入下單地址測驗:http://localhost:9000/order/create?userId=1&productId=1&count=10&money=100&status=1

請打開資料庫表,查看下單之后資料的變化,依次是:訂單資料庫表、賬戶資料庫表、庫存資料庫表



我們發現訂單下單失敗,訂單資料庫中多了一條訂單記錄,實際上這條記錄不應該有,同時,賬戶的余額也減少了,但是,庫存并沒有減少,這個問題是十分可怕的,因為沒有事務的支持,不能做到要不全部執行,要不全部失敗,
8.5、添加新表
在賬戶資料庫添加回滾日志表,sql陳述句如下:
USE `seata_account`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=1 DEFAULT CHARSET=utf8;
在庫存資料庫添加回滾日志表,sql陳述句如下:
USE `seata_storage`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=1 DEFAULT CHARSET=utf8;
在訂單資料庫添加回滾日志表,sql陳述句如下:
USE `seata_order`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=1 DEFAULT CHARSET=utf8;
最終完成以后的效果如下圖:

8.6、添加依賴
在pom.xml中新增依賴:
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.3.2</version>
</dependency>
8.7、添加配置
在application.yaml中新增配置:
server:
port: 9000
management:
endpoints:
web:
exposure:
include: '*'
spring:
application:
name: single-app-seata-tcc-study
datasource:
dynamic:
primary: ordere-ds
datasource:
#賬戶資料源(account-ds自定義的)
account-ds:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useUnicode=true&useSSL=false
username: root #資料庫用戶,請根據實際填寫
password: 123456 #資料庫密碼,請根據實際填寫
#庫存資料源(storage-ds自定義的)
storage-ds:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useUnicode=true&useSSL=false
username: root #資料庫用戶,請根據實際填寫
password: 123456 #資料庫密碼,請根據實際填寫
#訂單資料源(ordere-ds自定義的)
ordere-ds:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useUnicode=true&useSSL=false
username: root #資料庫用戶,請根據實際填寫
password: 123456 #資料庫密碼,請根據實際填寫
seata: true #開啟seata支持
seata-mode: at #選擇seata模式:at、xa,不支持tcc,我們需要手動編碼實作tcc第二階段
#seata配置
seata:
application-id: myapp #應用的編號
tx-service-group: myapp-group #事務組名稱
service:
vgroup-mapping:
myapp-group: default #當前事務組使用 cluster: default
config:
type: file #組態檔使用檔案存盤方式
registry:
type: nacos #注冊中心使用nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: public
cluster: default
username: nacos
password: nacos
8.8、添加注解
修改com.caochenlei.service.impl.OrderServiceImpl開啟全域事務管理,代碼如下:
@DS("order-ds")
@Slf4j
@Service
@GlobalTransactional //開啟seata全域事務注解,支持放在類上、方法上
public class OrderServiceImpl implements OrderService {
...
...
}
8.9、賬戶服務
修改介面:com.caochenlei.service.AccountService
@LocalTCC//此注解標識TCC為本地模式,即該事務是本地呼叫
public interface AccountService {
//第一階段:嘗試扣減余額
@TwoPhaseBusinessAction(name = "decreaseMoney", commitMethod = "commitDecreaseMoney", rollbackMethod = "rollbackDecreaseMoney")
void decrease(@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "money") BigDecimal money);
//第二階段:提交處理方法
boolean commitDecreaseMoney(BusinessActionContext context);
//第二階段:回滾處理方法
boolean rollbackDecreaseMoney(BusinessActionContext context);
}
修改實作:com.caochenlei.service.impl.AccountServiceImpl
@DS("account-ds")
@Service
public class AccountServiceImpl implements AccountService {
@Resource
private AccountMapper accountMapper;
@Override
public void decrease(Long userId, BigDecimal money) {
accountMapper.decrease(userId, money);
}
@Override
public boolean commitDecreaseMoney(BusinessActionContext context) {
return true;//可以直接回傳true,即空確認
}
@Override
public boolean rollbackDecreaseMoney(BusinessActionContext context) {
//TODO 這里可以實作中間件、非關系型資料庫的回滾操作
//通過業務動作背景關系獲取指定引數的引數值
String userId = context.getActionContext("userId").toString();
String money = context.getActionContext("money").toString();
//手動進行資料庫回滾,把減去的余額加回去
accountMapper.increase(new Long(userId), new BigDecimal(money));
//我們手動輸出一句話,代表回滾使用我們的
System.out.println("資料回滾了,這可真的好");
return true;
}
}
修改映射:com.caochenlei.mapper.AccountMapper
@Mapper
public interface AccountMapper {
//扣減余額
@Update("update t_account set used=used+#{money},residue=residue-#{money} where user_id=#{userId};")
int decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
//加回余額
@Update("update t_account set used=used-#{money},residue=residue+#{money} where user_id=#{userId};")
int increase(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
8.10、例外測驗
請重啟當前工程,然后輸入下單地址測驗:http://localhost:9000/order/create?userId=1&productId=1&count=10&money=100&status=1

請打開資料庫表,查看下單之后資料的變化,依次是:訂單資料庫表、賬戶資料庫表、庫存資料庫表



我們發現雖然下單失敗了,但是并沒有往訂單資料庫插入訂單資訊,賬戶余額和庫存也沒有減少,這符合我們的業務邏輯,可以通過這種方式實作中間件、非關系型資料庫的回滾操作,

而賬戶余額的回滾操作則是使用的是TCC模式下,我們自定義的第二階段回滾方法,
注意:測驗完畢,請關閉當前工程,防止影響其他專案,正常情況下,每一個服務都需要配置seata,我這里偷懶了,大家要注意一下!
第九章 分布式單資料源事務管理:TCC
9.1、匯入資料
9.1.1、重置賬戶資料庫
資料庫環境為mysql 5.7.33,請重新匯入運行以下sql陳述句:
DROP DATABASE IF EXISTS `seata_account`;
CREATE DATABASE `seata_account`;
USE `seata_account`;
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用戶id',
`total` decimal(10,0) DEFAULT NULL COMMENT '總額度',
`used` decimal(10,0) DEFAULT NULL COMMENT '已用余額',
`residue` decimal(10,0) DEFAULT '0' COMMENT '剩余額度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='賬戶表';
insert into `t_account`(`id`,`user_id`,`total`,`used`,`residue`) values (1,1,'1000','0','1000');
SELECT * FROM `seata_account`.`t_account` LIMIT 0, 1000;

9.1.2、重置庫存資料庫
資料庫環境為mysql 5.7.33,請重新匯入運行以下sql陳述句:
DROP DATABASE IF EXISTS `seata_storage`;
CREATE DATABASE `seata_storage`;
USE `seata_storage`;
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
`total` int(11) DEFAULT NULL COMMENT '總庫存',
`used` int(11) DEFAULT NULL COMMENT '已用庫存',
`residue` int(11) DEFAULT NULL COMMENT '剩余庫存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='庫存表';
insert into `t_storage`(`id`,`product_id`,`total`,`used`,`residue`) values (1,1,100,0,100);
SELECT * FROM `seata_storage`.`t_storage` LIMIT 0, 1000;

9.1.3、重置訂單資料庫
資料庫環境為mysql 5.7.33,請重新匯入運行以下sql陳述句:
DROP DATABASE `seata_order`;
CREATE DATABASE `seata_order`;
USE `seata_order`;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用戶id',
`product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
`count` int(11) DEFAULT NULL COMMENT '數量',
`money` decimal(11,0) DEFAULT NULL COMMENT '金額',
`status` int(1) DEFAULT NULL COMMENT '狀態',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='訂單表';
SELECT * FROM `seata_order`.`t_order` LIMIT 0, 1000;

9.2、匯入工程
在桌面重新創建一個檔案夾,名字無所謂,拷貝配套資料中的分布式\distributed-seata-tcc-study到此檔案夾,然后使用idea打開即可,

distributed-seata-tcc-study已經實作了基本的代碼流程,非常簡單,相信你一定能看懂:

distributed-seata-tcc-study是一個典型的微服務應用,一共有三個服務,每個服務都實作了基本的代碼邏輯,并且都對應一個資料源,架構如下圖:

唯一需要你注意的是,打開application.yaml,查看連接資料源的賬戶密碼是否正確,并且我們需要你啟動nacos服務注冊中心,如下:
service-account

service-storage

service-order

9.3、正常測驗
確保啟動nacos注冊中心,
請啟動當前工程,然后輸入下單地址測驗:http://localhost:9003/order/create?userId=1&productId=1&count=10&money=100&status=1


請打開資料庫表,查看下單之后資料的變化,依次是:訂單資料庫表、賬戶資料庫表、庫存資料庫表



9.4、例外測驗
打開工程service-order修改com.caochenlei.service.impl.OrderServiceImpl代碼添加例外,代碼如下:
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private AccountService accountService;
@Resource
private StorageService storageService;
@Override
public void create(Order order) {
// 1、新建訂單
log.info("----->新建訂單開始");
orderMapper.create(order);
log.info("----->新建訂單結束");
// 2、扣減余額
log.info("----->扣減余額開始");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("----->扣減余額結束");
int i = 1 / 0; //模擬例外出錯
// 3、扣減庫存
log.info("----->扣減庫存開始");
storageService.decrease(order.getProductId(), order.getCount());
log.info("----->扣減庫存結束");
}
}
請重啟當前工程,然后輸入下單地址測驗:http://localhost:9003/order/create?userId=1&productId=1&count=10&money=100&status=1

請打開資料庫表,查看下單之后資料的變化,依次是:訂單資料庫表、賬戶資料庫表、庫存資料庫表



我們發現訂單下單失敗,訂單資料庫中多了一條訂單記錄,實際上這條記錄不應該有,同時,賬戶的余額也減少了,但是,庫存并沒有減少,這個問題是十分可怕的,因為沒有事務的支持,不能做到要不全部執行,要不全部失敗,
9.5、添加新表
在賬戶資料庫添加回滾日志表,sql陳述句如下:
USE `seata_account`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=1 DEFAULT CHARSET=utf8;
在庫存資料庫添加回滾日志表,sql陳述句如下:
USE `seata_storage`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=1 DEFAULT CHARSET=utf8;
在訂單資料庫添加回滾日志表,sql陳述句如下:
USE `seata_order`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) 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=1 DEFAULT CHARSET=utf8;
最終完成以后的效果如下圖:

9.6、添加依賴
在service-order、service-account的pom.xml中新增依賴:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
9.7、添加配置
在service-order、service-account的application.yaml中新增配置:
...
...
#seata配置
seata:
application-id: myapp #應用的編號
tx-service-group: myapp-group #事務組名稱
service:
vgroup-mapping:
myapp-group: default #當前事務組使用 cluster: default
config:
type: file #組態檔使用檔案存盤方式
registry:
type: nacos #注冊中心使用nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: public
cluster: default
username: nacos
password: nacos
9.8、添加注解
打開service-order修改com.caochenlei.service.impl.OrderServiceImpl開啟全域事務管理,代碼如下:
@Slf4j
@Service
@GlobalTransactional //開啟seata全域事務注解,支持放在類上、方法上
public class OrderServiceImpl implements OrderService {
...
...
}
9.9、賬戶服務
打開service-account修改介面:com.caochenlei.service.AccountService
@LocalTCC//此注解標識TCC為本地模式,即該事務是本地呼叫
public interface AccountService {
//第一階段:嘗試扣減余額
@TwoPhaseBusinessAction(name = "decreaseMoney", commitMethod = "commitDecreaseMoney", rollbackMethod = "rollbackDecreaseMoney")
void decrease(@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "money") BigDecimal money);
//第二階段:提交處理方法
boolean commitDecreaseMoney(BusinessActionContext context);
//第二階段:回滾處理方法
boolean rollbackDecreaseMoney(BusinessActionContext context);
}
打開service-account修改實作:com.caochenlei.service.impl.AccountServiceImpl
@Service
public class AccountServiceImpl implements AccountService {
@Resource
private AccountMapper accountMapper;
@Override
public void decrease(Long userId, BigDecimal money) {
accountMapper.decrease(userId, money);
}
@Override
public boolean commitDecreaseMoney(BusinessActionContext context) {
return true;//可以直接回傳true,即空確認
}
@Override
public boolean rollbackDecreaseMoney(BusinessActionContext context) {
//TODO 這里可以實作中間件、非關系型資料庫的回滾操作
//通過業務動作背景關系獲取指定引數的引數值
String userId = context.getActionContext("userId").toString();
String money = context.getActionContext("money").toString();
//手動進行資料庫回滾,把減去的余額加回去
accountMapper.increase(new Long(userId), new BigDecimal(money));
//我們手動輸出一句話,代表回滾使用我們的
System.out.println("資料回滾了,這可真的好");
return true;
}
}
打開service-account修改映射:com.caochenlei.mapper.AccountMapper
@Mapper
public interface AccountMapper {
//扣減余額
@Update("update t_account set used=used+#{money},residue=residue-#{money} where user_id=#{userId};")
int decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
//加回余額
@Update("update t_account set used=used-#{money},residue=residue+#{money} where user_id=#{userId};")
int increase(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
9.10、例外測驗
請重啟service-order、service-account工程
然后輸入下單地址測驗:http://localhost:9003/order/create?userId=1&productId=1&count=10&money=100&status=1

請打開資料庫表,查看下單之后資料的變化,依次是:訂單資料庫表、賬戶資料庫表、庫存資料庫表



我們發現雖然下單失敗了,但是并沒有往訂單資料庫插入訂單資訊,賬戶余額和庫存也沒有減少,這符合我們的業務邏輯,分布式下的事務管理完美解決,

而賬戶余額的回滾操作則是使用的是TCC模式下,我們自定義的第二階段回滾方法,
注意:測驗完畢,請關閉當前工程,防止影響其他專案,正常情況下,每一個服務都需要配置seata,我這里偷懶了,大家要注意一下!
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/263873.html
標籤:其他
