分布式事務——Seata
分布式事務
1. 本地事務與分布式事務
1.1 本地事務
本地事務,也就是傳統的單機事務,在傳統資料庫事務中,必須要滿足四個原則:

1.2 分布式事務問題
分布式事務,就是指不是在單個服務或單個資料庫架構下,產生的事務,例如:
- 跨資料源的分布式事務
- 跨服務的分布式事務
- 綜合情況
在資料庫水平拆分、服務垂直拆分之后,一個業務操作通常要跨多個資料庫、服務才能完成,例如電商行業中比較常見的下單付款案例,包括下面幾個行為:
- 創建新訂單
- 扣減商品庫存
- 從用戶賬戶余額扣除金額
完成上面的操作需要訪問三個不同的微服務和三個不同的資料庫,

訂單的創建、庫存的扣減、賬戶扣款在每一個服務和資料庫內是一個本地事務,可以保證ACID原則,
但是當我們把三件事情看做一個"業務",要滿足保證“業務”的原子性,要么所有操作全部成功,要么全部失敗,不允許出現部分成功部分失敗的現象,這就是分布式系統下的事務了,
此時ACID難以滿足,這是分布式事務要解決的問題
2. 分布式事務解決理論
解決分布式事務問題,需要一些分布式系統的基礎知識作為理論指導,
2.1.CAP定理
CAP定理是說在P一定會出現的情況下,A和C之間只能實作一個
1998年,加州大學的計算機科學家 Eric Brewer 提出,分布式系統有三個指標,
- Consistency(一致性)
- Availability(可用性)
- Partition tolerance (磁區容錯性)

它們的第一個字母分別是 C、A、P,
Eric Brewer 說,這三個指標不可能同時做到,這個結論就叫做 CAP 定理,
2.1.1. C:一致性
Consistency(一致性):用戶訪問分布式系統中的任意節點,得到的資料必須一致,
比如現在包含兩個節點,其中的初始資料是一致的;當我們修改其中一個節點的資料時,兩者的資料產生了差異;要想保住一致性,就必須實作node01 到 node02的資料 同步:

2.1.2. A:可用性
Availability (可用性):用戶訪問集群中的任意健康節點,必須能得到回應,而不是超時或拒絕,
如圖,有三個節點的集群,訪問任何一個都可以及時得到回應:

當有部分節點因為網路故障或其它原因無法訪問時,代表節點不可用:

2.1.3. P:磁區容錯
Partition(磁區):因為網路故障或其它原因導致分布式系統中的部分節點與其它節點失去連接,形成獨立磁區,

Tolerance(容錯):在集群出現磁區時,整個系統也要持續對外提供服務
2.1.4. 矛盾
在分布式系統中,系統間的網路不能100%保證健康,一定會有故障的時候,而服務有必須對外保證服務,因此Partition Tolerance不可避免,
當節點接收到新的資料變更時,就會出現問題了:

如果此時要保證一致性,就必須等待網路恢復,完成資料同步后,整個集群才對外提供服務,服務處于阻塞狀態,不可用,
如果此時要保證可用性,就不能等待網路恢復,那node01、node02與node03之間就會出現資料不一致,
也就是說,在P一定會出現的情況下,A和C之間只能實作一個,
2.2.BASE理論
BASE理論是對CAP的一種解決思路,包含三個思想:
- Basically Available (基本可用):分布式系統在出現故障時,允許損失部分可用性,即保證核心可用,
- Soft State(軟狀態):在一定時間內,允許出現中間狀態,比如臨時的不一致狀態,
- Eventually Consistent(最終一致性):雖然無法保證強一致性,但是在軟狀態結束后,最終達到資料一致,
2.3.解決分布式事務的思路
分布式事務最大的問題是各個子事務的一致性問題,因此可以借鑒CAP定理和BASE理論,有兩種解決思路:
-
AP模式:各子事務分別執行和提交,允許出現結果不一致,然后采用彌補措施恢復資料即可,實作最終一致,
-
CP模式:各個子事務執行后互相等待,同時提交,同時回滾,達成強一致,但事務等待程序中,處于弱可用狀態,
小知識:ES就是使用了AP模式,當有節點宕機時就會斷開此節點,并將之前在此節點備份好的資料錄入其他節點
但不管是哪一種模式,都需要在子系統事務之間互相通訊,協調事務狀態,也就是需要一個事務協調者(TC):

這里的子系統事務,稱為分支事務;有關聯的各個分支事務在一起稱為全域事務,
Seata
Seata是 2019 年 1 月份螞蟻金服和阿里巴巴共同開源的分布式事務解決方案,致力于提供高性能和簡單易用的分布式事務服務,為用戶打造一站式的分布式解決方案,
官網地址:http://seata.io/,其中的檔案、播客中提供了大量的使用說明、原始碼分析,

1. Seata的架構
Seata事務管理中有三個重要的角色:
-
TC (Transaction Coordinator) - 事務協調者:維護全域和分支事務的狀態,協調全域事務提交或回滾,
-
TM (Transaction Manager) - 事務管理器:定義全域事務的范圍、開始全域事務、提交或回滾全域事務,
-
RM (Resource Manager) - 資源管理器:管理分支事務處理的資源,與TC交談以注冊分支事務和報告分支事務的狀態,并驅動分支事務提交或回滾,
整體的架構如圖:

Seata基于上述架構提供了四種不同的分布式事務解決方案:
- XA模式:強一致性分階段事務模式,犧牲了一定的可用性,無業務侵入
- AT模式:最終一致的分階段事務模式,無業務侵入,也是Seata的默認模式
- TCC模式:最終一致的分階段事務模式,有業務侵入
- SAGA模式:長事務模式,有業務侵入
無論哪種方案,都離不開TC,也就是事務的協調者,
2. 部署TC服務
就是把Seata注冊到nacos中
2.1 下載
首先我們要下載seata-server包,地址在http??/seata.io/zh-cn/blog/download.html
當然,課前資料也準備好了:

2.2 解壓
在非中文目錄解壓縮這個zip包,其目錄結構如下:

2.3 修改配置
修改conf目錄下的registry.conf檔案:

內容如下:
registry {
# tc服務的注冊中心類,這里選擇nacos,也可以是eureka、zookeeper等
type = "nacos"
nacos {
# seata tc 服務注冊到 nacos的服務名稱,可以自定義
application = "seata-tc-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "SH"
username = "nacos"
password = "nacos"
}
}
config {
# 讀取tc服務端的組態檔的方式,這里是從nacos配置中心讀取,這樣如果tc是集群,可以共享配置
type = "nacos"
# 配置nacos地址等資訊
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
2.4 在nacos添加配置
特別注意,為了讓tc服務的集群可以共享配置,我們選擇了nacos作為統一配置中心,因此服務端組態檔seataServer.properties檔案需要在nacos中配好,
格式如下:

配置內容如下:
# 資料存盤方式,db代表資料庫
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
# 事務、日志等配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
# 客戶端與服務端傳輸方式
transport.serialization=seata
transport.compressor=none
# 關閉metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
其中的資料庫地址、用戶名、密碼都需要修改成你自己的資料庫資訊
2.5 創建資料庫表
特別注意:tc服務在管理分布式事務時,需要記錄事務相關資料到資料庫中,你需要提前創建好這些表,
新建一個名為seata的資料庫,運行課前資料提供的sql檔案:

這些表主要記錄全域事務、分支事務、全域鎖資訊:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 分支事務表
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` tinyint(4) NULL DEFAULT NULL,
`client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime(6) NULL DEFAULT NULL,
`gmt_modified` datetime(6) NULL DEFAULT NULL,
PRIMARY KEY (`branch_id`) USING BTREE,
INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- 全域事務表
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`timeout` int(11) NULL DEFAULT NULL,
`begin_time` bigint(20) NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`xid`) USING BTREE,
INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
2.6 啟動TC服務
進入bin目錄,運行其中的seata-server.bat即可:

啟動成功后,seata-server應該已經注冊到nacos注冊中心了,
打開瀏覽器,訪問nacos地址:http://localhost:8848,然后進入服務串列頁面,可以看到seata-tc-server的資訊:

3. 微服務集成Seata
以order-service為例 【每個微服務要集成seata都需要如下操作】
3.1. 引入依賴
首先,在order-service中引入依賴:
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--版本較低,1.3.0,因此排除-->
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<!--seata starter 采用1.4.2版本 下面是有父模板指定為1.4.2了,所以才寫成${seata.version}-->
<version>${seata.version}</version>
</dependency>
3.2 配置TC地址
在order-service中的application.yml中,配置TC服務資訊,通過注冊中心nacos,結合服務名稱獲取TC地址:
seata:
registry: # TC服務注冊中心的配置,微服務根據這些資訊去注冊中心獲取tc服務地址
type: nacos # 注冊中心型別 nacos
nacos:
server-addr: 127.0.0.1:8848 # nacos地址
namespace: "" # namespace,默認為空
group: DEFAULT_GROUP # 分組,默認是DEFAULT_GROUP
application: seata-tc-server # seata服務名稱
username: nacos
password: nacos
tx-service-group: seata-demo # 事務組名稱
service:
vgroup-mapping: # 事務組與cluster的映射關系
seata-demo: SH
微服務如何根據這些配置尋找TC的地址呢?
我們知道注冊到Nacos中的微服務,確定一個具體實體需要四個資訊:
- namespace:命名空間
- group:分組
- application:服務名
- cluster:集群名
以上四個資訊,在剛才的yaml檔案中都能找到:

namespace為空,就是默認的public
結合起來,TC服務的資訊就是:public@DEFAULT_GROUP@seata-tc-server@SH,這樣就能確定TC服務集群了,然后就可以去Nacos拉取對應的實體資訊了,
4. 四大事務模式
4.1.XA模式
少用,因為性能太差了
XA 規范 是 X/Open 組織定義的分布式事務處理(DTP,Distributed Transaction Processing)標準,XA 規范描述了全域的TM與區域的RM之間的介面,幾乎所有主流的資料庫都對 XA 規范提供了支持,
4.1.1.兩階段提交
XA是規范,目前主流資料庫都實作了這種規范,實作的原理都是基于兩階段提交,
正常情況:

例外情況:

一階段:
- 事務協調者通知每個事物參與者執行本地事務
- 本地事務執行完成后報告事務執行狀態給事務協調者,此時事務不提交,繼續持有資料庫鎖
二階段:
- 事務協調者基于一階段的報告來判斷下一步操作
- 如果一階段都成功,則通知所有事務參與者,提交事務
- 如果一階段任意一個參與者失敗,則通知所有事務參與者回滾事務
4.1.2.Seata的XA模型
Seata對原始的XA模式做了簡單的封裝和改造,以適應自己的事務模型,基本架構如圖:

RM一階段的作業:
? ① 注冊分支事務到TC
? ② 執行分支業務sql但不提交
? ③ 報告執行狀態到TC
TC二階段的作業:
-
TC檢測各分支事務執行狀態
a.如果都成功,通知所有RM提交事務
b.如果有失敗,通知所有RM回滾事務
RM二階段的作業:
- 接收TC指令,提交或回滾事務
4.1.3.優缺點
XA模式的優點是什么?
- 事務的強一致性,滿足ACID原則,
- 常用資料庫都支持,實作簡單,并且沒有代碼侵入
XA模式的缺點是什么?
- 因為一階段需要鎖定資料庫資源,等待二階段結束才釋放,性能較差
- 依賴關系型資料庫實作事務
4.1.4.實作XA模式
Seata的starter已經完成了XA模式的自動裝配,實作非常簡單,步驟如下:
1)修改application.yml檔案(每個參與事務的微服務),開啟XA模式:
seata:
data-source-proxy-mode: XA
2)給發起全域事務的入口方法添加@GlobalTransactional注解:
本例中是OrderServiceImpl中的create方法.

4.2.AT模式
最常用的一種,因為無代碼只需簡單配置,且性能還行
AT模式同樣是分階段提交的事務模型,不過缺彌補了XA模型中資源鎖定周期過長的缺陷,
4.2.1.Seata的AT模型
基本流程圖:

階段一RM的作業:
- 注冊分支事務
- 記錄undo-log(資料快照)
- 執行業務sql并提交
- 報告事務狀態
階段二提交時RM的作業:
- 洗掉undo-log即可
階段二回滾時RM的作業:
- 根據undo-log恢復資料到更新前
4.2.2.流程梳理
我們用一個真實的業務來梳理下AT模式的原理,
比如,現在又一個資料庫表,記錄用戶余額:
| id | money |
|---|---|
| 1 | 100 |
其中一個分支業務要執行的SQL為:
update tb_account set money = money - 10 where id = 1
AT模式下,當前分支事務執行流程如下:
一階段:
1)TM發起并注冊全域事務到TC
2)TM呼叫分支事務
3)分支事務準備執行業務SQL
4)RM攔截業務SQL,根據where條件查詢原始資料,形成快照,
{
"id": 1, "money": 100
}
5)RM執行業務SQL,提交本地事務,釋放資料庫鎖,此時 money = 90
6)RM報告本地事務狀態給TC
二階段:
1)TM通知TC事務結束
2)TC檢查分支事務狀態
? a)如果都成功,則立即洗掉快照
? b)如果有分支事務失敗,需要回滾,讀取快照資料({"id": 1, "money": 100})將快斬訓復到資料庫,此時資料庫恢復為100
流程圖:

4.2.3.AT與XA的區別
簡述AT模式與XA模式最大的區別是什么?
XA是將資料庫進行鎖定占用整個資料庫dp鎖資源直到兩階段結束,而AT是在兩階段間用全域鎖來鎖單張表(甚至可以鎖單行資料)
- XA模式一階段不提交事務,鎖定資源;AT模式一階段直接提交,不鎖定資源,
- XA模式依賴資料庫機制實作回滾;AT模式利用資料快照實作資料回滾,
- XA模式強一致;AT模式最終一致
4.2.4.臟寫問題
在多執行緒并發訪問AT模式的分布式事務時,有可能出現臟寫問題,如圖:

解決思路就是引入了全域鎖的概念,在釋放DB鎖之前,先拿到全域鎖,避免同一時刻有另外一個事務來操作當前資料,

4.2.5.優缺點
AT模式的優點:
- 一階段完成直接提交事務,釋放資料庫資源,性能比較好
- 利用全域鎖實作讀寫隔離
- 沒有代碼侵入,框架自動完成回滾和提交
AT模式的缺點:
- 兩階段之間屬于軟狀態,屬于最終一致
- 框架的快照功能會影響性能,但比XA模式要好很多
4.2.6.實作AT模式
AT模式中的快照生成、回滾等動作都是由框架自動完成,沒有任何代碼侵入,因此實作非常簡單,
只不過,AT模式需要一個表來記錄全域鎖、另一張表來記錄資料快照undo_log,
1)匯入資料庫表,記錄全域鎖
匯入課前資料提供的Sql檔案:seata-at.sql,其中lock_table匯入到TC服務關聯的資料庫,undo_log表匯入到微服務關聯的資料庫:

2)修改application.yml檔案,將事務模式修改為AT模式即可:
seata:
data-source-proxy-mode: AT # 默認就是AT
3)給發起全域事務的入口方法添加@GlobalTransactional注解:
這個注解一般是加在最外層的微服務方法(該方法里在遠程呼叫其他服務的方法)
本例中是OrderServiceImpl中的create方法.

4.3.TCC模式
在大廠中常用,因為性能最高,但代碼復雜度高
TCC模式與AT模式非常相似,每階段都是獨立事務,不同的是TCC通過人工編碼來實作資料恢復,需要實作三個方法:
-
Try:資源的檢測和預留;
-
Confirm:完成資源操作業務;要求 Try 成功 Confirm 一定要能成功,
-
Cancel:預留資源釋放,可以理解為try的反向操作,
4.3.1.流程分析
階段一Try經歷后,階段二只會經過一個Confirm 或者 Canncel
舉例,一個扣減用戶余額的業務,假設賬戶A原來余額是100,需要余額扣減30元,
- 階段一( Try ):檢查余額是否充足,如果充足則凍結金額增加30元,可用余額扣除30
初識余額:

余額充足,可以凍結:

此時,總金額 = 凍結金額 + 可用金額,數量依然是100不變,事務直接提交無需等待其它事務,
- 階段二(Confirm):假如要提交(Confirm),則凍結金額扣減30
確認可以提交,不過之前可用金額已經扣減過了,這里只要清除凍結金額就好了:

此時,總金額 = 凍結金額 + 可用金額 = 0 + 70 = 70元
- 階段二(Canncel):如果要回滾(Cancel),則凍結金額扣減30,可用余額增加30
需要回滾,那么就要釋放凍結金額,恢復可用金額:

4.3.2.Seata的TCC模型
Seata中的TCC模型依然延續之前的事務架構,如圖:

4.3.3.優缺點
TCC模式的每個階段是做什么的?
- Try:資源檢查和預留
- Confirm:業務執行和提交
- Cancel:預留資源的釋放
TCC的優點是什么?
- 一階段完成直接提交事務,釋放資料庫資源,性能好
- 相比AT模型,無需生成快照,無需使用全域鎖,性能最強
- 不依賴資料庫事務,而是依賴補償操作,可以用于非事務型資料庫
TCC的缺點是什么?
- 有代碼侵入,需要人為撰寫try、Confirm和Cancel介面,太麻煩
- 軟狀態,事務是最終一致
- 需要考慮Confirm和Cancel的失敗情況,做好冪等處理
4.3.4.事務懸掛和慷訓滾
1)慷訓滾
當某分支事務的try階段阻塞時,可能導致全域事務超時而觸發二階段的cancel操作,在未執行try操作時先執行了cancel操作,這時cancel不能做回滾,就是慷訓滾,
如圖:

執行cancel操作時,應當判斷try是否已經執行,如果尚未執行,則應該慷訓滾,
2)業務懸掛
對于已經慷訓滾的業務,之前被阻塞的try操作恢復,繼續執行try,就永遠不可能confirm或cancel ,事務一直處于中間狀態,這就是業務懸掛,
執行try操作時,應當判斷cancel是否已經執行過了,如果已經執行,應當阻止慷訓滾后的try操作,避免懸掛
4.3.5.實作TCC模式
解決慷訓滾和業務懸掛問題,必須要記錄當前事務狀態,是在try、還是cancel?
1)思路分析
這里我們定義一張表:
CREATE TABLE `account_freeze_tbl` (
`xid` varchar(128) NOT NULL,
`user_id` varchar(255) DEFAULT NULL COMMENT '用戶id',
`freeze_money` int(11) unsigned DEFAULT '0' COMMENT '凍結金額',
`state` int(1) DEFAULT NULL COMMENT '事務狀態,0:try,1:confirm,2:cancel',
PRIMARY KEY (`xid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
其中:
- xid:是全域事務id
- freeze_money:用來記錄用戶凍結金額
- state:用來記錄事務狀態
那此時,我們的業務開怎么做呢?
- Try業務:
- 記錄凍結金額和事務狀態到account_freeze表
- 扣減account表可用金額
- Confirm業務
- 根據xid洗掉account_freeze表的凍結記錄
- Cancel業務
- 修改account_freeze表,凍結金額為0,state為2
- 修改account表,恢復可用金額
- 如何判斷是否慷訓滾?
- cancel業務中,根據xid查詢account_freeze,如果為null則說明try還沒做,需要慷訓滾
- 如何避免業務懸掛?
- try業務中,根據xid查詢account_freeze ,如果已經存在則證明Cancel已經執行,拒絕執行try業務
接下來,我們改造account-service,利用TCC實作余額扣減功能,
2)宣告TCC介面
TCC的Try、Confirm、Cancel方法都需要在介面中基于注解來宣告,
我們在account-service專案中的cn.itcast.account.service包中新建一個介面,宣告TCC三個介面:
package cn.itcast.account.service;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
@LocalTCC
public interface AccountTCCService {
@TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "money")int money);
boolean confirm(BusinessActionContext ctx);
boolean cancel(BusinessActionContext ctx);
}
3)撰寫實作類
在account-service服務中的cn.itcast.account.service.impl包下新建一個類,實作TCC業務:
記得要在Try資源預留的方法前加 事務@Transactional
package cn.itcast.account.service.impl;
import cn.itcast.account.entity.AccountFreeze;
import cn.itcast.account.mapper.AccountFreezeMapper;
import cn.itcast.account.mapper.AccountMapper;
import cn.itcast.account.service.AccountTCCService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountFreezeMapper freezeMapper;
@Override
@Transactional
public void deduct(String userId, int money) {
// 0.獲取事務id
String xid = RootContext.getXID();
// 1.扣減可用余額
accountMapper.deduct(userId, money);
// 2.記錄凍結金額,事務狀態
AccountFreeze freeze = new AccountFreeze();
freeze.setUserId(userId);
freeze.setFreezeMoney(money);
freeze.setState(AccountFreeze.State.TRY);
freeze.setXid(xid);
freezeMapper.insert(freeze);
}
@Override
public boolean confirm(BusinessActionContext ctx) {
// 1.獲取事務id
String xid = ctx.getXid();
// 2.根據id洗掉凍結記錄
int count = freezeMapper.deleteById(xid);
return count == 1;
}
@Override
public boolean cancel(BusinessActionContext ctx) {
// 0.查詢凍結記錄
String xid = ctx.getXid();
AccountFreeze freeze = freezeMapper.selectById(xid);
// 1.恢復可用余額
accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
// 2.將凍結金額清零,狀態改為CANCEL
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
int count = freezeMapper.updateById(freeze);
return count == 1;
}
}
4.4 SAGA模式
平時中也很少用,一般是在處理之前遺留的業務代碼時使用
Saga 模式是 Seata 即將開源的長事務解決方案,將由螞蟻金服主要貢獻,
其理論基礎是Hector & Kenneth 在1987年發表的論文Sagas,
Seata官網對于Saga的指南:https://seata.io/zh-cn/docs/user/saga.html
4.4.1 原理
在 Saga 模式下,分布式事務內有多個參與者,每一個參與者都是一個沖正補償服務,需要用戶根據業務場景實作其正向操作和逆向回滾操作,
分布式事務執行程序中,依次執行各參與者的正向操作,如果所有正向操作均執行成功,那么分布式事務提交,如果任何一個正向操作執行失敗,那么分布式事務會去退回去執行前面各參與者的逆向回滾操作,回滾已提交的參與者,使分布式事務回到初始狀態,

Saga也分為兩個階段:
- 一階段:直接提交本地事務
- 二階段:成功則什么都不做;失敗則通過撰寫補償業務來回滾
4.4.2.優缺點
優點:
- 事務參與者可以基于事件驅動實作異步呼叫,吞吐高
- 一階段直接提交事務,無鎖,性能好
- 不用撰寫TCC中的三個階段,實作簡單
缺點:
- 軟狀態持續時間不確定,時效性差
- 沒有鎖,沒有事務隔離,會有臟寫
4.5.四種模式對比
我們從以下幾個方面來對比四種實作:
- 一致性:能否保證事務的一致性?強一致還是最終一致?
- 隔離性:事務之間的隔離性如何?
- 代碼侵入:是否需要對業務代碼改造?
- 性能:有無性能損耗?
- 場景:常見的業務場景

5. TC服務集群
Seata的TC服務作為分布式事務核心,一定要保證集群的高可用性,
5.1.高可用架構模型
搭建TC服務集群非常簡單,啟動多個TC服務,注冊到nacos即可,
但集群并不能確保100%安全,萬一集群所在機房故障怎么辦?所以如果要求較高,一般都會做異地多機房容災,
比如一個TC集群在上海,另一個TC集群在杭州:

微服務基于事務組(tx-service-group)與TC集群的映射關系,來查找當前應該使用哪個TC集群,當SH集群故障時,只需要將vgroup-mapping中的映射關系改成HZ,則所有微服務就會切換到HZ的TC集群了,
5.2.實作高可用
TC服務的高可用和異地容災
5.2.1 模擬異地容災的TC集群
計劃啟動兩臺seata的tc服務節點:
| 節點名稱 | ip地址 | 埠號 | 集群名稱 |
|---|---|---|---|
| seata | 127.0.0.1 | 8091 | SH |
| seata2 | 127.0.0.1 | 8092 | HZ |
之前我們已經啟動了一臺seata服務,埠是8091,集群名為SH,
現在,將seata目錄復制一份,起名為seata2
修改組態檔seata2/conf/registry.conf內容如下:
registry {
# tc服務的注冊中心類,這里選擇nacos,也可以是eureka、zookeeper等
type = "nacos"
nacos {
# seata tc 服務注冊到 nacos的服務名稱,可以自定義
application = "seata-tc-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "HZ"
username = "nacos"
password = "nacos"
}
}
config {
# 讀取tc服務端的組態檔的方式,這里是從nacos配置中心讀取,這樣如果tc是集群,可以共享配置
type = "nacos"
# 配置nacos地址等資訊
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
進入seata2/bin目錄,然后運行命令:
seata-server.bat -p 8092
打開nacos控制臺,查看服務串列:

點進詳情查看:

5.2.2 將事務組映射配置到nacos
接下來,我們需要將tx-service-group與cluster的映射關系都配置到nacos配置中心,
新建一個配置:

配置的內容如下:
# 事務組映射關系
service.vgroupMapping.seata-demo=SH
service.enableDegrade=false
service.disableGlobalTransaction=false
# 與TC服務的通信配置
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
# RM配置
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
# TM配置
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
# undo日志配置
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
client.log.exceptionRate=100
5.2.3 微服務讀取nacos配置
接下來,需要修改每一個微服務的application.yml檔案,讓微服務讀取nacos中的client.properties檔案:
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
group: SEATA_GROUP
data-id: client.properties
重啟微服務,現在微服務到底是連接tc的SH集群,還是tc的HZ集群,都統一由nacos的client.properties來決定了,
本文來自博客園,作者:不吃紫菜,遵循CC 4.0 BY-SA著作權協議,轉載請附上原文出處鏈接:https://www.cnblogs.com/buchizicai/p/17093759.html及本宣告,
本文著作權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/543853.html
標籤:Java
下一篇:大數處理方案
