一、场景分析
假设现在有这么两个微服务 order、product,order 通过 feign 调用 product。

1.1、表结构
CREATE DATABASE IF NOT EXISTS `oms`;
CREATE TABLE `oms_order` (
`order_id` int NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`product_id` int NOT NULL COMMENT '商品ID',
`quantity` int NOT NULL COMMENT '总数量',
PRIMARY KEY (`order_id`)
) COMMENT='订单表';
CREATE DATABASE IF NOT EXISTS `pms`;
CREATE TABLE `pms_product` (
`product_id` int NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`stock` int NOT NULL COMMENT '商品库存',
`product_name` varchar(50) NOT NULL COMMENT '商品名称',
PRIMARY KEY (`product_id`)
) COMMENT='商品表';
INSERT INTO `pms_product` (`product_id`, `stock`, `product_name`)
VALUES (1, 100, 'HuaWei Mate60 Pro');
1.2、本地事务
@Service
@RequiredArgsConstructor
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
private final ProductFeignService productFeignService;
@Override
@Transactional(rollbackFor = Exception.class)
public Order createOrder() {
// 1、扣减库存
PmsDeductStockDto dto = new PmsDeductStockDto();
dto.setNum(1);
dto.setProductId(1);
productFeignService.deductStock(dto);
// 2、创建订单
Order order = new Order();
order.setQuantity(1);
order.setProductId(1);
this.baseMapper.insert(order);
int i = 1/0;// 报错
return order;
}
}
OrderServiceImpl#createOrder 加上了 @Transactional 注解,它是一个 Spring AOP 事务方法。因为 / by zero 错误,事务回滚,oms 数据库并没有插入订单数据;但是 pms 却扣减库存成功了。
pms_product 的库存 stock 从 100 被扣减到 99。
数据出现不一致:没有订单,库存却凭空减少。
这种不同数据库,或者相同数据库但不同服务之间的事务操作,所造成的数据不一致现象,叫做分布式事务问题。
我们使用 Seata 看看怎么来解决这个问题 →→
二、角色
Seata 的三大角色:
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
Seata分TC、TM和RM三个角色,TC(Server端)为单独服务端部署,TM和RM(Client端)由业务系统集成。
三、部署 Seata 服务端
3.1、版本选择
根据 Spring Cloud Alibaba 组件版本关系,到官网下载对应的 Seata 版本。
因为我的 Spring Cloud Alibaba 版本是 2021.0.5.0,所以下载 1.6.1.zip。

3.2、执行建表语句
创建数据库 seata:
CREATE DATABASE IF NOT EXISTS `seata`;
根据自己的数据库,执行下载包 seata\script\server\db 下脚本:


3.3、修改配置
- 修改 application.yml
修改 seata\conf\application.yml 文件,添加配置中心与注册中心信息。
seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
data-id: seataServer.properties
username: nacos
password: nacos
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
cluster: default
username: nacos
password: nacos
Seata 作为分布式事务协调者,可以通过注册中心与微服务通讯。
存疑:
config.nacos.namespace 如果配置 public 会起不来,不知道其他人会不会。
- 修改 config.txt
修改 seata\script\config-center\config.txt 文件,并将内容上传到 Nacos 配置中心 public 命名空间下,data-id 为 seataServer.properties。
#Transaction storage configuration, only for the server. The file, db, and redis configuration values are optional.
store.mode=db
store.lock.mode=db
store.session.mode=db
#These configurations are required if the `store mode` is `db`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can remove the configuration block.
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://192.168.40.111:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123456
修改事务信息的存储方式为 db,并修改数据库连接方式。
注意 mysql 8.0 以上,使用 com.mysql.cj.jdbc.Driver 驱动。
3.4、启动
运行 seata\bin\seata-server.bat
Nacos 服务列表有 seata-server 证明启动成功。

登录 http://localhost:7091/#/login 用户名密码 seata/seata

四、配置 Seata 客户端(AT 模式)
4.1、导入依赖
order、product 的 pom.xml 添加依赖:
<!-- seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
4.2、配置 application.yml
order、product 的 application.yml 添加配置:
seata:
application-id: ${spring.application.name}
tx-service-group: default_tx_group
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
username: nacos
password: nacos
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
data-id: seataServer.properties
username: nacos
password: nacos
- tx-service-group
seata 服务分组,要与服务端配置 service.vgroupMapping 的后缀对应。
- config.nacos.data-id
新版本的 seata 服务端、客户端配置可以共用同一套。
4.3、创建 undo_log 表(仅AT模式)
order、product 数据库创建 undo_log:
-- 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';


4.4、添加 @GlobalTransactional 注解
@Service
@RequiredArgsConstructor
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
private final ProductFeignService productFeignService;
@Override
// @Transactional(rollbackFor = Exception.class)
@GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
public Order createOrder() {
// 1、扣减库存
PmsDeductStockDto dto = new PmsDeductStockDto();
dto.setNum(1);
dto.setProductId(1);
productFeignService.deductStock(dto);
// 2、创建订单
Order order = new Order();
order.setQuantity(1);
order.setProductId(1);
this.baseMapper.insert(order);
int i = 1/0;
return order;
}
}
4.5、启动并测试

访问 localhost:8012/order/create
order-service 控制台报错:

product-service 回滚了事务:

数据库:


在文章开头从 100 减为 99 后,就没有再变化了。证明 Seata 是有效的。
五、原理
5.1、2PC
Seata AT 模式,整体机制还是两阶段提交协议(2PC Two-Phase Commit),只不过是协议的演变。
AT 模式(Automatic Transaction):
- 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录,释放本地锁和连接资源。
- 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
- 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
就因为 AT 模式 自动生成和清理回滚日志、自动提交和回滚事务、自动加锁和解锁,所以才叫 AT(Automatic Transaction)。
5.2、生命周期

1、TM 请求 TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号。XID,会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。当一进入事务方法中就会生成 XID , global_table 就是存储的全局事务信息。
2、RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。当运行数据库操作方法,branch_table 存储事务参与者分支事务信息。
3、TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚。
4、TC 驱动 RM 们将 XID 对应的自己的本地事务进行提交还是回滚。
5.3、流程图
带入我们上面的场景,就是:

订单服务,它既要开启全局事务,又要管理对 oms 的事务操作,所以它既是 TM 又是 RM。
商品服务,它只需要管理对 pms 的事务操作,所以它只是 RM。
- 第一阶段
1.1、用户下单,订单服务TM模块请求TC开启全局事务。TC存储全局事务信息(global_table),返回给TM XID(类似于 192.168.123.1:8091:2900960722371846145 这种格式)。
1.2、订单服务调用商品服务,传递 XID。
1.2.1、商品服务RM模块向TC注册分支事务扣减库存,TC存储商品服务的分支事务信息(branch_table)。
1.2.2、商品服务RM模块生成回滚日志记录(un_do)和本地事务一起提交。
1.3、订单保存。
1.3.1、订单服务RM模块向TC注册分支事务保存订单。TC存储订单服务的分支事务信息(branch_table)。
1.3.2、订单服务RM模块生成回滚日志记录(un_do)和本地事务一起提交。
- 第二阶段(正常提交 commit)
2.1、订单服务TM模块检测到下单逻辑无异常,向TC请求提交全局事务。
2.2、TC通知商品服务RM分支事务提交,RM清理本地回滚日志记录。
2.3、TC通知订单服务RM分支事务提交,RM清理本地回滚日志记录。
- 第二阶段(异常回滚 rollback)
2.1、订单服务TM模块捕获到下单异常,向TC请求回滚全局事务。
2.2、TC通知商品服务RM分支事务回滚,RM根据本地回滚日志记录回滚数据。
2.3、TC通知订单服务RM分支事务回滚,RM根据本地回滚日志记录回滚数据。




1万+

被折叠的 条评论
为什么被折叠?



