Skip to content
第 227 / 250 章架构⏱ 12 分钟阅读

第 227 章:分布式事务

学习目标

  • 理解 CAP / BASE 理论
  • 掌握 2PC / TCC / Saga 模式
  • 学会 Seata 实战
  • 选择合适的事务方案

一、为什么需要分布式事务

本地 @Transactional 只在单服务内有效,跨服务必须用分布式事务。

二、CAP 理论

维度含义
Consistency所有节点同时看到相同数据
Availability每个请求都能得到响应
Partition tolerance网络分区时仍能工作

定理:三者不可兼得。

组合选择
CPZooKeeper、Etcd(放弃 A)
APEureka、Cassandra(放弃 C)
CA单机数据库(放弃 P)

分布式系统P 必选,只能在 C 和 A 之间权衡。

三、BASE 理论

CAP 的"实用妥协"思想:

原则含义
BA(Basically Available)基本可用,允许降级
S(Soft State)软状态,允许中间态不一致
E(Eventually Consistent)最终一致

主流分布式系统都用 BASE,而不是 ACID。

四、事务模式

4.1 2PC(两阶段提交)

强一致协议,分两阶段:

阶段:

  1. Prepare:协调器问所有参与者"能不能提交?"
  2. Commit:所有参与者都 OK,协调器让大家提交

问题:

  • 同步阻塞
  • 协调器单点故障
  • 数据不一致(部分 commit)

4.2 3PC(改进版 2PC)

CanCommit 阶段,降低阻塞,但仍非完美。

4.3 TCC(补偿事务)

特点:

  • 每个业务有 Try / Confirm / Cancel 三个操作
  • 业务侵入性强
  • 性能较好

4.4 Saga(长事务拆分)

最终一致,拆为多个本地事务 + 补偿。

特点:

  • 每个子事务有对应补偿
  • 适合业务流程长的场景
  • 业务改造成本低

4.5 本地消息表

特点:

  • 业务无侵入
  • 适合最终一致性场景
  • 用本地表保证业务与消息一致

4.6 模式对比

模式一致性性能侵入性适用
2PC单库 / XA
TCC金融
Saga最终订单
本地消息表最终一般业务
MQ 事务消息最终通用
Seata AT通用首选

五、Seata 实战

Seata 是阿里开源的分布式事务解决方案,支持 AT、TCC、Saga、XA 四种模式。

5.1 核心角色

5.2 部署 Seata Server

yaml
# docker-compose.yml
services:
  seata-server:
    image: seataio/seata-server:1.6.1
    ports: ["8091:8091"]
    environment:
      - SEATA_PORT=8091
      - STORE_MODE=db
      - DB_URL=jdbc:mysql://mysql:3306/seata
      - DB_USER=seata
      - DB_PASSWORD=seata
    depends_on:
      - mysql

注册到 Nacos:

properties
# registry.conf
registry {
  type = "nacos"
  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = "public"
  }
}

config {
  type = "nacos"
  nacos {
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
  }
}

5.3 AT 模式(自动)

两阶段 + 补偿,Seata 自动接管本地事务。

客户端依赖

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

配置

yaml
spring:
  cloud:
    alibaba:
      seata:
        tx-service-group: my_test_tx_group

seata:
  registry:
    type: nacos
  config:
    type: nacos
  service:
    vgroup-mapping:
      my_test_tx_group: default

全局事务入口

java
@Service
public class OrderService {

    @Autowired
    private OrderRepo orderRepo;
    @Autowired
    private InventoryClient inventoryClient;
    @Autowired
    private PaymentClient paymentClient;

    // 全局事务
    @GlobalTransactional(name = "createOrder", rollbackFor = Exception.class)
    public void createOrder(OrderRequest req) {
        // 1. 本地
        Order order = new Order(req);
        orderRepo.save(order);

        // 2. 远程(库存服务)
        inventoryClient.deduct(req.getSkuId(), req.getQuantity());

        // 3. 远程(支付服务)
        paymentClient.pay(req.getUserId(), req.getAmount());
    }
}

原理

每个 RM 在本地提交时记录前后镜像(快照 + undo_log),TC 根据情况决定 commit 或 rollback。

数据源代理

AT 模式需要 Seata 接管数据源:

java
@Configuration
public class DataSourceConfig {

    @Bean
    @Primary
    public DataSourceProxy dataSourceProxy(DataSource druidDataSource) {
        return new DataSourceProxy(druidDataSource);
    }
}

5.4 TCC 模式(手工)

适用于性能高自动接管困难的场景。

接口定义:

java
public interface InventoryTccService {

    @TwoPhaseBusinessAction(
        name = "deductInventory",
        commitMethod = "commit",
        rollbackMethod = "rollback"
    )
    boolean tryDeduct(
        @BusinessActionContextParameter("skuId") String skuId,
        @BusinessActionContextParameter("qty") int qty
    );

    boolean commit(BusinessActionContext ctx);

    boolean rollback(BusinessActionContext ctx);
}

实现:

java
@Service
@Slf4j
public class InventoryTccServiceImpl implements InventoryTccService {

    @Autowired
    private InventoryMapper inventoryMapper;
    @Autowired
    private FrozenMapper frozenMapper;

    @Override
    public boolean tryDeduct(String skuId, int qty) {
        // 1. 检查库存
        if (inventoryMapper.getStock(skuId) < qty) {
            return false;
        }
        // 2. 冻结
        frozenMapper.freeze(skuId, qty);
        return true;
    }

    @Override
    public boolean commit(BusinessActionContext ctx) {
        // 真正扣减
        String skuId = (String) ctx.getActionContext("skuId");
        int qty = (int) ctx.getActionContext("qty");
        inventoryMapper.deduct(skuId, qty);
        frozenMapper.unfreeze(skuId, qty);
        return true;
    }

    @Override
    public boolean rollback(BusinessActionContext ctx) {
        String skuId = (String) ctx.getActionContext("skuId");
        int qty = (int) ctx.getActionContext("qty");
        // 解冻
        frozenMapper.unfreeze(skuId, qty);
        return true;
    }
}

5.5 Saga 模式

通过状态机编排:

json
{
  "name": "createOrder",
  "compensateCancel": true,
  "steps": [
    {
      "name": "createOrder",
      "compensate": "cancelOrder"
    },
    {
      "name": "deductInventory",
      "compensate": "compensateInventory"
    },
    {
      "name": "pay",
      "compensate": "refundPay"
    }
  ]
}

六、MQ 事务消息

RocketMQ 5.x 支持事务消息。

java
// 发送事务消息
TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
    "order-events",
    MessageBuilder.withPayload(new OrderCreatedEvent(...)).build(),
    new TransactionSendCallback() {
        @Override
        public LocalTransactionState executeLocalTransactionBranch(Message msg, Object arg) {
            try {
                orderRepo.save(order);
                return LocalTransactionState.COMMIT_MESSAGE;
            } catch (Exception e) {
                return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        }
    }
);

七、最佳实践

7.1 选择决策

业务推荐方案
支付TCC
订单Seata AT / Saga
库存TCC / Saga
退款MQ 事务消息
通知本地消息表

7.2 何时避免分布式事务

  1. 能拆分为独立流程的不需要事务
  2. 最终一致 + 补偿 替代强一致
  3. 用幂等 + 重试代替回滚
  4. CQRS:读写分离,避开分布式事务

7.3 幂等保证

无论事务如何,业务操作必须幂等。

java
@Idempotent(key = "#req.orderId", type = IdempotentTypeEnum.SPEL)
public Order createOrder(OrderRequest req) {
    // 即使重试,只能创建一次订单
    if (orderRepo.exists(req.getOrderId())) {
        return orderRepo.findById(req.getOrderId());
    }
    return orderRepo.save(new Order(req));
}

7.4 异常场景

场景处理
Seata Server 挂TC 持久化,重启回放
业务悬挂(try 后没收到 commit)定时补偿(扫描冻结记录)
资源一直加锁加 timeout
undo_log 太大定期清理已成功事务

八、本章小结

模式一致性性能适用
2PC单 DB
TCC金融
Saga最终长流程
AT弱强通用
MQ 事务消息最终通用

动手练习

  1. 部署 Seata Server(单机版)
  2. 写一个"下单-扣库存-支付"的 Seata AT 例子,人工触发其中一个失败,观察回滚
  3. 用 TCC 实现"扣库存"接口
  4. 用 Saga 配置一个完整退款流程

推荐阅读


下一章:第 228 章:分布式锁进阶

本站基于 VitePress 构建 · 由 Codebook 团队维护