当我们把一个单体内核拆成多个微服务之后,最先崩溃的往往不是接口性能,而是数据一致性。最典型的例子就是下单扣库存:订单服务把订单写入自己的库,库存服务再扣减另一个库的数量。如果第二笔操作失败,前一笔操作已经写进去了,数据库本地事务帮不了你,因为这是两个独立的数据库连接,两个不同的事务。于是,账面上多了一笔没有库存的订单,或者库存被多扣了却没有订单。
很多团队在这个阶段才开始认真研究分布式事务,随手翻开资料就会发现,结论几乎都会指向同一个名字:Seata。它是目前国内 Spring Cloud 微服务架构中使用最广泛的分布式事务框架之一,社区活跃、资料多、接入成本相对可控。更关键的是,它把“分布式事务”从理论名词变成了可以实际落地的工程方案。
这篇文章会先讲清楚 Seata 到底解决什么问题,然后拆解它的核心架构和几种事务模式,最后用一个 Spring Cloud 下单扣库存的完整示例,带你跑通 Seata 的 AT 模式,并说明验证方式、常见故障排查方法和工程建议。读完你应该能判断自己的项目是否需要 Seata,以及它最合理的接入方式是什么。
1. 这篇文章真正要解决的问题
先直接给一个判断:Seata 不是分布式事务的万能药,但它是把“跨服务强一致”做成可接受工程成本的最实际路径之一。
为什么这么说?因为分布式事务有太多理论模型,比如两阶段提交、三阶段提交、补偿事务、本地消息表、事务消息,但真正能在 Spring Cloud 体系里直接落地、被大量生产项目验证过的方案并不多。Seata 之所以流行,核心原因是它的 AT 模式对业务代码侵入非常小,开发者几乎只需要在业务入口加一个注解,再配合几个配置项,就能让原本跨库、跨服务的事务获得提交或回滚能力。
这篇文章会围绕三个问题展开:
- 微服务拆分后,本地事务为什么无法解决跨库数据一致性问题。
- Seata 的架构角色和事务模式分别适合什么场景。
- 从环境搭建、代码实现到运行验证,Seata 的 AT 模式到底怎么落地。
如果你正在做微服务改造,或者你的系统已经出现“订单和库存对不上”“账户余额和流水对不上”这类问题,这篇文章会比较适合你。如果你是技术负责人,需要给团队一个分布式事务方案,这篇文章也可以帮你快速建立判断框架。
2. 基础概念与核心原理
2.1 分布式事务问题是怎么产生的
在没有微服务的时候,一个下单流程通常是单库多表操作,比如订单表、库存表、账户表都在同一个数据库里。数据库的本地事务用 BEGIN、COMMIT、ROLLBACK 就能保证要么全部成功、要么全部失败。
微服务拆分后,订单数据、库存数据、账户数据各自独立的数据库,甚至分属不同的物理机器。这个时候你面对的不再是同一个事务管理器能管理的范围,而是多个独立事务之间的协调问题。一个业务流程横跨多个服务,任意一个环节失败,都需要让前面已经成功的操作撤销。这就是分布式事务要解决的问题,本质是跨多个数据源、多个服务的事务一致性。
2.2 Seata 是什么
Seata 是 Simple Extensible Autonomous Transaction Architecture 的缩写,一个开源的分布式事务中间件。它由阿里巴巴发起,目前是 Apache 基金会孵化项目。它在设计上把一次分布式事务拆分为全局事务和分支事务:
- 全局事务:由发起方开启,覆盖整个业务流程,包含多个参与服务。
- 分支事务:全局事务中每一个参与服务内部执行的本地事务。
Seata 的任务就是协调这些分支事务,让它们在外界看来像一个整体事务一样,要么全部生效,要么全部回滚。
2.3 Seata 的四种事务模式
Seata 提供四种事务模式,分别是 AT、TCC、SAGA 和 XA。它们解决同一个问题,但侵入性、性能、一致性强度各不相同。
AT 模式是 Seata 最有特色的模式。它对业务代码侵入最小,核心思路是“自动补偿”。AT 模式会代理数据源,在业务 SQL 执行前记录数据快照到undo_log表,业务 SQL 执行成功后进入两阶段提交流程。如果全局事务需要回滚,Seata 根据undo_log中的快照自动生成反向 SQL,把数据恢复到操作前状态。
TCC 模式要求业务方自己实现 Try、Confirm、Cancel 三组接口,侵入性最强,但调优空间也最大,适合资源锁定和补偿逻辑需要精细控制的场景。
SAGA 模式适合长事务,将一个大事务拆分为多个本地事务,每个事务都有对应的补偿操作,由状态机引擎编排执行。
XA 模式借助数据库原生支持的两阶段提交协议来实现强一致,对业务代码几乎无侵入,但资源锁时间较长,性能开销相对高。
四种模式的对比可以看下面的表:
| 模式 | 对业务侵入 | 一致性 | 性能 | 适用场景 |
|---|---|---|---|---|
| AT | 低 | 最终一致 | 较好 | 多数基于关系型数据库的业务场景 |
| TCC | 高 | 最终一致 | 较好 | 需要人工控制资源锁和补偿逻辑的场景 |
| SAGA | 中 | 最终一致 | 较好 | 长事务、基于状态机的流程编排场景 |
| XA | 低 | 强一致 | 一般 | 数据库原生支持且可接受锁时长的场景 |
对于大多数 Spring Cloud 内部服务,AT 模式是最优先考虑的选择。TCC 更适用于对外部资源或非关系型存储的精确补偿,SAGA 更适合有明确流程节点划分的长业务链。
2.4 一个容易误解的地方
很多人以为 Seata 是消息中间件或者定时任务,其实它是一个事务协调平台。它不负责传输业务数据,也不帮你重放消息,它的核心工作是维护全局事务状态、记录分支事务状态、在合适时机下发提交或回滚指令。
还有一点容易误解:AT 模式的回滚依赖于数据库undo_log表,而不是应用层做逆向业务操作。它是在数据层面做恢复,不调用业务接口。这意味着业务代码里写的逻辑补偿并不需要额外处理,但前提是undo_log表在每个业务库中都要创建。
3. Seata 核心架构:TC、TM、RM 分别负责什么
要理解 Seata,必须先理解它的三个核心角色。
TC 是事务协调者,独立部署的 Seata Server,负责维护全局事务和分支事务的状态,驱动全局提交或回滚。可以把它理解为总调度室,所有分布式事务的元信息都在 TC 侧。
TM 是事务管理器,负责定义全局事务的范围。通常体现在业务方法上的@GlobalTransactional注解。TM 向 TC 发起开启全局事务、提交或回滚的请求。它是全局事务的发起入口。
RM 是资源管理器,负责管理分支事务的资源。在 AT 模式下,RM 通过代理数据源拦截 JDBC 操作,向 TC 注册分支事务,并记录回滚日志。它负责真正执行本地 SQL,并上报事务结果。
一次完整的 AT 模式分布式事务流程大致如下:
- TM 向 TC 开启全局事务,拿到全局事务 ID,也就是 XID。
- XID 通过服务调用链路传播到下游服务。
- 下游服务在执行本地 SQL 前,RM 把业务数据的前镜像写入
undo_log。 - 本地 SQL 执行完成,RM 把后镜像写入
undo_log,并向 TC 注册分支事务。 - 业务方法执行成功,TM 发起全局提交;如果任意服务抛出异常,TM 发起全局回滚。
- 全局提交时,TC 通知各 RM 删除
undo_log记录。 - 全局回滚时,TC 通知各 RM 根据
undo_log构造反向 SQL 恢复数据。
这里面最关键的机制就是“前镜像和后镜像”。前镜像是该行数据在执行 SQL 之前的值,后镜像是执行之后的值。回滚时,如果当前数据等于后镜像,说明数据没有被其他事务改过,可以直接恢复成前镜像;如果已经被改过,就需要做冲突处理。这也是为什么 Seata 还要依赖全局锁来控制写写冲突。
在 Spring Cloud 集成场景中,还需要理解“事务分组”这个概念。客户端配置tx-service-group指定事务分组,例如my_test_tx_group,然后通过配置映射到 Seata Server 集群名,客户端再根据集群名从注册中心找到 TC。这个分组配置经常是大家启动失败的第一原因,下面实战部分会重点看它。
4. 环境准备与前置条件
4.1 需要准备哪些组件
本文的实战示例以一个常见的 Seata + Nacos + Spring Cloud 组合为例,涉及以下环境:
- JDK 8 及以上。
- Maven 3.6 及以上。
- MySQL 5.7 或 8.x,需要准备两个数据库,分别给订单服务和库存服务使用。
- Nacos 作为注册中心和配置中心。
- Seata Server,独立部署事务协调器 TC。
- 两个 Spring Boot 服务,分别模拟订单服务和库存服务。
需要提醒的是,Seata 与 Spring Cloud Alibaba、Nacos 的版本之间存在依赖关系,具体版本请以你项目实际使用的 Spring Cloud Alibaba BOM 为准。本文代码示例重点演示通用思路,不绑定某个精确版本号。
4.2 安装并启动 Nacos
Nacos 的启动相对简单。下载 Nacos 后,进入bin目录,在 Linux 或 macOS 上执行:
sh startup.sh -m standaloneWindows 环境使用startup.cmd -m standalone。启动成功后,浏览器访问http://127.0.0.1:8848/nacos,默认用户名密码都是nacos。
Nacos 在这里的作用是作为注册中心。Seata Server 会把自己注册到 Nacos,两个业务服务也会注册到 Nacos,并通过 Nacos 发现彼此。
4.3 下载并配置 Seata Server
Seata Server 在 Seata 官方仓库或 GitHub Releases 页面下载。解压后需要关注两个目录:conf目录和bin目录。conf里有application.yml、registry.conf等配置文件,不同版本文件名略有差异。
在本文示例中,我们可以通过配置文件让 Seata Server 使用 Nacos 作为注册中心。以常见的 Seata 1.x 版本的registry.conf为例,核心内容如下:
registry { type = "nacos" nacos { serverAddr = "127.0.0.1:8848" namespace = "" cluster = "default" } } config { type = "nacos" nacos { serverAddr = "127.0.0.1:8848" namespace = "" } }如果你的 Seata 版本不是通过registry.conf配置,而是在application.yml中配置,含义是相同的:指定 registry 类型和 config 类型为 Nacos。请以具体版本和官方文档为准。
配置完成后,进入bin目录启动 Seata Server:
sh seata-server.sh -p 8091 -h 127.0.0.1启动成功后,在 Nacos 的服务列表里应该能看到名为serverAddr对应的 Seata Server 服务实例。这个动作说明 TC 已经就绪。
4.4 初始化业务数据库与 undo_log 表
我们需要为两个服务分别创建数据库,例如seata_order和seata_storage。先执行订单库的建表语句:
CREATE DATABASE IF NOT EXISTS `seata_order` DEFAULT CHARSET utf8mb4; USE `seata_order`; CREATE TABLE `t_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_id` varchar(64) NOT NULL, `product_id` varchar(32) NOT NULL, `count` int(11) NOT NULL, `user_id` varchar(32) NOT NULL, `status` int(11) NOT NULL DEFAULT '1', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;再执行库存库的建表语句:
CREATE DATABASE IF NOT EXISTS `seata_storage` DEFAULT CHARSET utf8mb4; USE `seata_storage`; CREATE TABLE `storage` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `product_id` varchar(32) NOT NULL, `remaining` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO `storage` (`product_id`, `remaining`) VALUES ('P001', 100);每个业务库中还必须创建 Seata AT 模式依赖的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=utf8mb4;注意undo_log表必须和业务表在同一个数据库。如果订单库有业务表而没有undo_log表,回滚时会报分支回滚失败。
5. 核心流程拆解与完整示例代码
5.1 项目结构规划
本文示例包含两个服务:
order-service,端口 8081,负责创建订单,是整个全局事务的发起方。storage-service,端口 8082,负责扣减库存,是全局事务的参与方。
调用关系是:order-service先插入订单,然后通过 OpenFeign 调用storage-service扣减库存。如果库存不足,storage-service抛出异常,order-service捕获到异常后,整个全局事务自动回滚,订单插入被撤销。
这种设计可以很直观地验证分布式事务的回滚效果:成功时两边数据都变化,失败时两边数据都还原。
5.2 引入依赖
两个服务都需要引入 Spring Boot Web、Spring Cloud OpenFeign、Nacos Discovery、Spring JDBC 以及 Seata 相关依赖。以 Maven 为例,核心依赖如下:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>依赖版本由 Spring Cloud Alibaba BOM 统一管理,不需要在项目里手工指定 Seata 版本。如果你的工程比较老旧,需要手动管理版本,建议优先参考官方提供的版本兼容矩阵。
5.3 配置 application.yml
order-service的src/main/resources/application.yml配置如下:
server: port: 8081 spring: application: name: order-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 datasource: url: jdbc:mysql://127.0.0.1:3306/seata_order?useUnicode=true&characterEncoding=utf8&useSSL=false username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver seata: enabled: true application-id: order-service tx-service-group: my_test_tx_group registry: type: nacos nacos: server-addr: 127.0.0.1:8848storage-service的配置与order-service类似,端口改成 8082,数据库 URL 改成seata_storage,应用名改成storage-service。
事务分组my_test_tx_group是我们在业务代码中定义的逻辑分组。客户端会拿着这个分组去找对应的 Seata Server 集群,不同服务之间要保持一致,否则会出现找不到 Seata Server 的报错。
5.4 订单服务核心代码
OrderService是全局事务的发起方,关键在于@GlobalTransactional注解:
// 文件路径:order-service/src/main/java/com/example/order/service/OrderService.java @Service public class OrderService { private final JdbcTemplate jdbcTemplate; private final StorageFeignClient storageFeignClient; public OrderService(JdbcTemplate jdbcTemplate, StorageFeignClient storageFeignClient) { this.jdbcTemplate = jdbcTemplate; this.storageFeignClient = storageFeignClient; } @GlobalTransactional(name = "create-order-global-tx", timeoutMills = 30000) public void createOrder(String orderId, String productId, Integer count, String userId) { jdbcTemplate.update( "INSERT INTO t_order (order_id, product_id, count, user_id, status) VALUES (?, ?, ?, ?, 1)", orderId, productId, count, userId ); storageFeignClient.deductStock(productId, count); } }代码逻辑非常简单:先写订单库,再远程调库存服务扣库存。真正容易踩坑的是远程调用不能吞异常。如果storage-service扣减失败后返回了一个包含错误码的对象,但 Feign Client 没有把非 2xx 状态抛出,order-service会认为调用成功,全局事务自然也就不会回滚。所以这里的 Feign 接口要确保业务异常能通过 HTTP 状态码或抛异常的方式传递回来。
对应的 Controller:
// 文件路径:order-service/src/main/java/com/example/order/controller/OrderController.java @RestController @RequestMapping("/order") public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService = orderService; } @PostMapping("/create") public String create( @RequestParam("orderId") String orderId, @RequestParam("productId") String productId, @RequestParam("count") Integer count, @RequestParam("userId") String userId) { orderService.createOrder(orderId, productId, count, userId); return "success"; } }5.5 库存服务核心代码
库存服务的StorageService使用 JdbcTemplate 执行扣减 SQL。这里用了一个防超扣的条件:只有剩余库存大于等于扣减数量时才更新,否则影响行数为 0,直接抛出异常:
// 文件路径:storage-service/src/main/java/com/example/storage/service/StorageService.java @Service public class StorageService { private final JdbcTemplate jdbcTemplate; public StorageService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public void deductStock(String productId, Integer count) { int rows = jdbcTemplate.update( "UPDATE storage SET remaining = remaining - ? WHERE product_id = ? AND remaining >= ?", count, productId, count ); if (rows == 0) { throw new RuntimeException("库存不足"); } } }对应的 Controller 和 Feign 接口:
// 文件路径:storage-service/src/main/java/com/example/storage/controller/StorageController.java @RestController @RequestMapping("/storage") public class StorageController { private final StorageService storageService; public StorageController(StorageService storageService) { this.storageService = storageService; } @PostMapping("/deduct") public String deduct(@RequestParam("productId") String productId, @RequestParam("count") Integer count) { storageService.deductStock(productId, count); return "success"; } }在order-service中定义 Feign Client:
// 文件路径:order-service/src/main/java/com/example/order/client/StorageFeignClient.java @FeignClient(name = "storage-service", path = "/storage") public interface StorageFeignClient { @PostMapping("/deduct") String deductStock(@RequestParam("productId") String productId, @RequestParam("count") Integer count); }5.6 运行顺序与验证命令
按顺序启动组件:
# 1. 启动 Nacos sh startup.sh -m standalone # 2. 启动 Seata Server sh seata-server.sh -p 8091 -h 127.0.0.1 # 3. 启动 storage-service mvn spring-boot:run -pl storage-service # 4. 启动 order-service mvn spring-boot:run -pl order-service所有服务启动成功后,执行成功场景请求:
curl -X POST "http://127.0.0.1:8081/order/create?orderId=ORD001&productId=P001&count=1&userId=U001"这时候预期结果有两个:
seata_order.t_order中新增一条ORD001的订单记录。seata_storage.storage中P001的剩余库存从 100 变成 99。
再执行失败场景请求:
curl -X POST "http://127.0.0.1:8081/order/create?orderId=ORD002&productId=P001&count=9999&userId=U001"库存不足会触发RuntimeException,全局事务应当回滚。预期结果是接口返回异常信息,t_order中没有ORD002这条记录,storage的剩余库存保持 99 不变。
6. 运行结果与效果验证
验证 Seata 是否生效,不能只看业务结果,还要看事务日志和数据库镜像。
6.1 从发起方日志观察
order-service的日志中会出现类似下面的关键行:
global tx started: 192.168.x.x:8091:123456789这说明 TM 已经向 TC 请求并成功开启了全局事务,后面的数字就是全局事务 ID,也就是 XID。
当业务方法成功执行完,会出现全局提交相关日志;当业务方法抛出异常,会出现全局回滚相关日志。如果日志里从头到尾没有出现global tx started,说明@GlobalTransactional没有生效,需要检查 Seata 相关配置是否被正确加载。
6.2 从参与方日志观察
storage-service的日志中会出现分支事务注册相关日志,例如:
branch transaction registered: 192.168.x.x:8091:123456789:1234567890这表示库存服务执行本地 SQL 后,RM 向 TC 注册了分支事务。如果走到这一步,说明 XID 已经成功通过服务调用链路传播到了下游。
回滚时,参与方日志中还会出现 undo 日志删除或反向 SQL 执行的记录。失败场景中,库存服务抛出异常后,order-service需要先把异常通过网络传递回来,然后触发全局回滚,两个服务都会收到 TC 的回滚指令。
6.3 从数据库观察
成功场景下,执行订单插入后立即去查询undo_log表。事务提交完成后,undo_log中可能只会短暂存在记录,最终会被 Seata 清理掉。如果整个流程执行太快,你可能看不到记录,这是正常的。
失败场景下,回滚完成后,undo_log表中同样会被清理。如果t_order没有新增记录,但undo_log里却有残留记录,说明回滚阶段出了问题,需要重点排查。
6.4 判断事务是否真正生效
最简单的判断方式就是构造一个必失败的下游操作。库存扣减为 0 行就是一个很合适的失败开关。如果订单没有落库,说明全局回滚成功;如果订单依然存在,说明 Seata 没有起作用或者配置有遗漏。
如果只是为了验证 Seata 的提交路径,也可以只跑成功场景,观察两个库的数据变化。但建议把失败场景一起跑,因为验证回滚才是用 Seata 的核心价值。
7. 常见问题与排查方法
Seata 的安装和接入本身不难,但实际运行中确实有不少高频问题。下面把最常见的几种现象整理成表格,方便快速定位。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 应用启动后报找不到 Seata Server | Seata Server 未启动、注册中心配置不一致、事务分组配置错误 | 检查 Nacos 服务列表,确认 Seata Server 实例在线;检查各服务事务分组是否一致 | 启动 Seata Server,统一事务分组和注册中心地址 |
| 全局事务并未开启 | 入口方法没有加@GlobalTransactional,或 Seata 自动配置未生效 | 查看发起方日志是否有global tx started | 在全局事务发起方法加注解,确认 Seata 相关依赖和配置存在 |
| 库存扣减成功但订单不生成 | 订单服务和库存服务不在同一个全局事务中 | 检查下游日志是否出现分支事务注册记录 | 确认 XID 通过 Feign 调用传递,检查调用链路是否有异常被吞 |
| 回滚时报 Branch transaction rollback failure | 业务库缺少undo_log表、表结构不对、业务表无主键 | 查看回滚堆栈,定位具体表;检查undo_log表是否创建 | 在每个业务库创建undo_log表,确认业务表有主键 |
| 回滚时数据恢复不正确 | 业务表存在其他并发操作,前后镜像匹配失败 | 分析全局锁等待日志,确认是否有并发修改同一行数据 | 缩短事务时间,或结合业务幂等控制并发 |
| 全局事务运行超时 | 事务方法内包含外部接口、文件处理、网络等待等耗时操作 | 查看 TC 日志中的超时时间,检查业务方法耗时 | 将耗时操作移出全局事务,增加超时时间 |
| 启动时提示事务分组找不到 vgroup mapping | 客户端事务分组没有映射到 Seata Server 集群名 | 检查 Seata 配置中心中的分组映射,检查客户端 tx-service-group 拼写 | 在配置中心补充service.vgroupMapping.{group}={cluster}映射 |
这里最容易被忽略的是第二个问题。@GlobalTransactional不是放在任何一个方法上都生效,它必须放在全局事务的入口方法上,也就是跨服务调用的最外层。如果你放在库存服务自己的方法上,订单服务调用它时并不会开启全局事务。
还有一个常见问题是 Feign 调用时异常被吞掉。如果storage-service抛出异常后被某个全局异常处理器捕获,然后返回了一个 200 状态码的 JSON,order-service不会感知到失败,全局事务也不会回滚。比较好的做法是让服务间的业务异常以非 2xx 状态码返回,或者由 Feign 解码器把业务错误码转换为异常抛出。
8. 最佳实践与工程建议
Seata 接入项目后,真正决定成败的往往是工程细节。我从实际项目角度整理了一些建议。
第一,事务边界要尽量小。全局事务内部不要包含用户交互、外部 HTTP 调用、短信发送、文件上传等不可控耗时操作。每个服务内部的本地事务时间尽量控制在毫秒级,否则全局锁的等待时间会线性上升。能用本地事务解决的就不要升级为分布式事务。
第二,AT 模式不是没有成本。它在执行本地 SQL 时要写undo_log,提交时要删除日志,回滚时要根据镜像生成反向 SQL。这个开销对大多数业务是可以接受的,但高并发写入同一行数据的场景下,全局锁会成为瓶颈。如果业务对某个热点账户、热点商品的并发写入很高,需要评估是否改用 TCC,或者用消息队列做最终一致。
第三,undo_log表是回滚的命根子。每个业务库都要创建,表名不能改,字段结构也不能随意增删。业务表必须有主键,否则 Seata 无法锁定具体行数据,回滚时也无法精确定位。数据库账号至少需要 SELECT、UPDATE、DELETE、INSERT 操作业务表和undo_log表的权限,不要在生产环境使用超级管理员账号跑业务。
第四,全局事务的隔离性需要业务设计兜底。AT 模式提供了写隔离,但默认的读隔离是读未提交级别。也就是说,在极端情况下,一个事务还没提交,另一个事务可能读到它修改的中间数据。如果业务不允许这种脏读,需要自己在查询时加SELECT ... FOR UPDATE,或者考虑其他一致性更强的方案。
第五,幂等设计不能省略。Seata 保证的是事务原子性,不负责消息重试和接口重复调用的幂等。比如订单服务在全局事务提交成功后,如果客户端超时重试,同一个订单可能会被创建两次。业务表需要一个业务唯一键,比如order_id字段,用于防止重复插入。
第六,监控和演练要纳入日常。Seata Server 的日志中可以看到全局事务提交数和回滚数;Nacos 的控制台上可以看到服务健康状态。建议在测试环境定期模拟库存不足、超时、Seata Server 宕机等场景,确认系统在回滚时不会把数据弄坏。
第七,不要把所有跨服务操作都升级为分布式事务。很多场景更适合消息补偿。比如下单后发优惠券,用事务消息先写订单,再异步发券,配合对账任务处理失败记录。这种方式对性能和可用性的冲击更小。Seata 应该在真正需要强一致的业务链路上使用。
第八,版本的统一和升级要谨慎。Seata 客户端和服务端的版本最好保持一致,跨小版本升级前先在测试环境走一遍全链路事务用例。Seata 版本和 Spring Cloud Alibaba 版本之间还有兼容关系,不要随意升级其中的某一个。
9. 总结与后续学习方向
回到开头的问题:Seata 是什么?它不是一个能把所有分布式事务问题都包办的框架,而是一个把跨服务数据一致性问题转化为可工程化处理方案的中间件。AT 模式通过数据镜像和自动补偿,让业务代码只需要关心事务边界,TCC 模式则给了团队精细控制资源锁和补偿逻辑的能力,SAGA 模式适合长业务流程,XA 模式在用得上时可以借助数据库能力实现强一致。
如果你准备开始实践,建议先把本文的 AT 模式示例完整跑通。成功和失败两个场景都验证一遍,观察日志和undo_log的行为,这是建立分布式事务手感最快的方式。跑通之后,再去看 TCC 和 SAGA 的官方示例,评估它们与自家业务形态的匹配度。
需要记住的是,Seata 本身不会消解决分布式带来的所有复杂性,真正决定方案上限的,是事务边界的设计、异常链路的管理和业务幂等的落地。先把这几件事做好,再引入 Seata,你会发现分布式事务并没有想象中那么可怕。