谈谈你对 Seata 的理解?8 年 Java 开发:从业务踩坑到源码级解析(附实战代码)
作为一名摸爬滚打 8 年的 Java 开发,从单体项目的本地事务(@Transactional
一把梭),到微服务拆分后 "订单创建成功、库存没减" 的血案,再到现在复杂中台的分布式事务治理,Seata 几乎是绕不开的坎。今天不聊空洞的理论,只从实战角度跟大家掰扯:Seata 到底是什么?解决了什么业务痛点?怎么用才不踩坑?
一、从业务痛点说起:为什么需要 Seata?
在聊 Seata 之前,先回想一个你肯定遇到过的场景 ------电商下单 :
用户下单时,需要调用 3 个服务:
-
订单服务:创建订单(状态 "待支付");
-
库存服务:扣减对应商品库存;
-
积分服务:给用户加下单积分。
如果用单体架构,一个@Transactional
就能保证这 3 步要么全成、要么全败。但微服务拆分后,这 3 个操作分布在 3 个独立的服务、3 个独立的数据库里 ------ 问题来了:
-
万一订单创建成功,库存扣减时数据库宕机了怎么办?
-
库存扣完了,积分服务加积分超时了怎么办?
结果就是 "数据不一致":订单有了但库存没少(超卖风险)、库存少了但积分没加(用户投诉)。这时候,分布式事务的需求就冒出来了 ------ 而 Seata,就是阿里开源的、专门解决微服务分布式事务的框架。
8 年开发的第一个结论:Seata 不是 "银弹",而是 "对症药" ------ 它解决的是 "微服务跨库操作的数据一致性问题",前提是你真的遇到了这类问题(小项目单体架构没必要上 Seata,反而增加复杂度)。
二、Seata 核心:4 种模式 + 3 大组件,一篇讲透
Seata 的核心设计很清晰:用 "3 大组件" 支撑 "4 种事务模式",不同模式对应不同业务场景。先搞懂这俩,才算真的理解 Seata。
1. 先搞懂 3 大组件:谁在干活?
Seata 的分布式事务靠 3 个角色协同完成,类比 "剧组拍戏" 很好理解:
组件 | 角色定位 | 举个例子(下单场景) |
---|---|---|
TC(协调者) | 事务 "导演":负责协调所有参与者提交 / 回滚 | 独立部署的 Seata-Server,决定订单、库存、积分事务最终是提交还是回滚 |
TM(管理器) | 事务 "发起人":开启 / 结束事务 | 订单服务:调用@GlobalTransactional 注解的方法,发起分布式事务 |
RM(资源器) | 事务 "参与者":执行本地事务 + 上报状态 | 订单库、库存库、积分库:执行本地 SQL,并向 TC 上报 "执行成功 / 失败" |
工作流程简化:
- TM(订单服务)向 TC 申请开启全局事务,TC 生成全局事务 ID(XID);
- TM 调用 RM1(订单库)执行 "创建订单",RM1 执行本地 SQL,生成 undo log,向 TC 上报 "执行成功";
- TM 调用 RM2(库存库)执行 "扣减库存",RM2 同理,上报状态;
- TM 调用 RM3(积分库)执行 "加积分",RM3 同理,上报状态;
- 若所有 RM 都成功,TC 通知所有 RM 提交事务;若有一个 RM 失败,TC 通知所有 RM 回滚事务。
2. 再选对 4 种模式:不同业务用不同方案
Seata 提供 4 种事务模式,8 年开发的经验是:90% 的业务用 AT 模式就够了,复杂场景再考虑 TCC/SAGA。
模式 | 核心原理 | 适用场景 | 优点 | 缺点 |
---|---|---|---|---|
AT | 基于 SQL 解析 + undo log,自动提交 / 回滚 | 简单业务(下单、支付),支持 MySQL/Oracle 等 | 零侵入(不用改业务代码),易用 | 依赖关系型数据库,不支持非 SQL 操作 |
TCC | 手动写 Try(预留资源)/Confirm(确认)/Cancel(取消) | 复杂业务(资金扣减、库存预留) | 灵活,支持非 DB 操作 | 开发成本高(需写 3 个方法) |
SAGA | 长事务拆分,正向流程 + 反向补偿 | 长事务(物流、审批流) | 支持超长时间事务 | 一致性弱(最终一致),需写补偿方法 |
TXC | 基于阿里云 PolarDB,强一致性 | 阿里云 PolarDB 用户 | 强一致,性能好 | 依赖特定数据库,通用性差 |
重点讲 AT 模式 (最常用):
比如 "扣减库存" 操作,AT 模式会做 3 件事:
-
SQL 解析:拦截 "update stock set num=num-1 where id=1",生成前镜像(更新前的 num 值,比如 10)和后镜像(更新后的 num 值,比如 9);
-
生成 undo log :把前镜像、后镜像、SQL 信息存到
undo_log
表,作为回滚依据; -
两阶段提交:
-
第一阶段:RM 执行本地 SQL,上报 "成功",持有本地锁;
-
第二阶段:若所有 RM 都成功,TC 通知 RM 删除 undo log、释放锁(提交);若有失败,TC 通知 RM 用 undo log 回滚(比如把 num 从 9 改回 10)。
-
这也是 AT 模式 "零侵入" 的原因 ------ 开发者不用关心回滚逻辑,Seata 自动搞定。
三、实战:Spring Cloud Alibaba 整合 Seata(附代码 + 避坑)
光说不练假把式,结合 "订单 - 库存" 场景,手把手教你搭 Seata 环境(用 Seata 1.6.1 版本,Spring Cloud Alibaba 2022.0.0.0 版本)。
1. 第一步:搭建 TC 服务(Seata-Server)
TC 是核心协调者,必须先启动:
-
下载 Seata-Server :从Seata 官网下载 1.6.1 版本,解压;
-
修改配置文件 :
打开
conf/application.yml
,配置注册中心(用 Nacos,微服务常用)和数据库(存储全局事务信息):yamlseata: registry: type: nacos nacos: server-addr: 127.0.0.1:8848 # 你的Nacos地址 group: SEATA_GROUP application: seata-server config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP store: mode: db # 事务信息存数据库(生产用db,不要用file) db: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/seata?useSSL=false&serverTimezone=UTC user: root password: 123456
-
建 TC 数据库表 :在
seata
库中执行官网 SQL,创建global_table
(全局事务表)、branch_table
(分支事务表)等; -
启动 Seata-Server :
进入
bin
目录,执行:yaml# Windows seata-server.bat -p 8091 -h 127.0.0.1 # Linux sh seata-server.sh -p 8091 -h 127.0.0.1
启动后,在 Nacos 的 "服务列表" 能看到
seata-server
,说明 TC 就绪。
2. 第二步:配置 RM 和 TM(微服务端)
以 "订单服务(TM)" 和 "库存服务(RM)" 为例,整合 Seata。
(1)加依赖(pom.xml)
两个服务都要加 Seata 和 Spring Cloud Alibaba 的依赖:
xml
<!-- Seata starter -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!-- 排除旧版本Seata,统一用1.6.1 -->
<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.6.1</version>
</dependency>
<!-- Nacos注册中心(微服务必备) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
(2)改配置(application.yml)
两个服务的配置类似,重点是 "Seata 事务组" 和 "数据源代理":
yaml
spring:
application:
name: order-service # 订单服务名(库存服务改名为stock-service)
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # Nacos地址
alibaba:
seata:
tx-service-group: my_seata_group # 事务组名,所有服务必须一致
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/order_db?useSSL=false&serverTimezone=UTC
username: root
password: 123456
# Seata配置
seata:
service:
vgroup-mapping:
my_seata_group: default # 事务组映射到TC的集群名(默认是default)
grouplist:
default: 127.0.0.1:8091 # TC服务地址
tx-service-group: my_seata_group
data-source-proxy-mode: AT # 数据源代理模式,AT模式必须配
(3)建 undo_log 表(关键!)
AT 模式需要在每个 RM 的数据库(订单库order_db
、库存库stock_db
)中创建undo_log
表,用于存储回滚信息:
r
CREATE TABLE `undo_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`branch_id` bigint NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int 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=utf8mb4;
(4)写业务代码
-
订单服务(TM) :发起分布式事务,调用库存服务;
scss@Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RestTemplate restTemplate; // 调用库存服务 // 全局事务注解:加在TM的方法上! @GlobalTransactional(rollbackFor = Exception.class) public void createOrder(Long userId, Long productId, Integer count) { // 1. 本地事务:创建订单 Order order = new Order(); order.setUserId(userId); order.setProductId(productId); order.setCount(count); order.setStatus(0); // 0:待支付 orderMapper.insert(order); System.out.println("订单创建成功:" + order.getId()); // 2. 远程调用:扣减库存(调用库存服务) String url = "http://stock-service/deduct?productId=" + productId + "&count=" + count; // 故意抛异常测试回滚:int i = 1/0; restTemplate.postForObject(url, null, String.class); } }
-
库存服务(RM) :执行本地事务(扣减库存);
java@Service public class StockService { @Autowired private StockMapper stockMapper; // 本地事务:不用加@GlobalTransactional,Seata自动拦截 public void deduct(Long productId, Integer count) { // 扣减库存 int rows = stockMapper.deductStock(productId, count); if (rows == 0) { throw new RuntimeException("库存不足!"); } System.out.println("库存扣减成功:商品ID=" + productId); } }
(5)测试效果
- 初始数据:库存库
stock_db
中,商品 ID=1 的库存是 10; - 正常调用:调用订单服务的
createOrder(1L, 1L, 2)
,订单创建成功,库存从 10 变为 8; - 测试回滚:在订单服务的
createOrder
中加int i = 1/0
(故意抛异常),再次调用 ------ 订单不会创建,库存还是 10,说明分布式事务回滚成功!
四、8 年开发总结:Seata 落地的 8 个避坑点
- 数据源代理必须配对 :AT 模式要设
data-source-proxy-mode: AT
,TCC 模式不用配 ------ 之前忘配这个,导致 undo log 没生成,回滚失败; - undo_log 表不能少 :每个 RM 的数据库都要建,字段不能错(尤其是
rollback_info
是 longblob 类型); - 事务组名要一致 :所有服务的
tx-service-group
必须相同,否则 TC 找不到对应的 RM; - @GlobalTransactional 加对地方 :只能加在 TM 的方法上(比如订单服务的
createOrder
),加在 RM 的方法上无效; - TC 要高可用:生产环境不能单节点部署 TC,要集群化(用 Nacos 做注册中心,多启动几个 Seata-Server);
- 避免长事务:AT 模式会持有数据库锁,长事务会导致锁竞争加剧,性能下降 ------ 复杂长事务建议用 SAGA 模式;
- 日志要开全 :Seata 日志级别设为 DEBUG,方便排查问题(配置
logging.level.io.seata=DEBUG
); - 版本要兼容:Spring Cloud Alibaba 和 Seata 版本要匹配(比如 2022.0.0.0 对应 Seata 1.6.x,2021.0.1.0 对应 Seata 1.4.x),版本不兼容会报各种奇怪的错。
五、Seata 选型:不是所有场景都适合用
最后,8 年开发的真心话:Seata 虽好,但不是所有场景都需要用。给大家一个选型参考:
-
用 Seata 的场景:跨服务、跨库的写操作,需要强一致性(比如下单、支付、资金转账);
-
不用 Seata 的场景:
- 单体项目:本地事务
@Transactional
足够; - 只读操作:比如查询订单 + 查询库存,不需要事务;
- 最终一致性即可:比如用户下单后发通知,用 MQ 异步通知就行,不用 Seata(Seata 是强一致,性能开销比 MQ 大)。
- 单体项目:本地事务
总结
Seata 本质是 "微服务分布式事务的解决方案",核心是通过 TC 协调 TM 和 RM,用不同模式(AT/TCC/SAGA)解决不同业务的一致性问题。作为 8 年 Java 开发,我的建议是:先理解业务痛点,再选对模式,最后做好落地配置------ 别为了用 Seata 而用 Seata,适合业务的才是最好的。
如果你在 Seata 落地中遇到过 "回滚失败""锁超时" 等问题,欢迎在评论区交流,咱们一起踩坑一起成长~