Skip to content
Home
Go back

Spring Cloud Seata:跨服务的分布式事务

回顾:跨服务数据一致性的困境

到上一篇为止,我们系统的可靠性已经上了一个台阶:熔断、降级、限流护着,一个服务挂了不会拖垮整个系统。

但可靠性不等于正确性。考虑一个真实的业务场景:

  1. order-service 收到下单请求,在自己的数据库里创建了一条订单记录
  2. 然后调用 payment-service 扣款
  3. 扣款失败

现在怎么办?订单已经入库了,钱没扣成。如果不管,就是一笔坏账。如果手动写代码把订单删掉,那几十个跨服务调用组合起来,补偿代码量会比业务代码还多。

这就是分布式事务要解决的问题:多个服务、多个数据库之间,要么全成功、要么全回滚。

Seata 是什么

Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务框架。它提供三种事务模式:

模式特点适用场景
AT自动生成反向 SQL,业务代码零侵入基于关系型数据库的微服务
TCC需要手写 Try / Confirm / Cancel 三个接口对一致性要求极高、需要自定义资源
Saga长事务,通过正向 + 补偿串联流程长、不能加锁的场景

我们这一篇聚焦 AT 模式——对初学者最友好、业务代码改动最少的一种。

AT 模式原理

AT 模式的核心思想是两阶段提交,但它不像传统的 XA 协议那样全程锁资源。它的做法更轻量:

一阶段:执行业务 SQL + 记录 undo_log

  1. order-service 执行 INSERT INTO orders ...,Seata 自动解析这条 SQL,算出回滚这条 INSERT 需要执行什么 DELETE,把反向 SQL 写入 undo_log
  2. payment-service 执行 UPDATE account SET balance = balance - 100 ...,同样自动生成反向 SQL 写入 undo_log
  3. 两个服务的业务数据已经写入了各自的数据库,但事务还没定案

二阶段:提交或回滚

整个过程中,你写的业务代码里不需要出现任何补偿逻辑。Seata 在你执行 SQL 时偷偷记下了 undo_log,回滚时再拿出来用。

Seata 三角色

Seata 把分布式事务的参与者分成三个角色:

 TM (事务发起者)

  │ 开启/提交/回滚全局事务

 TC (事务协调者)  ◄──── Seata Server

  │ 调度分支事务的提交/回滚

 RM (资源管理者)  ◄──── order-service / payment-service

三者协作流程:

TM 开启全局事务 → RM1 执行 SQL + 写 undo_log → RM2 执行 SQL + 写 undo_log → TM 提交/回滚

                                                            ┌───────────────────────┘

                                              TC 通知所有 RM:提交(删 undo_log)
                                              或 回滚(执行反向 SQL)

动手:订单 + 支付分布式事务

环境准备:Seata Server

docker-compose.yml 中添加 Seata Server:

seata-server:
  image: seataio/seata-server:2.0.0
  container_name: seata-server
  ports:
    - "8091:8091"
    - "7091:7091"
  environment:
    - SEATA_IP=localhost

各个服务引入依赖

order-servicepayment-service 都加入:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

配置 seata

在两个服务的 application.yml 中加入:

seata:
  tx-service-group: default_tx_group
  service:
    vgroup-mapping:
      default_tx_group: default
    grouplist:
      default: localhost:8091

创建 undo_log 表

在每个服务连接的数据库中执行:

CREATE TABLE IF NOT EXISTS 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)
);

这张表就是 Seata 的「保险柜」——每个本地事务执行时,反向 SQL 就存在这里。

标记全局事务

order-service 的创建订单方法上加 @GlobalTransactional

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private PaymentClient paymentClient;

    @GlobalTransactional
    public void createOrder(Order order) {
        // 本地操作:插入订单
        orderMapper.insert(order);

        // 远程调用:扣款
        paymentClient.deduct(order.getAmount());
    }
}

一个注解就够了。正常执行时两个服务的操作都会提交;如果 paymentClient.deduct() 抛异常,Seata 会自动回滚 orderMapper.insert()

验证

正常提交

发送一个正常的下单请求:

curl -X POST http://localhost:8082/order -d '{"amount": 100}'

检查数据库:orders 表有记录,account 表余额减少了 100。undo_log 表对应记录已清空。

回滚

模拟异常:在 payment-service 中让扣款逻辑抛异常,再发下单请求。

检查数据库:orders 表没有新记录,account 表余额没变。分布式事务回滚成功。

小结与下篇预告

这一篇我们解决了跨服务的原子性问题:

  1. 认识分布式事务的困境——单服务事务管不了跨服务的数据一致性
  2. Seata 三种模式,聚焦 AT 模式:业务 SQL + undo_log 自动反向 SQL
  3. TC / TM / RM 三角色协作模型
  4. 二阶段提交:一阶段执行业务 SQL + 不改业务代码,二阶段删 undolog 或执行反向 SQL
  5. 动手:Docker Compose 启动 Seata Server,@GlobalTransactional 一个注解收口全局事务

现在服务已经能跑、能稳、能保证数据一致了。但还有一个问题:当一个请求从 Gateway → order-service → payment-service 一路走下去,如果中途报错了或者慢了,你只能看到一个最终的报错信息,中间环节发生了什么完全黑盒。

下一篇,我们用 Micrometer + Brave + Zipkin 给整个调用链装上追踪系统,让每次请求的全链路都可视化。



Previous Post
Spring Cloud 链路追踪:一个 Trace ID 串联所有服务
Next Post
Spring Cloud 系列合集