SpringCloud系列 - Seata 分布式事务(六)
目录
一、介绍
二、什么情况下要使用分布式事务?
三、Seata 核心架构与组件
2.1 TC
2.2 TM
2.3 RM
三、案例引入
3.1 案例说明
3.2 环境搭建
3.3 测试接口
四、整合Seata
4.1 下载并启动Seata服务端
4.2 各微服务引入seata客户端
(1)引入依赖
(2)配置文件
(3)添加@GlobalTransactional
五、Seata 事务模式
5.1 AT 模式
5.2 TCC 模式
5.3 XA 模式
5.4 Saga 模式
一、介绍
Seata 是阿里巴巴开源的分布式事务解决方案,旨在为微服务架构提供高性能且简单易用的分布式事务服务。它通过全局事务协调机制,解决了微服务架构下跨服务、跨数据库的数据一致性问题。
二、什么情况下要使用分布式事务?
public void createOrder(Long productId, int quantity) {// 1. 扣减库存stockRepository.deductStock(productId, quantity);// 2. 创建订单Order order = new Order(productId, quantity);orderRepository.save(order);// 3. 业务逻辑校验(如库存不足、订单校验等)if (someBusinessValidationFails()) {throw new RuntimeException("业务校验失败");}}
如上所示,某业务方法内需要扣减库存、然后创建订单,如果出现业务逻辑异常,如何保证这两个操作能同时进行事务回滚?
情况一:该方法内扣库存和创建订单都在同一个数据库下,且是单体应用单实例部署。
很简单。本地事务(@Transactional
) 就可以来保证 扣减库存 和 创建订单 两个操作的原子性,确保在业务逻辑异常时能同时回滚。
需要注意并发竞争问题。
举个例子:扣减库存的前提是有库存才能扣减,如果扣减库存的逻辑是先查询出结果判断剩余数量再写sql扣减库存,那么就要合理的加锁,防止并发操作导致库存查询异常。
情况二:该方法内扣库存和创建订单都在同一个数据库下,且是单体应用多实例部署。
同样的,还是可以通过本地事务(@Transactional
) 来保证 扣减库存 和 创建订单 两个操作的原子性,确保在业务逻辑异常时能同时回滚。
但是要注意的是并发竞争问题。
多实例下并发竞争问题更明显,像前面单体单实例部署的解决方案用普通锁,在多实例下是行不通的。普通锁不能跨进程生效,因此还必须引入分布式锁解决并发竞争问题,比如Redis的红锁。
情况三:该方法内扣库存和创建订单在不同的数据库下,且是单体应用单实例部署。
核心挑战
- 跨数据库事务隔离
不同数据库的事务彼此独立,无法通过本地事务( @Transactional
)直接协调回滚。
- 动态数据源切换的局限性
若使用动态数据源,事务开启后切换数据源无效,因事务管理器已绑定初始数据源的连接。
解决办法:因此需要使用分布式事务来解决数据一致性问题,保证不同数据库同时回滚或提交。
情况四:该方法内扣库存和创建订单在不同的数据库下,且是单体应用多实例部署。
一样的,只要跨库了那么事务就是彼此独立的。本地事务肯定不能直接回滚。所以还是要用分布式事务。
关于并发竞争问题,这个无论是哪种情况下都要考虑。多实例更要考虑,必要时引入分布式锁。
情况五:该方法内扣库存和创建订单在同一个数据库下,但是扣减库存和创建订单来自不同的微服务,相当于该方法内远程调用
虽然共享同一数据库,但不同微服务的事务管理器独立,无法通过本地事务( @Transactional
)直接协调回滚。
因此必须使用分布式事务。
同样的并发竞争问题依然必须考虑,使用分布式锁解决。
三、Seata 核心架构与组件
Seata 的架构设计围绕三个核心组件展开,这三个组件协同工作,共同完成分布式事务的管理:
2.1 TC
Transaction Coordinator - 事务协调器
角色定位:Seata 服务端的核心组件,相当于分布式事务的"大脑"
核心职责:
- 维护全局事务和分支事务的状态
- 协调并驱动全局事务的提交或回滚
- 管理全局事务ID(XID)的生成和传播
- 处理分支事务的注册和状态报告
部署方式:通常独立部署为Seata Server,支持集群部署保证高可用
2.2 TM
Transaction Manager - 事务管理器
角色定位:集成在业务应用中,是全局事务的发起者和终结者
核心职责:
- 定义全局事务的边界(通过@GlobalTransactional注解)
- 发起全局事务的开始、提交或回滚请求
- 与TC保持通信,报告全局事务的最终状态
实现方式:通过Java注解与业务代码集成,对业务侵入性低
2.3 RM
Resource Manager - 资源管理器
角色定位:集成在每个参与分布式事务的微服务中,管理本地资源
核心职责:
- 管理分支事务的执行和状态
- 向TC注册分支事务并报告执行状态
- 根据TC指令执行本地事务的提交或回滚
- 在AT模式下负责生成和管理undo_log回滚日志
实现机制:通过代理数据源的方式拦截SQL操作,实现自动补偿
三组件协作比喻:将分布式事务比作糖葫芦串,TC是串糖葫芦的竹签,TM是第一个糖葫芦(触发分布式事务的服务),RM则是各个糖葫芦(参与事务的微服务)。
三、案例引入
3.1 案例说明
现在有一个用户下单的业务场景。
首先,通过采购服务的购买接口,远程调用库存服务扣减库存,然后远程调用订单服务创建订单。创建订单服务又需要远程调用账户服务扣减个人账户余额。
这几个服务和数据库都是独立的,一旦任意环节出了问题,肯定是需要回滚操作的。
示意图如下:
3.2 环境搭建
导入事先提供好的关于seata-demo文件夹中的几个模块,到我们的项目中。
可以看到服务进来了,但是idea没有识别,原因是我们还没有在父工程的pom文件定义这几个模块。不用担心,处理完成后,刷新maven,然后就能识别出来。
配置文件中我们可以看到,每个服务对应的数据库是不一样的,是满足案例要求的。
然后就创建数据库和数据表,直接复制这段生成即可。
CREATE DATABASE
IFNOT EXISTS `storage_db`;
USE `storage_db`;
DROP TABLE
IFEXISTS `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;
INSERT INTO storage_tbl ( commodity_code, count )
VALUES( 'P0001', 100 );
INSERT INTO storage_tbl ( commodity_code, count )
VALUES( 'B1234', 10 );-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE
IFEXISTS `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;
CREATE DATABASE
IFNOT EXISTS `order_db`;
USE `order_db`;
DROP TABLE
IFEXISTS `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;-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE
IFEXISTS `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;
CREATE DATABASE
IFNOT EXISTS `account_db`;
USE `account_db`;
DROP TABLE
IFEXISTS `account_tbl`;
CREATE TABLE `account_tbl` (`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,`user_id` VARCHAR ( 255 ) DEFAULT NULL,`money` INT ( 11 ) DEFAULT 0,PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8;
INSERT INTO account_tbl ( user_id, money )
VALUES( '1', 10000 );-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE
IFEXISTS `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;
然后在idea中连接上这几个数据库方便查看。
3.3 测试接口
测试 购买 接口,结果程序报错了,订单服务中create方法有异常。
我们观察数据库,发现余额和库存都扣了,但是订单没有创建。这就存在了严重的一致性问题。
如果有一个地方失败,应该是全部回滚的。可是余额和库存都扣减了。
所以接下来进入分布式事务seata来解决这个问题~
四、整合Seata
4.1 下载并启动Seata服务端
官网下载地址:Seata-Server版本历史 | Apache Seata
推荐版本:apache-seata-2.1.0-incubating-bin.tar.gz
解压后,进入bin目录,cmd 执行 seata-server.bat
可以看到默认的服务端是在8091端口。
还额外提供了一个客户端web界面,在7091端口。账号密码均为seata。
4.2 各微服务引入seata客户端
(1)引入依赖
在servie父工程的pom.xml中引入
<!-- 分布式事务seata --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId></dependency>
(2)配置文件
file.conf
service {#transaction service group mapping#事务分组,默认名称就叫default,不改可以不配vgroupMapping.default_tx_group = "default"#only support when registry.type=file, please don't set multiple addresses#default事务分组的服务器列表地址,注意不是7091,7091是客户端页面端口default.grouplist = "127.0.0.1:8091"#degrade, current not supportenableDegrade = false#disable seatadisableGlobalTransaction = false}
(3)添加@GlobalTransactional
只需要在全局事务入口(购买业务上)标记 @GlobalTransactional 注解,其他的都不用动。
启动项目查看效果
发现请求报错了,但是提交都回滚了,说明分布式事务启动成功!
五、Seata 事务模式
5.1 AT 模式
AT 模式是 Seata 创新的一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等。
5.2 TCC 模式
TCC 模式是 Seata 支持的一种由业务方细粒度控制的侵入式分布式事务解决方案,是继 AT 模式后第二种支持的事务模式,最早由蚂蚁金服贡献。其分布式事务模型直接作用于服务层,不依赖底层数据库,可以灵活选择业务资源的锁定粒度,减少资源锁持有时间,可扩展性好,可以说是为独立部署的 SOA 服务而设计的。
5.3 XA 模式
XA 模式是从 1.2 版本支持的事务模式。XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。Seata XA 模式是利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。
5.4 Saga 模式
Saga 模式是 SEATA 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。