使用AT模式的前提條件
AT模式支援的資料庫有:MySQL、Oracle、PostgreSQL、 TiDB、MariaDB。
整體機制為兩階段提交協定的演變
寫隔離
讀隔離
工作機制
一階段
解析 SQL:得到 SQL 的型別(UPDATE),表(product),條件(where name = 'TXC')等相關的資訊。select id, name, since from product where name = 'TXC';
查詢前映象:根據解析得到的條件資訊,生成查詢語句,定位資料。前映象為
執行業務 SQL:更新這條記錄的 name 為 'GTS'。
查詢後映象:根據前映象的結果,通過 主鍵 定位資料。select id, name, since from product where id = 1;後映象為
插入回滾紀錄檔:把前後映象資料以及業務 SQL 相關的資訊組成一條回滾紀錄檔記錄,插入到 UNDO_LOG
表中。
提交前,向 TC 註冊分支:申請 product
表中,主鍵值等於 1 的記錄的 全域性鎖 。
本地事務提交:業務資料的更新和前面步驟中生成的 UNDO LOG 一併提交。
將本地事務提交的結果上報給 TC。
二階段
我們重新修改下上一小節seata,專門定義一個group: SEATA_GROUP,然後重新啟動seata
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${user.home}/logs/seata
extend:
logstash-appender:
destination: 127.0.0.1:4560
kafka-appender:
bootstrap-servers: 127.0.0.1:9092
topic: logback_to_logstash
console:
user:
username: seata
password: seata
seata:
config:
# support: nacos 、 consul 、 apollo 、 zk 、 etcd3
type: nacos
nacos:
server-addr: 192.168.50.95:8848
namespace: a2b1a5b7-d0bc-48e8-ab65-04695e61db01
group: SEATA_GROUP
username: nacos
password: nacos
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key: ""
#secret-key: ""
data-id: seataServer.properties
registry:
# support: nacos 、 eureka 、 redis 、 zk 、 consul 、 etcd3 、 sofa
type: nacos
preferred-networks: 30.240.*
nacos:
application: seata-server
server-addr: 192.168.50.95:8848
group: SEATA_GROUP
namespace: a2b1a5b7-d0bc-48e8-ab65-04695e61db01
#cluster: default
username: nacos
password: nacos
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key: ""
#secret-key: ""
server:
service-port: 8091 #If not configured, the default is '${server.port} + 1000'
max-commit-retry-timeout: -1
max-rollback-retry-timeout: -1
rollback-retry-timeout-unlock-enable: false
enableCheckAuth: true
retryDeadThreshold: 130000
xaerNotaRetryTimeout: 60000
recovery:
handle-all-session-period: 1000
undo:
log-save-days: 7
log-delete-period: 86400000
session:
branch-async-queue-size: 5000 #branch async remove queue size
enable-branch-async-remove: false #enable to asynchronous remove branchSession
metrics:
enabled: false
registry-type: compact
exporter-list: prometheus
exporter-prometheus-port: 9898
transport:
rpc-tc-request-timeout: 30000
enable-tc-server-batch-send-response: false
shutdown:
wait: 3
thread-factory:
boss-thread-prefix: NettyBoss
worker-thread-prefix: NettyServerNIOWorker
boss-thread-size: 1
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
我們建立訂單和庫存兩個微服務來測試AT事務模式
SEATA AT 模式需要 UNDO_LOG
表,UNDO_LOG
表語句在原始碼目錄下script\client\at\db\mysql.sql裡
建立訂單資料庫storage,建立庫存業務表storage_tbl和undo_log
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
建立庫存資料庫order,建立訂單業務表order_tbl
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
引入seata依賴,由於庫存微服務和訂單微服務都參照ecom-commons,所以在ecom-commons的pom中新增如下seata依賴,注意這裡web容器建議使用tomcat,原來我使用的是undertow,但是執行分散式事務報錯所以最後註釋使用undertow
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2021.1</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
由於使用最新版本seata-all依賴需要class位元組碼版本為55也即是JDK11,因此演示專案使用的JDK11,庫存微服務和訂單微服務微服務的啟動組態檔中加入如下設定
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.50.95:8848
group: ecom-group
namespace: a2b1a5b7-d0bc-48e8-ab65-04695e61db01
username: nacos
password: nacos
# seata 設定
seata:
# 使用哪個事務組
tx-service-group: default_tx_group
service:
vgroup-mapping:
default_tx_group: default # TC 叢集(必須與seata-server保持一致)
enabled: true
config:
type: nacos
nacos:
username: nacos
password: nacos
# 讀取的設定分組
group: SEATA_GROUP
server-addr: 192.168.50.95:8848
namespace: a2b1a5b7-d0bc-48e8-ab65-04695e61db01
dataId: seataServer.properties
# 註冊中心設定
registry:
type: nacos
nacos:
# SEATA服務中心的微服務名,此處與伺服器端保持一致
application: seata-server
server-addr: 192.168.50.95:8848
username: nacos
password: nacos
group: SEATA_GROUP
namespace: a2b1a5b7-d0bc-48e8-ab65-04695e61db01
訂單控制器提供訂單建立介面
庫存控制器提供扣減庫存介面
通過在訂單的實現類上新增一個@GlobalTransactional註解實現分散式事務
目前是一個正常場景,存取http://localhost:4070/order/create/1000/1001/2 返回成功
庫存成功扣減2個
訂單表也增加一條記錄
庫存實現類增加異常程式碼
存取http://localhost:4070/order/create/1000/1001/2 返回失敗
庫存表沒有減庫存,訂單表也沒有新的訂單記錄
在訂單的實現類實現類建立訂單方法中去掉@GlobalTransactional後再次存取,這時候就沒有分散式事務支援,再次存取後訂單表有新增新的訂單記錄,但是庫存表沒有減少,至此AT事務模式的範例完整結束。
在訂單實現類create方法和庫存實現類的deduct方法入口加上埠,重新啟動兩個微服務
檢視seata server的執行紀錄檔,出現庫存和訂單微服務已經註冊到seata中
檢視nacos服務列表,由於我啟動兩個訂單微服務
存取http://localhost:4070/order/create/1000/1001/2 ,進入第一個斷點,注意整個分散式事務不要超過超時時間預設為60秒,可以設定,我是快速截圖儲存
當斷點走完庫存返回檢視幾張表如下,鎖表記錄
分支表記錄
庫存表undo_log表資訊如下
rollback_info資訊,核心是beforeImage和afterImage
{"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"192.168.5.52:8091:9043512942211305791","branchId":9043512942211305793,"sqlUndoLogs":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.undo.SQLUndoLog","sqlType":"UPDATE","tableName":"storage_tbl","beforeImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"storage_tbl","rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"id","keyType":"PRIMARY_KEY","type":4,"value":1},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"count","keyType":"NULL","type":4,"value":760}]]}]]},"afterImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"storage_tbl","rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"id","keyType":"PRIMARY_KEY","type":4,"value":1},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"count","keyType":"NULL","type":4,"value":758}]]}]]}}]]}
訂單表的undo_log表資訊如下
訂單表的undolog的rollback_info資訊,核心是beforeImage和afterImage
{"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"192.168.5.52:8091:9043512942211305857","branchId":9043512942211305887,"sqlUndoLogs":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.undo.SQLUndoLog","sqlType":"INSERT","tableName":"order_tbl","beforeImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords","tableName":"order_tbl","rows":["java.util.ArrayList",[]]},"afterImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"order_tbl","rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"commodity_code","keyType":"NULL","type":12,"value":"1001"},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"count","keyType":"NULL","type":4,"value":2},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"id","keyType":"PRIMARY_KEY","type":4,"value":9},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"money","keyType":"NULL","type":4,"value":20},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"user_id","keyType":"NULL","type":12,"value":"1000"}]]}]]}}]]}
剛性事務指的是分散式事務要像本地式事務⼀樣,具備資料強⼀致性。從 CAP 來看就是要達到 CP 狀態。常見的剛性事務方案有:XA 協定(2PC、JTA、JTS)、3PC。由於剛性事務同步阻塞,處理效率低,不適合⼤型⽹站分散式場景。
XA 規範是 X/Open 組織定義的分散式事務處理 (DTP,Distributed Transaction Processing) 標準。規範描述了全域性的事務管理器與區域性的資源管理器之間的介面。對於 XA 模型,包含三個角色:
XA 規範的目的是允許多個資源 (如資料庫,應用伺服器,訊息佇列等) 在同一事務中存取,這樣可以使 ACID 屬性跨越應用程式而保持有效。XA 規範使用兩階段提交 (2PC,Two-Phase Commit) 協定來保證所有資源同時提交或回滾任何特定的事務。目前知名的資料庫,如 Oracle, DB2, mysql 等,都是實現了 XA 介面的,都可以作為 RM。
XA 規範定義了 (全域性) 事務管理器 (Transaction Manager) 和 (區域性) 資源管理器 (Resource Manager) 之間的介面。XA 介面是雙向的系統介面,在事務管理器 (Transaction Manager) 以及一個或多個資源管理器 (Resource Manager) 之間形成通訊橋樑。
XA 之所以需要引入事務管理器是因為,在分散式系統中,從理論上講,兩臺機器理論上無法達到一致的狀態,需要引入一個單點進行協調。事務管理器控制著全域性事務,管理事務生命週期,並協調資源。資源管理器負責控制和管理實際資源 (如資料庫或 JMS 佇列)
XA 是資料庫的分散式事務,強一致性,在整個過程中,資料一張鎖住狀態,即從 prepare 到 commit、rollback 的整個過程中,TM 一直把持折資料庫的鎖,如果有其他人要修改資料庫的該條資料,就必須等待鎖的釋放,存在⻓事務⻛險。
Seata 已經支援了三大事務模式:AT、TCC、SAGA,這三個都是補償型事務,補償型事務處理你機制構建在 事務資源之上(要麼中介軟體層面,要麼應用層),事務資源本身對於分散式的事務是無感知的,這種對於分散式事務的無感知存在有一個根本性的問題,無法做到真正的全域性一致性。
例如一個庫存記錄,在補償型事務處理過程中,用80扣減為60,這個時候倉庫管理員查詢資料結果,看到的是60,之後因為異常回滾,庫存回滾到原來的80,那麼這個時候庫存管理員看到的60,其實就是髒資料,而這個中間狀態就是補償型事務存在的髒資料。
和補償型事務不同,XA協定要求事務資源 本身提供對規範和協定的支援,因為事務資源感知並參與分散式事務處理過程中,所以事務資源可以保證從任意視角對資料的存取有效隔離性,滿足全域性資料的一致性。
與 補償型 不同,XA 協定 要求 事務資源 本身提供對規範和協定的支援。因為 事務資源 感知並參與分散式事務處理過程,所以 事務資源(如資料庫)可以保障從任意視角對資料的存取有效隔離,滿足全域性資料一致性。比如,剛才提到的庫存更新場景,XA 事務處理過程中,中間狀態資料庫存 50 由資料庫本身保證,是不會被倉庫管理員的查詢統計看到的。除了 全域性一致性 這個根本性的價值外,支援 XA 還有如下幾個方面的好處:
前提
整體機制
執行階段
完成階段
XA模式只支援實現了XA協定的資料庫。Seata支援MySQL、Oracle、PostgreSQL和MariaDB。
XA事務模式使用和AT事務模式使用基本沒有區別,我們還是用前面的例子,只需要在庫存和訂單微服務的組態檔中新增data-source-proxy-mode為XA,其他不變
seata:
data-source-proxy-mode: XA
XA模式不需要undo_log表,演示時我們可以先把訂單和庫存資料庫的undo_log表刪掉,為了看下xid,增加一個xid列印紀錄檔,啟動訂單和庫存微服務,存取http://localhost:4070/order/create/2000/1001/5 ,訂單微服務紀錄檔列印如下
庫存微服務資訊紀錄檔資訊也使用XA模式,全面xid事務deduct xid:192.168.5.52:8091:9043512942211306753和訂單相同
檢視訂單資料庫已經新增訂單,庫存已經建庫存,這個是正常場景。
在訂單中新增一個異常,http://localhost:4070/order/create/2000/1001/5 顯示異常,後臺紀錄檔列印異常的提示
庫存微服務中紀錄檔已經進行回滾,檢視庫存表沒有減庫存,訂單也沒有建立
一個分散式的全域性事務,整體是 兩階段提交 的模型。全域性事務是由若干分支事務組成的,分支事務要滿足 兩階段提交 的模型要求,即需要每個分支事務都具備自己的:
根據兩階段行為模式的不同,我們將分支事務劃分為 Automatic (Branch) Transaction Mode 和 Manual (Branch) Transaction Mode.
TCC模式不依賴資料來源(1.4.2版本及之前),1.4.2版本之後增加了TCC防懸掛措施,需要資料來源支援。
AT模式是基於 支援本地 ACID 事務 的 關係型資料庫
TCC 模式則不依賴於底層資料資源的事務支援,支援把 自定義 的分支事務納入到全域性事務的管理中。
案例可以參考https://github.com/seata/seata-samples ,後續再補充實戰案例
Saga模式是SEATA提供的長事務解決方案,在Saga模式中,業務流程中每個參與者都提交本地事務,當出現某一個參與者失敗則補償前面已經成功的參與者,一階段正向服務和二階段補償服務都由業務開發實現。
Seata基於狀態機引擎的 Saga 實現:
目前SEATA提供的Saga模式是基於狀態機引擎來實現的,機制是:
注意: 異常發生時是否進行補償也可由使用者自定義決定
Seata Saga 提供了一個視覺化的狀態機設計器方便使用者使用,程式碼和執行指南請參考: https://github.com/seata/seata/tree/develop/saga/seata-saga-statemachine-designer,官方案例可以參考https://github.com/seata/seata-samples ,後續再充實戰案例
**本人部落格網站 **IT小神 www.itxiaoshen.com