概述
Seat是螞蟻金服和阿里巴巴聯合推出的一個開源的分佈式事務框架,在阿里雲商用的叫做GTS。
項目地址:https://github.com/longxiaonan/springcloud-demo
官網:http://seata.io/zh-cn/index.html
一個XID和三個概念:
- Transaction ID (XID) : 全局唯一的事務ID
- Transaction Coordinator (TC) : 事務協調器,維護全局事務的運行狀態
- Transaction Manager (TM):控制全局事務的邊界,負責開啟一個全局事務
- Resource Manager (RM):控制分支事務,負責分支註冊、狀態彙報,並接受TC的提交或者回滾操作
事務分為全局事務和本地事務。
全局事務通過XID(全局唯一事務id)來標識。分支通過分支id和資源id來標識,標籤每個分支事務都標有XID來標識是屬於哪個全局事務。
事務流程如下:
- TM向TC申請開啟一個全局事務,全局事務創建成功並且生成一個全局唯一的XID;
- XID在微服務調用鏈路的上下文中傳播;
- RM向TC註冊分支事務,彙報事務資源準備狀態,將其納入XID對應全局事務的管轄;
- RM執行業務邏輯;
- TM結束分佈式事務,事務一階段結束,通知TC針對XID的全局提交或回滾決議;
- TC彙總事務信息,決定分佈式事務是提交還是回滾;
- TC調度XID下管轄的RM的分支事務完成提交或者回滾請求,事務二階段結束。
TC,TM和RM對應到實際服務節點
全局事務需要在服務調用方的service上開啟,服務調用方就是TM,其他被調用方就是RM。
兩段提交的詳細過程
一階段加載:
二階段提交
二階段提交失敗,則進行回滾
下載seata
下載地址:http://seata.io/zh-cn/blog/download.html
本文采用的是 seata 1.3.0 (2020-07-14)
和springCloudAlibaba版本支持說明:
https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E
添加seata依賴(建議單選)
- 依賴seata-all
- 依賴seata-spring-boot-starter,支持yml、properties配置(.conf可刪除),內部已依賴seata-all
- 依賴spring-cloud-alibaba-seata,內部集成了seata,並實現了xid傳遞
因為是微服務方式,故添加依賴:
- spring-cloud-starter-alibaba-seata推薦依賴配置方式
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>最新版</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.1.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
文件配置
修改registry.type、config.type
文件在下載的seata包下
啟動包: seata-->conf-->file.conf,修改store.mode="db或者redis"
源碼: 根目錄-->seata-server-->resources-->file.conf,修改store.mode="db或者redis"
啟動包: seata-->conf-->file.conf,修改store.db或store.redis相關屬性。
源碼: 根目錄-->seata-server-->resources-->file.conf,修改store.db或store.redis相關屬性。
registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "file"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
...
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = ""
password = ""
}
}
改成
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "localhost:8848"
namespace = "public"
cluster = "default"
username = ""
password = ""
}
...
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = "public"
group = "SEATA_GROUP"
username = ""
password = ""
}
}
store.mode 和 對應的nacos和db連接配置
file.conf
注意數據源類型,比如springboot的默認數據源是hikari而不是druid
store {
## store mode: file、db、redis
mode = "file"
...
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"
user = "mysql"
password = "mysql"
}
...
}
修改為:
store {
## store mode: file、db、redis
mode = "db"
...
## 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"
user = "root"
password = "123456"
}
...
}
配置nacos-config.txt
文件地址:https://github.com/seata/seata/blob/1.3.0/script/config-center/config.txt
官網seata-server-0.9.0的conf目錄下有該文件,官網seata-server-0.9.0的conf目錄下有該文件,後面的版本無該文件需要手動下載執行。
修改為自己的服務組名,各個微服務之間使用相同的服務組名,務必保持一致!
service.vgroupMapping.my_test_tx_group=default
service.vgroupMapping.my_test_tx_group1=default
service.vgroupMapping.my_test_tx_group2=default
service.vgroupMapping.my_test_tx_group3=default
配置seata服務器mysql鏈接信息,注意數據源類型,比如springboot的默認數據源是hikari而不是druid
store.mode=db
...
store.db.datasource=druid
store.db.dbType=mysql
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true
store.db.user=root
store.db.password=123456
執行nacos-config.sh腳本
腳本地址:https://github.com/seata/seata/blob/1.3.0/script/config-center/nacos/nacos-config.sh
官網seata-server-0.9.0的conf目錄下有該文件,後面的版本無該文件需要手動下載執行。
如果本地是windows,使用git工具git bash執行nacos-config.sh腳本:
sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -u nacos -w nacos
執行完成後nacos會新增seata配置。
需要注意config.txt中目錄的對應關係,否則可能提示finished,其實未執行成功!
$ sh nacos-config.sh -h localhost -p 8848
set nacosAddr=localhost:8848
set group=SEATA_GROUP
cat: /d/soft/config.txt: No such file or directory
=========================================================================
Complete initialization parameters, total-count:0 , failure-count:0
=========================================================================
Init nacos config finished, please start seata-server.
Seata Server需要依賴的表
表的地址:https://github.com/seata/seata/blob/develop/script/server/db/mysql.sql
新建數據庫seata, 創建如下三個表,用於seata服務, 0.0.9版本才有這個文件1.0.0版本後需要手動添加。
-- the table to store GlobalSession data
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `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`)
);
-- the table to store BranchSession data
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT ,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256) ,
`lock_key` VARCHAR(128) ,
`branch_type` VARCHAR(8) ,
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
);
-- the table to store lock data
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` LONG ,
`branch_id` LONG,
`resource_id` VARCHAR(256) ,
`table_name` VARCHAR(32) ,
`pk` VARCHAR(36) ,
`gmt_create` DATETIME ,
`gmt_modified` DATETIME,
PRIMARY KEY(`row_key`)
);
AT模式下每個業務數據庫需要創建undo_log表,用於seata記錄分支的回滾信息
表的地址:https://github.com/seata/seata/blob/1.3.0/script/client/at/db/mysql.sql
-- 注意此處0.3.0+ 增加唯一索引 ux_undo_log
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,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
運行 Seata-server
Linux/Unix/Mac
sh seata-server.sh -p $LISTEN_PORT -m $STORE_MODE -h $IP(此參數可選)
Windows
cmd seata-server.bat -p $LISTEN_PORT -m $STORE_MODE -h $IP(此參數可選)
$LISTEN_PORT: Seata-Server 服務端口
$STORE_MODE: 事務操作記錄存儲模式:file、db
$IP(可選參數): 用於多 IP 環境下指定 Seata-Server 註冊服務的IP,配置自己的ip即可
測試的時候 直接雙擊運行seata-server.bat 即可。
# linux
nohup ./seata-server.sh -h 192.168.1.4 -p 8092 &
# windows cmd
seata-server.bat -m file -h 192.168.1.4 -p 8092
用例
參考官網中用戶購買商品的業務邏輯。整個業務邏輯由3個微服務提供支持:
- 存儲服務:扣除給定商品的存儲數量。
- 訂單服務:根據購買請求創建訂單。
- 帳戶服務:借記用戶帳戶的餘額。
項目地址:
https://github.com/longxiaonan/springcloud-demo
添加依賴:
<!-- 分佈式事務seata包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.1.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
配置seata:
seata:
enabled: true
application-id: ${spring.application.name}
txServiceGroup: my_test_tx_group
# 是否開啟數據源自動代理 如果不開啟設置為false
enable-auto-data-source-proxy: true
registry:
type: nacos
nacos:
application: seata-server
server-addr: localhost:8848
namespace:
# userName: "nacos"
# password: "nacos"
config:
type: nacos
nacos:
namespace:
serverAddr: localhost:8848
group: SEATA_GROUP
# userName: "nacos"
# password: "nacos"
在代碼中通過註解開啟:
@GlobalTransactional(rollbackFor = Exception.class)
請求邏輯
commint測試接口:http://127.0.0.1:8008/purchase/commit接口是可以正常執行完成的方法
rollback測試接口:http://127.0.0.1:8008/purchase/rollback接口是會發生異常並正常回滾的方法
測試
當未開啟seata服務rollback測試:
未開啟seata服務,啟動業務服務節點, 業務節點的console log如下提示:
2020-07-25 10:59:50.492 ERROR 11284 --- [eoutChecker_1_1] i.s.c.r.netty.NettyClientChannelManager : no available service 'default' found, please make sure registry config correct
2020-07-25 10:59:50.654 ERROR 11284 --- [eoutChecker_2_1] i.s.c.r.netty.NettyClientChannelManager : no available service 'default' found, please make sure registry config correct
此時,訪問rollback測試接口,controller不能進入加了@GlobalTransitional的service,無任何服務被執行和調用。console log如下報錯:
io.seata.common.exception.FrameworkException: No available service
at io.seata.core.rpc.netty.AbstractNettyRemotingClient.loadBalance(AbstractNettyRemotingClient.java:257)
at io.seata.core.rpc.netty.AbstractNettyRemotingClient.sendSyncRequest(AbstractNettyRemotingClient.java:133)
at io.seata.tm.DefaultTransactionManager.syncCall(DefaultTransactionManager.java:95)
at io.seata.tm.DefaultTransactionManager.begin(DefaultTransactionManager.java:53)
at io.seata.tm.api.DefaultGlobalTransaction.begin(DefaultGlobalTransaction.java:104)
at io.seata.tm.api.TransactionalTemplate.beginTransaction(TransactionalTemplate.java:175)
開啟seata服務rollback測試
雙擊 `` 啟動seata服務
業務服務console提示如下log,說明註冊成功
2020-07-25 11:08:31.599 INFO 11284 --- [eoutChecker_1_1] i.s.c.rpc.netty.TmNettyRemotingClient : register TM success. client version:1.3.0, server version:1.3.0,channel:[id: 0xdd694462, L:/169.254.249.134:3959 - R:/169.254.249.134:8091]
2020-07-25 11:08:31.618 INFO 11284 --- [eoutChecker_2_1] i.s.c.rpc.netty.RmNettyRemotingClient : register RM success. client version:1.3.0, server version:1.3.0,channel:[id: 0x5ece5f24, L:/169.254.249.134:3960 - R:/169.254.249.134:8091]
2020-07-25 11:08:31.624 INFO 11284 --- [eoutChecker_2_1] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 264 ms, version:1.3.0,role:RMROLE,channel:[id: 0x5ece5f24, L:/169.254.249.134:3960 - R:/169.254.249.134:8091]
2020-07-25 11:08:31.624 INFO 11284 --- [eoutChecker_1_1] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 263 ms, version:1.3.0,role:TMROLE,channel:[id: 0xdd694462, L:/169.254.249.134:3959 - R:/169.254.249.134:8091]
rollback接口在執行到account服務的時候會拋異常:
java.lang.RuntimeException: account branch exception
at com.javasea.account.service.AccountService.debit(AccountService.java:34) ~[classes/:na]
at com.javasea.account.service.AccountService$$FastClassBySpringCGLIB$$e3edd550.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
order服務作為account服務的調用方,會知道account服務發生了異常:
feign.FeignException$InternalServerError: [500] during [GET] to [http://account-service/debit/1002/10] [AccountFeignClient#debit(String,Integer)]: [{"timestamp":"2020-07-25T03:36:18.494+0000","status":500,"error":"Internal Server Error","message":"account branch exception","path":"/debit/1002/10"}]
at feign.FeignException.serverErrorStatus(FeignException.java:231) ~[feign-core-10.7.4.jar:na]
at feign.FeignException.errorStatus(FeignException.java:180) ~[feign-core-10.7.4.jar:na]
at feign.FeignException.errorStatus(FeignException.java:169) ~[feign-core-10.7.4.jar:na]
at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:92) ~[feign-core-10.7.4.jar:na]
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:156) ~[feign-core-10.7.4.jar:na]
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:80) ~[feign-core-10.7.4.jar:na]
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:100) ~[feign-core-10.7.4.jar:na]
at com.sun.proxy.$Proxy89.debit(Unknown Source) ~[na:na]
at com.javasea.order.service.OrderService.create(OrderService.java:38) ~[classes/:na]
business服務log如下:
2020-07-25 11:36:18.288 INFO 11284 --- [nio-8008-exec-4] i.seata.tm.api.DefaultGlobalTransaction : Begin new global transaction [169.254.249.134:8091:30253423644925952]
2020-07-25 11:36:18.289 INFO 11284 --- [nio-8008-exec-4] c.j.business.service.BusinessService : 開始全局事務,XID = 169.254.249.134:8091:30253423644925952
2020-07-25 11:36:18.627 INFO 11284 --- [nio-8008-exec-4] i.seata.tm.api.DefaultGlobalTransaction : [169.254.249.134:8091:30253423644925952] rollback status: Rollbacked
feign.FeignException$InternalServerError: [500] during [GET] to [http://order-service/order/1002/2001/1] [OrderFeignClient#create(String,String,Integer)]: [{"timestamp":"2020-07-25T03:36:18.530+0000","status":500,"error":"Internal Server Error","message":"[500] during [GET] to [http://account-service/debit/1002/10] [AccountFeignClient#debit(String,Intege... (405 bytes)]
at feign.FeignException.serverErrorStatus(FeignException.java:231)
at feign.FeignException.errorStatus(FeignException.java:180)
at feign.FeignException.errorStatus(FeignException.java:169)
storage服務的扣減庫存已經提交,因為全局事務沒成功,故觸發回滾:
2020-07-25 11:36:18.303 INFO 5924 --- [io-8010-exec-10] c.j.storage.service.StorageService : 開始分支事務,XID = 169.254.249.134:8091:30253423644925952
2020-07-25 11:36:18.400 WARN 5924 --- [io-8010-exec-10] c.a.c.seata.web.SeataHandlerInterceptor : xid in change during RPC from 169.254.249.134:8091:30253423644925952 to null
2020-07-25 11:36:18.543 INFO 5924 --- [ch_RMROLE_1_7_8] i.s.c.r.p.c.RmBranchRollbackProcessor : rm handle branch rollback process:xid=169.254.249.134:8091:30253423644925952,branchId=30253423821086720,branchType=AT,resourceId=jdbc:mysql://localhost:3306/seata_storage,applicationData=null
2020-07-25 11:36:18.544 INFO 5924 --- [ch_RMROLE_1_7_8] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 169.254.249.134:8091:30253423644925952 30253423821086720 jdbc:mysql://localhost:3306/seata_storage
2020-07-25 11:36:18.618 INFO 5924 --- [ch_RMROLE_1_7_8] i.s.r.d.undo.AbstractUndoLogManager : xid 169.254.249.134:8091:30253423644925952 branch 30253423821086720, undo_log deleted with GlobalFinished
2020-07-25 11:36:18.619 INFO 5924 --- [ch_RMROLE_1_7_8] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked
遇到的問題:
console一直提示如下error log:
00:46:38.070 ERROR 10652 --- [imeoutChecker_2] i.s.c.r.n.NettyClientChannelManager - no available service 'default' found, please make sure registry config correct
00:46:47.729 ERROR 10652 --- [imeoutChecker_1] i.s.c.r.n.NettyClientChannelManager - no available service 'default' found, please make sure registry config correct
一直百事不得其解。後面突然發現會不會是jar包問題。
查看在父項目的pom配置:
<!-- 分佈式事務seata包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.1.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
到本項目分析pom.xml的依賴關係,發現jia包竟然是1.1.0的,在父項目不是引入的1.3.0嗎?
去文檔中查找發現1.1.0是默認 spring-cloud-starter-alibaba-seata 2.2.1.RELEASE 內嵌的,也就是pom依賴關係基礎問題。
先不管pom依賴的繼承問題。將pom依賴分別添加到子項目即可。
然後重啟,RM註冊成功:
00:49:17.541 INFO 13260 --- [eoutChecker_1_1] i.s.c.rpc.netty.NettyPoolableFactory - NettyPool create channel to transactionRole:TMROLE,address:169.254.249.134:8091,msg:< RegisterTMRequest{applicationId='lmwy-flow-longxiaonan', transactionServiceGroup='my_test_tx_group'} >
00:49:17.557 INFO 13260 --- [eoutChecker_1_1] i.s.c.r.netty.TmNettyRemotingClient - register TM success. client version:1.3.0, server version:1.3.0,channel:[id: 0x1d35034a, L:/169.254.249.134:9977 - R:/169.254.249.134:8091]
00:49:17.557 INFO 13260 --- [eoutChecker_1_1] i.s.c.rpc.netty.NettyPoolableFactory - register success, cost 8 ms, version:1.3.0,role:TMROLE,channel:[id: 0x1d35034a, L:/169.254.249.134:9977 - R:/169.254.249.134:8091]
00:49:32.077 INFO 13260 --- [.12.11.240_8848] c.a.n.c.config.impl.ClientWorker - get changedGroupKeys:[]
00:49:38.880 INFO 13260 --- [.12.11.240_8848] c.a.n.c.config.impl.ClientWorker - get changedGroupKeys:[]
00:50:01.684 INFO 13260 --- [.12.11.240_8848] c.a.n.c.config.impl.ClientWorker - get changedGroupKeys:[]
本文項目地址:https://github.com/longxiaonan/springcloud-demo
參考: