Seata:Spring Cloud Alibaba分布式事务组件(非常详细)
随着业务的不断发展,单体架构已经无法满足我们的需求,分布式微服务架构逐渐成为大型互联网平台的,但所有使用分布式微服务架构的应用都必须面临一个十分棘手的问题,那就是“分布式事务”问题。
在分布式微服务架构中,几乎所有业务操作都需要多个服务协作才能完成。对于其中的某个服务而言,它的数据一致性可以交由其自身数据库事务来保证,但从整个分布式微服务架构来看,其全局数据的一致性却是无法保证的。
例如,用户在某电商系统下单购买了一件商品后,电商系统会执行下 4 步:
调用订单服务创建订单数据
调用库存服务扣减库存
调用账户服务扣减账户金额
调用订单服务修改订单状态
为了保证数据的正确性和一致性,我们必须保证所有这些操作要么全部成功,要么全部失败,否则就可能出现类似于商品库存已扣减,但用户账户资金尚未扣减的情况。各服务自身的事务特性显然是无法实现这一目标的,此时,我们可以通过分布式事务框架来解决这个问题。
Seata 就是这样一个分布式事务处理框架,它是由阿里巴巴和蚂蚁金服共同开源的分布式事务解决方案,能够在微服务架构下提供高性能且简单易用的分布式事务服务。
Seata 的发展历程
阿里巴巴作为国内最早一批进行应用分布式(微服务化)改造的企业,很早就遇到微服务架构下的分布式事务问题。阿里巴巴对于分布式事务问题先后发布了以下解决方案:
2014 年,阿里中间件团队发布 TXC(Taobao Transaction Constructor),为集团内应用提供分布式事务服务。
2016 年,TXC 在经过产品化改造后,以 GTS(Global Transaction Service) 的身份登陆阿里云,成为当时业界一款云上分布式事务产品。在阿云里的公有云、专有云解决方案中,开始服务于众多外部客户。
2019 年起,基于 TXC 和 GTS 的技术积累,阿里中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback, FESCAR),和社区一起建设这个分布式事务解决方案。
2019 年 fescar 被重名为了seata(simple extensiable autonomous transaction architecture)。
TXC、GTS、Fescar 以及 seata 一脉相承,为解决微服务架构下的分布式事务问题交出了一份与众不同的答卷。
分布式事务相关概念
分布式事务主要涉及以下概念:- 事务:由一组操作构成的可靠、独立的工作单元,事务具备 ACID 的特性,即原子性、一致性、隔离性和持久性。
- 本地事务:本地事务由本地资源管理器(通常指数据库管理系统 DBMS,例如 MySQL、Oracle 等)管理,严格地支持 ACID 特性,高效可靠。本地事务不具备分布式事务的处理能力,隔离的最小单位受限于资源管理器,即本地事务只能对自己数据库的操作进行控制,对于其他数据库的操作则无能为力。
- 全局事务:全局事务指的是一次性操作多个资源管理器完成的事务,由一组分支事务组成。
- 分支事务:在分布式事务中,就是一个个受全局事务管辖和协调的本地事务。
我们可以将分布式事务理解成一个包含了若干个分支事务的全局事务。全局事务的职责是协调其管辖的各个分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个满足 ACID 特性的本地事务。
Seata 整体工作流程
Seata 对分布式事务的协调和控制,主要是通过 XID 和 3 个核心组件实现的。
XID
XID 是全局事务的标识,它可以在服务的调用链路中传递,绑定到服务的事务上下文中。
核心组件
Seata 定义了 3 个核心组件:
TC(Transaction Coordinator):事务协调器,它是事务的协调者(这里指的是 Seata 服务器),主要负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。
TM(Transaction Manager):事务管理器,它是事务的发起者,负责定义全局事务的范围,并根据 TC 维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议。
RM(Resource Manager):资源管理器,它是资源的管理者(这里可以将其理解为各服务使用的数据库)。它负责管理分支事务上的资源,向 TC 注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚。
以上三个组件相互协作,TC 以 Seata 服务器(Server)形式独立部署,TM 和 RM 则是以 Seata Client 的形式集成在微服务中运行,其整体工作流程如下图。

图1:Sentinel 的工作流程
Seata 的整体工作流程如下:
TM 向 TC 申请开启一个全局事务,全局事务创建成功后,TC 会针对这个全局事务生成一个全局的 XID;
XID 通过服务的调用链传递到其他服务;
RM 向 TC 注册一个分支事务,并将其纳入 XID 对应全局事务的管辖;
TM 根据 TC 收集的各个分支事务的执行结果,向 TC 发起全局事务提交或回滚决议;
TC 调度 XID 下管辖的所有分支事务完成提交或回滚操作。
Seata AT 模式
Seata 提供了 AT、TCC、SAGA 和 XA 四种事务模式,可以快速有效地对分布式事务进行控制。在这四种事务模式中使用最多,最方便的就是 AT 模式。与其他事务模式相比,AT 模式可以应对大多数的业务场景,且基本可以做到无业务入侵,开发人员能够有更多的精力关注于业务逻辑开发。
AT 模式的前提
任何应用想要使用 Seata 的 AT 模式对分布式事务进行控制,必须满足以下 2 个前提:必须使用支持本地 ACID 事务特性的关系型数据库,例如 MySQL、Oracle 等;
应用程序必须是使用 JDBC 对数据库进行访问的 JAVA 应用。
此外,我们还需要针对业务中涉及的各个数据库表,分别创建一个 UNDO_LOG(回滚日志)表。不同数据库在创建 UNDO_LOG 表时会略有不同,以 MySQL 为例,其 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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
AT 模式的工作机制
Seata 的 AT 模式工作时大致可以分为以两个阶段,下面我们就结一个实例来对 AT 模式的工作机制进行介绍。假设某数据库中存在一张名为 webset 的表,表结构如下。
列名 | 类型 | 主键 |
---|---|---|
id | bigint(20) | √ |
name | varchar(255) |
|
url | varchar(255) |
|
在某次分支事务中,我们需要在 webset 表中执行以下操作。
update webset set url = 'c.biancheng网站站点" rel="nofollow" />
图2:Seata AT 模式一阶段
Seata AT 模式一阶段工作流程如下。
1. 获取 SQL 的基本信息:Seata 拦截并解析业务 SQL,得到 SQL 的操作类型(UPDATE)、表名(webset)、判断条件(where name = 'C语言中文网')等相关信息。
2. 查询前镜像:根据得到的业务 SQL 信息,生成“前镜像查询语句”。
select id,name,url from webset where name='C语言中文网';
执行“前镜像查询语句”,得到即将执行操作的数据,并将其保存为“前镜像数据(beforeImage)”。
id | name | url | ||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | C语言中文网 | biancheng网站站点" rel="nofollow" /> select id,name,url from webset where id= 1; id name url 1 C语言中文网 c.biancheng网站站点" rel="nofollow" /> { "@class": "io.seata.rm.datasource.undo.BranchUndoLog", "xid": "172.26.54.1:8091:59629674", "branchId": 59629674, "sqlUndoLogs": [ "java.util.ArrayList", [ { "@class": "io.seata.rm.datasource.undo.SQLUndoLog", "sqlType": "UPDATE", "tableName": "webset", "beforeImage": { "@class": "io.seata.rm.datasource.sql.struct.TableRecords", "tableName": "webset", "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": -5, "value": [ "java.lang.Long", 1 ] }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "url", "keyType": "NULL", "type": 12, "value": "biancheng网站站点" rel="nofollow" /> update webset set url= 'biancheng网站站点" rel="nofollow" /> 图3:Seata 服务器下载页面 图4:Seata Server 目录结构 Seata Server 目录中包含以下子目录: bin:用于存放 Seata Server 可执行令。 conf:用于存放 Seata Server 的配置文件。 lib:用于存放 Seata Server 依赖的各种 Jar 包。 logs:用于存放 Seata Server 的日志。 Seata 配置中心 Seata 支持多种配置中心: nacos consul apollo etcd zookeeper file (读本地文件,包含 conf、properties、yml 等配置文件) Seata 整 Nacos 配置中心 添加 Maven 依赖 <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>版</version> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> <version>1.2.0及以上版本</version> </dependency> 在 Spring Cloud 项目中,通常只需要在 pom.xml 中添加 spring-cloud-starter-alibaba-seata 依赖即可,代码如下。 <!--引入 seata 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> Seata Server 配置 在 Seata Server 安装目录下的 config/registry.conf 中,将配置方式(config.type)修改为 Nacos,并对 Nacos 配置中心的相关信息进行配置,示例配置如下。 config { # Seata 支持 file、nacos 、apollo、zk、consul、etcd3 等多种配置中心 #配置方式修改为 nacos type = "nacos" nacos { #修改为使用的 nacos 服务器地址 serverAddr = "127.0.0.1:1111" #配置中心的名空间 namespace = "" #配置中心所在的分组 group = "SEATA_GROUP" #Nacos 配置中心的用户名 username = "nacos" #Nacos 配置中心的密码 password = "nacos" } } Seata Client 配置 seata: config: type: nacos nacos: server-addr: 127.0.0.1:1111 # Nacos 配置中心的地址 group : "SEATA_GROUP" #分组 namespace: "" username: "nacos" #Nacos 配置中心的用于名 password: "nacos" #Nacos 配置中心的密码 上传配置到 Nacos 配置中心 图5:config.txt 2. 在 /script/config-center/nacos 目录中,有以下 2 个 Seata 脚本: nacos-config.py:python 脚本。 nacos-config.sh:为 Linux 脚本,我们可以在 Windows 下通过 Git 令,将 config.txt 中的 Seata 配置上传到 Nacos 配置中心。 在 seata-1.4.2\script\config-center\nacos 目录下,右键鼠标选择 Git Bush Here,并在弹出的 Git 令窗口中执行以下令,将 config.txt 中的配置上传到 Nacos 配置中心。 sh nacos-config.sh -h 127.0.0.1 -p 1111 -g SEATA_GROUP -u nacos -w nacos Git 令各参数说明如下: -h:Nacos 的 host,默认取值为 localhost -p:端口号,默认取值为 8848 -g:Nacos 配置的分组,默认取值为 SEATA_GROUP -u:Nacos 用户名 -w:Nacos 密码 验证 Nacos 配置中心 图6:Seata Nacos 配置中心 Seata 注册中心 Seata 支持多种服务注册中心: eureka consul nacos etcd zookeeper sofa redis file (直连) Seata 整 Nacos 注册中心 Seata 整 Nacos 注册中心的步骤十分简单,步骤如下。 添加 Maven 依赖 将 nacos-client 的 Maven 依赖添加到项目的 pom.xml 文件中: <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>版</version> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> <version>1.2.0及以上版本</version> </dependency> 在 Spring Cloud 项目中,通常只需要在 pom.xml 中添加 spring-cloud-starter-alibaba-seata 依赖即可,代码如下。 <!--引入 seata 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> Seata Server 配置注册中心 在 Seata Server 安装目录下的 config/registry.conf 中,将注册方式(registry.type)修改为 Nacos,并对 Nacos 注册中心的相关信息进行配置,示例配置如下。 registry { # Seata 支持 file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 作为其注册中心 # 将注册方式修改为 nacos type = "nacos" nacos { application = "seata-server" # 修改 nacos 注册中心的地址 serverAddr = "127.0.0.1:1111" group = "SEATA_GROUP" namespace = "" cluster = "default" username = "" password = "" } } Seata Client 配置注册中心 我们可以在 Seata Client 的 application.yml 中,对 Nacos 注册中心进行配置,示例配置如下。 seata: registry: type: nacos nacos: application: seata-server server-addr: 127.0.0.1:1111 # Nacos 注册中心的地址 group : "SEATA_GROUP" #分组 namespace: "" username: "nacos" #Nacos 注册中心的用户名 password: "nacos" # Nacos 注册中心的密码 验证 Nacos 注册中心 图7:Seata Nacos 注册中心 Seata 事务分组 在应用中配置事务分组。 获得集群名称后,应用通过一定的前后缀 + 集群名称去构造服务名。 得到服务名后,去注册中心去拉取服务列表,获得后端真实的 TC 服务列表。 下面我们以 Nacos 服务注册中心为例,介绍 Seata 事务的使用。 Seata Server 配置 registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" #使用 Nacos作为注册中心 nacos { serverAddr = "127.0.0.1:1111" # Nacos 注册中心的地址 namespace = "" # Nacos 名空间id,"" 为 Nacos 保留 public 空间控件,用户勿配置 namespace = "public" cluster = "c.biancheng网站站点" rel="nofollow" /> spring: alibaba: seata: tx-service-group: service-order-group #事务分组名 seata: registry: type: nacos #从 Nacos 获取 TC 服务 nacos: server-addr: 127.0.0.1:1111 config: type: nacos #使用 Nacos 作为配置中心 nacos: server-addr: 127.0.0.1:1111 name 上传配置到 Nacos service.vgroupMapping.service-order-group=c.biancheng网站站点" rel="nofollow" /> -- -------------------------------- 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; branch_table 的建表 SQL 如下。 -- 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; lock_table 的建表 SQL 如下。 -- 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; 2. 修改 Seata Server 配置 在 seata-server-1.4.2/conf/ 目录下的 registry.conf 中,将 Seata Server 的服务注册方式(registry.type)和配置方式(config.type)都修改为 Nacos,修改内容如下。 registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa # 将注册方式修改为 nacos type = "nacos" nacos { application = "seata-server" # 修改 nacos 的地址 serverAddr = "127.0.0.1:1111" group = "SEATA_GROUP" namespace = "" cluster = "default" username = "" password = "" } } config { # file、nacos 、apollo、zk、consul、etcd3 #配置方式修改为 nacos type = "nacos" nacos { #修改为使用的 nacos 服务器地址 serverAddr = "127.0.0.1:1111" namespace = "" group = "SEATA_GROUP" username = "nacos" password = "nacos" #不使用 seataServer.properties 方式配置 #dataId = "seataServer.properties" } } 3. 将 Seata 配置上传到 Nacos 1) 下载并解压 Seata Server 的源码 seata-1.4.2.zip,然后修改 seata-1.4.2/script/config-center 目录下的 config.txt,修改内容如下。 #将 Seata Server 的存储模式修改为 db store.mode=db # 数据库驱动 store.db.driverClassName=com.mysql.cj.jdbc.Driver # 数据库 url store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useSSL=false&characterEncoding=UTF-8&useUnicode=true&serverTimezone=UTC # 数据库的用户名 store.db.user=root # 数据库的密码 store.db.password=root # 自定义事务分组 service.vgroupMapping.service-order-group=default service.vgroupMapping.service-storage-group=default service.vgroupMapping.service-account-group=default 2) 在 seata-1.4.2\script\config-center\nacos 目录下,右键鼠标选择 Git Bush Here,在弹出的 Git 令窗口中执行以下令,将 config.txt 中的配置上传到 Nacos 配置中心。 sh nacos-config.sh -h 127.0.0.1 -p 1111 -g SEATA_GROUP -u nacos -w nacos 3) 当 Git 令窗口出现以下执行日志时,则说明配置上传成功。 ========================================================================= Complete initialization parameters, total-count:87 , failure-count:0 ========================================================================= Init nacos config finished, please start seata-server. 图8:Seata 配置上传到 Nacos 注意:在使用 Git 令将配置上传到 Nacos 前,应该先确保 Nacos 服务器已启动。 4. 启动 Seata Server 图9:Seata Server 启动脚本 16:52:48,549 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml] 16:52:48,549 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy] 16:52:48,550 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/C:/Users/79330/Desktop/seata-server-1.4.2/conf/logback.xml] 16:52:48,551 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath. 16:52:48,551 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [jar:file:/C:/Users/79330/Desktop/seata-server-1.4.2/lib/seata-server-1.4.2.jar!/logback.xml] …… SLF4J: A number (18) of logging calls during the initialization phase have been intercepted and are SLF4J: now being replayed. These are subject to the filtering rules of the underlying logging system. SLF4J: See also http://www.slf4j网站站点" rel="nofollow" /> 图10:电商系统架构图 当用户从这个电商网站购买了一件商品后,其服务调用步骤如下: 调用 Order 服务,创建一条订单数据,订单状态为“未完成”; 调用 Storage 服务,扣减商品库存; 调用 Account 服务,从用户账户中扣除商品金额; 调用 Order 服务,将订单状态修改为“已完成”。 创建订单(Order)服务 1. 在 MySQL 数据库中,新建一个名为 seata-order 的数据库实例,并通过以下 SQL 语句创建 2 张表:t_order(订单表)和 undo_log(回滚日志表)。 -- ---------------------------- -- Table structure for t_order -- ---------------------------- DROP TABLE IF EXISTS `t_order`; CREATE TABLE `t_order` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint DEFAULT NULL COMMENT '用户id', `product_id` bigint DEFAULT NULL COMMENT '产品id', `count` int DEFAULT NULL COMMENT '数量', `money` decimal(11,0) DEFAULT NULL COMMENT '金额', `status` int DEFAULT NULL COMMENT '订单状态:0:未完成;1:已完结', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for undo_log -- ---------------------------- DROP TABLE IF EXISTS `undo_log`; CREATE TABLE `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 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 DEFAULT CHARSET=utf8 ; 2. 在主工程 spring-cloud-alibaba-demo 下,创建一个名为 spring-cloud-alibaba-seata-order-8005 的 Spring Boot 模块,并在其 pom.xml 中添加以下依赖,内容如下。 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache网站站点" rel="nofollow" /> spring: cloud: ## Nacos认证信息 nacos: config: username: nacos password: nacos context-path: /nacos server-addr: 127.0.0.1:1111 # 设置配置中心服务端地址 namespace: # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可 4. 在 spring-cloud-alibaba-seata-order-8005 的类路径(/resources 目录)下,创建一个配置文件 application.yml,配置内容如下。 spring: application: name: spring-cloud-alibaba-seata-order-8005 #服务名 #数据源配置 datasource: driver-class-name: com.mysql.jdbc.Driver #数据库驱动 name: defaultDataSource url: jdbc:mysql://localhost:3306/seata_order?serverTimezone=UTC #数据库连接地址 username: root #数据库的用户名 password: root #数据库密码 cloud: nacos: discovery: server-addr: 127.0.0.1:1111 #nacos 服务器地址 namespace: public #nacos 名空间 username: password: sentinel: transport: dashboard: 127.0.0.1:8080 #Sentinel 控制台地址 port: 8719 alibaba: seata: #自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同 tx-service-group: service-order-group server: port: 8005 #端口 seata: application-id: ${spring.application.name} #自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同 tx-service-group: service-order-group service: grouplist: #Seata 服务器地址 seata-server: 127.0.0.1:8091 # Seata 的注册方式为 nacos registry: type: nacos nacos: server-addr: 127.0.0.1:1111 # Seata 的配置中心为 nacos config: type: nacos nacos: server-addr: 127.0.0.1:1111 feign: sentinel: enabled: true #开启 OpenFeign 功能 management: endpoints: web: exposure: include: "*" ###################################### MyBatis 配置 ###################################### mybatis: # 指定 mapper.xml 的位置 mapper-locations: classpath:mybatis/mapper/*.xml #扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名 type-aliases-package: net.biancheng.c.entity configuration: #默认开启驼峰名法,可以不用设置该属性 map-underscore-to-camel-case: true 5. 在 net.biancheng.c.entity 包下,创建一个名为 Order 的实体类,代码如下。 package net.biancheng.c.entity; import java.math.BigDecimal; public class Order { private Long id; private Long userId; private Long productId; private Integer count; private BigDecimal money; private Integer status; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public Long getProductId() { return productId; } public void setProductId(Long productId) { this.productId = productId; } public Integer getCount() { return count; } public void setCount(Integer count) { this.count = count; } public BigDecimal getMoney() { return money; } public void setMoney(BigDecimal money) { this.money = money; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } } 6. 在 net.biancheng.c.mapper 包下,创建一个名为 OrderMapper 的 Mapper 接口,代码如下。 package net.biancheng.c.mapper; import net.biancheng.c.entity.Order; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @Mapper public interface OrderMapper { int deleteByPrimaryKey(Long id); int insert(Order record); int create(Order order); int insertSelective(Order record); //2 修改订单状态,从零改为1 void update(@Param("userId") Long userId, @Param("status") Integer status); Order selectByPrimaryKey(Long id); int updateByPrimaryKeySelective(Order record); int updateByPrimaryKey(Order record); } <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis网站站点" rel="nofollow" /> package net.biancheng.c.service; import net.biancheng.c.entity.Order; public interface OrderService { /** * 创建订单数据 * @param order */ CommonResult create(Order order); } 9. 在 net.biancheng.c.service 包下,创建一个名为 StorageService 的接口,代码如下。 package net.biancheng.c.service; import net.biancheng.c.entity.CommonResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; @FeignClient(value = "spring-cloud-alibaba-seata-storage-8006") public interface StorageService { @PostMapping(value = "/storage/decrease") CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count); } 10. 在 net.biancheng.c.service 包下,创建一个名为 AccountService 的接口,代码如下。 package net.biancheng.c.service; import net.biancheng.c.entity.CommonResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import java.math.BigDecimal; @FeignClient(value = "spring-cloud-alibaba-seata-account-8007") public interface AccountService { @PostMapping(value = "/account/decrease") CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money); } 11. 在 net.biancheng.c.service.impl 包下,创建 OrderService 接口的实现类 OrderServiceImpl,代码如下。 package net.biancheng.c.service.impl; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import net.biancheng.c.entity.Order; import net.biancheng.c.mapper.OrderMapper; import net.biancheng.c.service.AccountService; import net.biancheng.c.service.OrderService; import net.biancheng.c.service.StorageService; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service @Slf4j public class OrderServiceImpl implements OrderService { @Resource private OrderMapper orderMapper; @Resource private StorageService storageService; @Resource private AccountService accountService; /** * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态 * 简单说:下订单->扣库存->减余额->改订单状态 */ @Override @GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class) public CommonResult create(Order order) { log.info("----->开始新建订单"); //1 新建订单 order.setUserId(new Long(1)); order.setStatus(0); orderMapper.create(order); //2 扣减库存 log.info("----->订单服务开始调用库存服务,开始扣减库存"); storageService.decrease(order.getProductId(), order.getCount()); log.info("----->订单微服务开始调用库存,扣减库存结束"); //3 扣减账户 log.info("----->订单服务开始调用账户服务,开始从账户扣减商品金额"); accountService.decrease(order.getUserId(), order.getMoney()); log.info("----->订单微服务开始调用账户,账户扣减商品金额结束"); //4 修改订单状态,从零到1,1代表已经完成 log.info("----->修改订单状态开始"); orderMapper.update(order.getUserId(), 0); log.info("----->修改订单状态结束"); log.info("----->下订单结束了------->"); return new CommonResult(200, "订单创建成功"); } } 12. 在 net.biancheng.c.controller 包下,创建一个名为 OrderController 的 Controller,代码如下。 package net.biancheng.c.controller; import net.biancheng.c.entity.CommonResult; import net.biancheng.c.entity.Order; import net.biancheng.c.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; @RestController public class OrderController { @Autowired private OrderService orderService; @GetMapping("/order/create/{productId}/{count}/{money}") public CommonResult create(@PathVariable("productId") Integer productId, @PathVariable("count") Integer count , @PathVariable("money") BigDecimal money) { Order order = new Order(); order.setProductId(Integer.valueOf(productId).longValue()); order.setCount(count); order.setMoney(money); return orderService.create(order); } } 13. 主启动类代码如下。 package net.biancheng.c; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @EnableDiscoveryClient @EnableFeignClients @SpringBootApplication(scanBasePackages = "net.biancheng") public class SpringCloudAlibabaSeataOrder8005Application { public static void main(String[] args) { SpringApplication.run(SpringCloudAlibabaSeataOrder8005Application.class, args); } } 搭建库存(Storage)服务 1. 在 MySQL 数据库中,新建一个名为 seata-storage 的数据库实例,并通过以下 SQL 语句创建 2 张表:t_storage(库存表)和 undo_log(回滚日志表)。 -- ---------------------------- -- Table structure for t_storage -- ---------------------------- DROP TABLE IF EXISTS `t_storage`; CREATE TABLE `t_storage` ( `id` bigint NOT NULL AUTO_INCREMENT, `product_id` bigint DEFAULT NULL COMMENT '产品id', `total` int DEFAULT NULL COMMENT '总库存', `used` int DEFAULT NULL COMMENT '已用库存', `residue` int DEFAULT NULL COMMENT '剩余库存', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of t_storage -- ---------------------------- INSERT INTO `t_storage` VALUES ('1', '1', '100', '0', '100'); -- Table structure for undo_log -- ---------------------------- DROP TABLE IF EXISTS `undo_log`; CREATE TABLE `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 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 DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table'; <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache网站站点" rel="nofollow" /> spring: cloud: ## Nacos认证信息 nacos: config: username: nacos password: nacos context-path: /nacos server-addr: 127.0.0.1:1111 # 设置配置中心服务端地址 namespace: # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可 4. 在 spring-cloud-alibaba-seata-storage-8006 的类路径(/resources 目录)下,创建一个配置文件 application.yml,配置内容如下。 spring: application: name: spring-cloud-alibaba-seata-storage-8006 datasource: driver-class-name: com.mysql.jdbc.Driver name: defaultDataSource url: jdbc:mysql://localhost:3306/seata_storage?serverTimezone=UTC username: root password: root cloud: nacos: discovery: server-addr: 127.0.0.1:1111 namespace: public username: password: sentinel: transport: dashboard: 127.0.0.1:8080 port: 8719 alibaba: seata: tx-service-group: service-storage-group server: port: 8006 seata: application-id: ${spring.application.name} tx-service-group: service-storage-group service: grouplist: seata-server: 127.0.0.1:8091 registry: type: nacos nacos: server-addr: 127.0.0.1:1111 config: type: nacos nacos: server-addr: 127.0.0.1:1111 feign: sentinel: enabled: true management: endpoints: web: exposure: include: "*" ###################################### MyBatis 配置 ###################################### mybatis: # 指定 mapper.xml 的位置 mapper-locations: classpath:mybatis/mapper/*.xml #扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名 type-aliases-package: net.biancheng.c.entity configuration: #默认开启驼峰名法,可以不用设置该属性 map-underscore-to-camel-case: true 5. 在 net.biancheng.c.entity 包下,创建一个名为 Storage 的实体类,代码如下。 package net.biancheng.c.entity; public class Storage { private Long id; private Long productId; private Integer total; private Integer used; private Integer residue; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getProductId() { return productId; } public void setProductId(Long productId) { this.productId = productId; } public Integer getTotal() { return total; } public void setTotal(Integer total) { this.total = total; } public Integer getUsed() { return used; } public void setUsed(Integer used) { this.used = used; } public Integer getResidue() { return residue; } public void setResidue(Integer residue) { this.residue = residue; } } 6. 在 net.biancheng.c.mapper 包下,创建一个名为 StorageMapper 的接口,代码如下。 package net.biancheng.c.mapper; import net.biancheng.c.entity.Storage; import org.apache.ibatis.annotations.Mapper; @Mapper public interface StorageMapper { Storage selectByProductId(Long productId); int decrease(Storage record); } 7. 在 /resouces/mybatis/mapper 目录下,创建一个名为 StorageMapper.xml 的 MyBatis 映射文件,代码如下。 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis网站站点" rel="nofollow" /> package net.biancheng.c.service; public interface StorageService { int decrease(Long productId, Integer count); } package net.biancheng.c.service.impl; import lombok.extern.slf4j.Slf4j; import net.biancheng.c.entity.Storage; import net.biancheng.c.mapper.StorageMapper; import net.biancheng.c.service.StorageService; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service @Slf4j public class StorageServiceImpl implements StorageService { @Resource StorageMapper storageMapper; @Override public int decrease(Long productId, Integer count) { log.info("------->storage-service中扣减库存开始"); log.info("------->storage-service 开始查询商品是否存在"); Storage storage = storageMapper.selectByProductId(productId); if (storage != null && storage.getResidue().intValue() >= count.intValue()) { Storage storage2 = new Storage(); storage2.setProductId(productId); storage.setUsed(storage.getUsed() + count); storage.setResidue(storage.getTotal().intValue() - storage.getUsed()); int decrease = storageMapper.decrease(storage); log.info("------->storage-service 扣减库存成功"); return decrease; } else { log.info("------->storage-service 库存不足,开始回滚!"); throw new RuntimeException("库存不足,扣减库存失败!"); } } } package net.biancheng.c.controller; import lombok.extern.slf4j.Slf4j; import net.biancheng.c.entity.CommonResult; import net.biancheng.c.service.StorageService; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; @RestController @Slf4j public class StorageController { @Resource private StorageService storageService; @Value("${server.port}") private String serverPort; @PostMapping(value = "/storage/decrease") CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count) { int decrease = storageService.decrease(productId, count); CommonResult result; if (decrease > 0) { result = new CommonResult(200, "from mysql,serverPort: " + serverPort, decrease); } else { result = new CommonResult(505, "from mysql,serverPort: " + serverPort, "库存扣减失败"); } return result; } } 11. 主启动类的代码如下。 package net.biancheng.c; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @EnableDiscoveryClient @EnableFeignClients @SpringBootApplication(scanBasePackages = "net.biancheng") public class SpringCloudAlibabaSeataStorage8006Application { public static void main(String[] args) { SpringApplication.run(SpringCloudAlibabaSeataStorage8006Application.class, args); } } 搭建账户(Account)服务 1. 在 MySQL 数据库中,新建一个名为 seata-account 的数据库实例,并通过以下 SQL 语句创建 2 张表:t_account(账户表)和 undo_log(回滚日志表)。 DROP TABLE IF EXISTS `t_account`; CREATE TABLE `t_account` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id', `user_id` bigint 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; -- ---------------------------- -- Records of t_account -- ---------------------------- INSERT INTO `t_account` VALUES ('1', '1', '1000', '0', '1000'); -- ---------------------------- -- Table structure for undo_log -- ---------------------------- DROP TABLE IF EXISTS `undo_log`; CREATE TABLE `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 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 DEFAULT CHARSET=utf8; 2. 在主启动类 spring-cloud-alibaba-seata-account-8007 下,创建一个名为 spring-cloud-alibaba-seata-account-8007 的 Spring Boot 模块,并在其 pom.xml 中添加以下依赖,代码如下。 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache网站站点" rel="nofollow" /> spring: cloud: ## Nacos认证信息 nacos: config: username: nacos password: nacos context-path: /nacos server-addr: 127.0.0.1:1111 # 设置配置中心服务端地址 namespace: # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可 spring: application: name: spring-cloud-alibaba-seata-account-8007 datasource: driver-class-name: com.mysql.cj.jdbc.Driver name: defaultDataSource url: jdbc:mysql://localhost:3306/seata_account?serverTimezone=UTC username: root password: root cloud: nacos: discovery: server-addr: 127.0.0.1:1111 namespace: public username: password: sentinel: transport: dashboard: 127.0.0.1:8080 port: 8719 alibaba: seata: tx-service-group: service-account-group server: port: 8007 seata: application-id: ${spring.application.name} tx-service-group: service-account-group service: grouplist: seata-server: 127.0.0.1:8091 registry: type: nacos nacos: server-addr: 127.0.0.1:1111 config: type: nacos nacos: server-addr: 127.0.0.1:1111 feign: sentinel: enabled: true management: endpoints: web: exposure: include: "*" ###################################### MyBatis 配置 ###################################### mybatis: # 指定 mapper.xml 的位置 mapper-locations: classpath:mybatis/mapper/*.xml #扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名 type-aliases-package: net.biancheng.c.entity configuration: #默认开启驼峰名法,可以不用设置该属性 map-underscore-to-camel-case: true 5. 在 net.biancheng.c.entity 包下,创建一个名为 Account 的实体类,代码如下。 package net.biancheng.c.entity; import java.math.BigDecimal; public class Account { private Long id; private Long userId; private BigDecimal total; private BigDecimal used; private BigDecimal residue; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public BigDecimal getTotal() { return total; } public void setTotal(BigDecimal total) { this.total = total; } public BigDecimal getUsed() { return used; } public void setUsed(BigDecimal used) { this.used = used; } public BigDecimal getResidue() { return residue; } public void setResidue(BigDecimal residue) { this.residue = residue; } } 6. 在 net.biancheng.c.mapper 包下,创建一个名为 AccountMapper 的接口,代码如下。 package net.biancheng.c.mapper; import net.biancheng.c.entity.Account; import org.apache.ibatis.annotations.Mapper; import java.math.BigDecimal; @Mapper public interface AccountMapper { Account selectByUserId(Long userId); int decrease(Long userId, BigDecimal money); } 7. 在 /resouces/mybatis/mapper 目录下,创建一个名为 AccountMapper.xml 的 MyBatis 映射文件,代码如下。 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis网站站点" rel="nofollow" /> package net.biancheng.c.service; import org.springframework.web.bind.annotation.RequestParam; import java.math.BigDecimal; public interface AccountService { /** * 扣减账户余额 * * @param userId 用户id * @param money 金额 */ int decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money); } 9. 在 net.biancheng.c.service.impl 包下,创建 AccountService 的实现类 AccountServiceImpl,代码如下。 package net.biancheng.c.service.impl; import lombok.extern.slf4j.Slf4j; import net.biancheng.c.entity.Account; import net.biancheng.c.mapper.AccountMapper; import net.biancheng.c.service.AccountService; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; @Service @Slf4j public class AccountServiceImpl implements AccountService { @Resource AccountMapper accountMapper; @Override public int decrease(Long userId, BigDecimal money) { log.info("------->account-service 开始查询账户余额"); Account account = accountMapper.selectByUserId(userId); log.info("------->account-service 账户余额查询完成," + account); if (account != null && account.getResidue().intValue() >= money.intValue()) { log.info("------->account-service 开始从账户余额中扣钱!"); int decrease = accountMapper.decrease(userId, money); log.info("------->account-service 从账户余额中扣钱完成"); return decrease; } else { log.info("账户余额不足,开始回滚!"); throw new RuntimeException("账户余额不足!"); } } } 10. 主启动类代码如下。 package net.biancheng.c; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @EnableDiscoveryClient @EnableFeignClients @SpringBootApplication(scanBasePackages = "net.biancheng") public class SpringCloudAlibabaSeataAccount8007Application { public static void main(String[] args) { SpringApplication.run(SpringCloudAlibabaSeataAccount8007Application.class, args); } } 11. 依次启动 Nacos Server(集群)和 Seata Server(集群),启动 spring-cloud-alibaba-seata-order-8005,当控制台出现以下日志时,说明该服务已经成功连接上 Seata Server(TC)。 2021-11-25 15:16:27.389 INFO 19564 --- [ restartedMain] com.za er.hikari.HikariDataSource : defaultDataSource - Start completed. 2021-11-25 15:16:27.553 INFO 19564 --- [ restartedMain] i.s.c.r网站站点" rel="nofollow" /> 2021-11-25 15:16:28.621 INFO 14772 --- [ restartedMain] com.za er.hikari.HikariDataSource : defaultDataSource - Start completed. 2021-11-25 15:16:28.969 INFO 14772 --- [ restartedMain] i.s.c.r网站站点" rel="nofollow" /> 2021-11-25 15:16:29.914 INFO 8616 --- [ restartedMain] com.za er.hikari.HikariDataSource : defaultDataSource - Start completed. 2021-11-25 15:16:30.253 INFO 8616 --- [ restartedMain] i.s.c.r网站站点" rel="nofollow" /> {"code":200,"message":"订单创建成功","data":null} 15. 执行以下 SQL 语句,查询 seata_order 数据库中的 t_order 表。 SELECT * FROM seata_order.t_order; id user_id product_id count money status 1 1 2 20 1 16. 执行以下 SQL 语句,查询 seata_storage 数据库中的 t_storage 表。 SELECT * FROM seata_storage.t_storage; id product_id total used residue 1 1 100 2 98 17. 执行以下 SQL 语句,查询 seata_account 数据库中的 t_account 表, SELECT * FROM seata_account.t_account; id user_id total used residue 1 1 1000 20 980 18. 使用浏览器访问“http://localhost:8005/order/createByAnnotation/1/10/1000”,结果如下。 Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback. Thu Nov 25 15:27:03 CST 2021 There was an unexpected error (type=Internal Server Error, status=500). [500] during [POST] to [http://spring-cloud-alibaba-seata-account-8007/account/decrease?userId=1&money=1000] [AccountService#decrease(Long,BigDecimal)]: [{"timestamp":"2021-11-25T07:27:03.512+00:00","status":500,"error":"Internal Server Error","trace":"java.lang.RuntimeException: 账户余额不足!\r\n\tat net.biancheng.c.service.impl.AccountServiceImpl.decrease(... (5673 bytes)] feign.FeignException$InternalServerError: [500] during [POST] to [http://spring-cloud-alibaba-seata-account-8007/account/decrease?userId=1&money=1000] [AccountService#decrease(Long,BigDecimal)]: [{"timestamp":"2021-11-25T07:27:03.512+00:00","status":500,"error":"Internal Server Error","trace":"java.lang.RuntimeException: 账户余额不足!\r\n\tat net.biancheng.c.service.impl.AccountServiceImpl.decrease(... (5673 bytes)] 图11:分布式事务问题 t_order(订单表):订单服务生成了一条订单数据(id 为 2),但订单状态(status)为未完成(0); t_storage(库存表):商品库存已扣减; t_account(账户表):账户金额不足,账户(Account)服务出现异常,进而导致账户金额并未扣减。 @GlobalTransactional 注解 在类中某个方法使用时,全局事务的范围就是该方法以及它所涉及的所有服务。 在类上使用时,全局事务的范围就是这个类中的所有方法以及这些方法涉及的服务。 1. 在 spring-cloud-alibaba-seata-order-8005 的 net.biacheng.c 包下的 OrderController 中,添加一个名为 createByAnnotation 的方法,代码如下。 /** * 使用 @GlobalTransactional 注解对分布式事务进行管理 * @param productId * @param count * @param money * @return */ @GetMapping("/order/createByAnnotation/{productId}/{count}/{money}") @GlobalTransactional(name = "c-biancheng-net-create-order", rollbackFor = Exception.class) public CommonResult createByAnnotation(@PathVariable("productId") Integer productId, @PathVariable("count") Integer count , @PathVariable("money") BigDecimal money) { Order order = new Order(); order.setProductId(Integer.valueOf(productId).longValue()); order.setCount(count); order.setMoney(money); return orderService.create(order); } 从以上代码可以看出,添加的 createByAnnotation() 方法与 create() 方法无论是参数还是代码逻辑都一摸一样,的不同就是前者标注了 @GlobalTransactional 注解。 3. 重启订单(Order)服务、库存(Storage)服务和账户(Account)服务,并使用浏览器访问“http://localhost:8005/order/createByAnnotation/1/10/1000”,结果如下。 Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback. Thu Nov 25 15:27:03 CST 2021 There was an unexpected error (type=Internal Server Error, status=500). [500] during [POST] to [http://spring-cloud-alibaba-seata-account-8007/account/decrease?userId=1&money=1000] [AccountService#decrease(Long,BigDecimal)]: [{"timestamp":"2021-11-25T07:27:03.512+00:00","status":500,"error":"Internal Server Error","trace":"java.lang.RuntimeException: 账户余额不足!\r\n\tat net.biancheng.c.service.impl.AccountServiceImpl.decrease(... (5673 bytes)] feign.FeignException$InternalServerError: [500] during [POST] to [http://spring-cloud-alibaba-seata-account-8007/account/decrease?userId=1&money=1000] [AccountService#decrease(Long,BigDecimal)]: [{"timestamp":"2021-11-25T07:27:03.512+00:00","status":500,"error":"Internal Server Error","trace":"java.lang.RuntimeException: 账户余额不足!\r\n\tat net.biancheng.c.service.impl.AccountServiceImpl.decrease(... (5673 bytes)] 4. spring-cloud-alibaba-seata-order-8005 控制台输出如下(标红部分为回滚日志)。 2021-11-25 15:26:57.586 INFO 19564 --- [nio-8005-exec-1] n.b.c.service.impl.OrderServiceImpl : ----->开始新建订单 2021-11-25 15:26:58.276 INFO 19564 --- [nio-8005-exec-1] n.b.c.service.impl.OrderServiceImpl : ----->订单服务开始调用库存服务,开始扣减库存 2021-11-25 15:26:58.413 WARN 19564 --- [nio-8005-exec-1] c.l.c.ServiceInstanceListSupplierBuilder : LoadBalancerCacheManager not available, returning delegate without caching. 2021-11-25 15:27:00.705 INFO 19564 --- [nio-8005-exec-1] n.b.c.service.impl.OrderServiceImpl : ----->订单微服务开始调用库存,扣减库存结束 2021-11-25 15:27:00.705 INFO 19564 --- [nio-8005-exec-1] n.b.c.service.impl.OrderServiceImpl : ----->订单服务开始调用账户服务,开始从账户扣减商品金额 2021-11-25 15:27:00.723 WARN 19564 --- [nio-8005-exec-1] c.l.c.ServiceInstanceListSupplierBuilder : LoadBalancerCacheManager not available, returning delegate without caching. 2021-11-25 15:27:03.670 INFO 19564 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 172.30.194.1:8091:2702361983450404762 2702361983450404764 jdbc:mysql://localhost:3306/seata_order 2021-11-25 15:27:03.738 INFO 19564 --- [h_RMROLE_1_1_16] i.s.r.d.undo.AbstractUndoLogManager : xid 172.30.194.1:8091:2702361983450404762 branch 2702361983450404764, undo_log deleted with GlobalFinished 2021-11-25 15:27:03.742 INFO 19564 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked 2021-11-25 15:27:03.817 INFO 19564 --- [nio-8005-exec-1] i.seata.tm.api.DefaultGlobalTransaction : [172.30.194.1:8091:2702361983450404762] rollback status: Rollbacked 2021-11-25 15:27:03.853 ERROR 19564 --- [nio-8005-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.FeignException$InternalServerError: [500] during [POST] to [http://spring-cloud-alibaba-seata-account-8007/account/decrease?userId=1&money=1000] [AccountService#decrease(Long,BigDecimal)]: [{"timestamp":"2021-11-25T07:27:03.512+00:00","status":500,"error":"Internal Server Error","trace":"java.lang.RuntimeException: 账户余额不足!\r\n\tat net.biancheng.c.service.impl.AccountServiceImpl.decrease(... (5673 bytes)]] with root cause 5. spring-cloud-alibaba-seata-storage-8006 控制台输出如下。(标红部分为回滚日志) 2021-11-25 15:26:59.315 INFO 14772 --- [nio-8006-exec-1] n.b.c.service.impl.StorageServiceImpl : ------->storage-service中扣减库存开始 2021-11-25 15:26:59.316 INFO 14772 --- [nio-8006-exec-1] n.b.c.service.impl.StorageServiceImpl : ------->storage-service 开始查询商品是否存在 2021-11-25 15:27:00.652 INFO 14772 --- [nio-8006-exec-1] n.b.c.service.impl.StorageServiceImpl : ------->storage-service 扣减库存成功 2021-11-25 15:27:03.572 INFO 14772 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 172.30.194.1:8091:2702361983450404762 2702361983450404769 jdbc:mysql://localhost:3306/seata_storage 2021-11-25 15:27:03.631 INFO 14772 --- [h_RMROLE_1_1_16] i.s.r.d.undo.AbstractUndoLogManager : xid 172.30.194.1:8091:2702361983450404762 branch 2702361983450404769, undo_log deleted with GlobalFinished 2021-11-25 15:27:03.635 INFO 14772 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked 6. spring-cloud-alibaba-seata-account-8007 控制台输出如下(标红部分为回滚日志)。 2021-11-25 15:27:03.366 INFO 8616 --- [nio-8007-exec-1] n.b.c.service.impl.AccountServiceImpl : ------->account-service 开始查询账户余额 2021-11-25 15:27:03.484 INFO 8616 --- [nio-8007-exec-1] n.b.c.service.impl.AccountServiceImpl : ------->account-service 账户余额查询完成,net.biancheng.c.entity.Account@2a95537f 2021-11-25 15:27:03.499 ERROR 8616 --- [nio-8007-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: 账户余额不足!] with root cause java.lang.RuntimeException: 账户余额不足! 图12:Seata 事务回滚 |