概述
Seat是蚂蚁金服和阿里巴巴联合推出的一个开源的分布式事务框架,在阿里云商用的叫做GTS。
本文项目地址:github.com/longxiaonan…
一个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
本文采用的是 seata 1.3.0 (2020-07-14)
和springCloudAlibaba版本支持说明:
添加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
官网seata-server-0.9.0的conf目录下有该文件,官网seata-server-0.9.0的conf目录下有该文件,后面的版本无该文件需要手动下载执行。
修改为自己的服务组名,各个微服务之间使用相同的服务组名,务必保持一致!
service.vgroupMapping.my_test_tx_group=defaultservice.vgroupMapping.my_test_tx_group1=defaultservice.vgroupMapping.my_test_tx_group2=defaultservice.vgroupMapping.my_test_tx_group3=default配置seata服务器mysql链接信息,注意数据源类型,比如springboot的默认数据源是hikari而不是druid
store.mode=db...store.db.datasource=druidstore.db.dbType=mysqlstore.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=truestore.db.user=rootstore.db.password=123456执行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 8848set nacosAddr=localhost:8848set group=SEATA_GROUPcat: /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需要依赖的表
新建数据库seata, 创建如下三个表,用于seata服务, 0.0.9版本才有这个文件1.0.0版本后需要手动添加。
-- the table to store GlobalSession dataDROP 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 dataDROP 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 dataDROP 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记录分支的回滚信息
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_logCREATE 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 即可。
# linuxnohup ./seata-server.sh -h 192.168.1.4 -p 8092 &# windows cmdseata-server.bat -m file -h 192.168.1.4 -p 8092 用例
参考官网中用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:
存储服务:扣除给定商品的存储数量。
订单服务:根据购买请求创建订单。
帐户服务:借记用户帐户的余额。
本文项目地址:
添加依赖:
<!-- 分布式事务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 correct2020-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:302534236449259522020-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: Rollbackedfeign.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:302534236449259522020-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 null2020-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=null2020-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_storage2020-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 GlobalFinished2020-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 correct00: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:[]本文项目地址:github.com/longxiaonan…
参考: