跳转到内容
123xiao | 无名键客

《微服务架构中分布式事务的实战选型与落地:Seata、可靠消息最终一致性与补偿机制对比》

字数: 0 阅读时长: 1 分钟

背景与问题

做微服务之后,业务拆开了,事务也跟着碎了。

单体时代,一个本地数据库事务基本能解决大部分问题:扣库存、创建订单、冻结余额,放进一个事务里,要么都成功,要么都回滚。但到了微服务架构里,这几个动作可能分别在订单服务、库存服务、账户服务里,各自有独立数据库。此时再指望一个数据库事务兜底,已经不现实。

最典型的问题有三类:

  1. 强一致要求高
    比如资金扣减、额度控制,这类场景通常不能接受“过几秒再一致”。

  2. 高吞吐优先,允许短暂不一致
    比如下单后发积分、发优惠券、更新推荐画像。失败后重试或补偿通常可以接受。

  3. 跨系统、跨组织边界
    有些调用方甚至不是自己团队维护,根本没法要求对方接入统一事务框架。

所以,“分布式事务怎么选”从来不是一个纯技术问题,而是一个业务一致性等级、性能目标、团队控制力、运维复杂度的综合取舍题。

这篇文章我会从实战角度,把常见三类方案放在一起看:

  • Seata(以 AT / TCC 为代表)
  • 可靠消息最终一致性
  • 补偿机制(Saga / 业务补偿)

目标不是讲概念大全,而是回答三个更实在的问题:

  • 什么场景适合哪种方案?
  • 落地时代码怎么写?
  • 线上容易踩哪些坑,怎么排查?

先给结论:怎么选更靠谱

如果你时间有限,先看这张图。

flowchart TD
    A[开始:是否需要分布式事务] --> B{是否要求强一致且可接受较高耦合?}
    B -- 是 --> C{参与服务是否可统一接入框架?}
    C -- 是 --> D[优先 Seata AT/TCC]
    C -- 否 --> E[考虑 TCC 或业务补偿]
    B -- 否 --> F{是否允许异步化和短暂不一致?}
    F -- 是 --> G[可靠消息最终一致性]
    F -- 否 --> H[补偿机制/Saga 拆分流程]
    D --> I[资金/库存预留/核心链路]
    G --> J[积分/通知/非核心派生数据]
    H --> K[跨系统长流程/人工介入]

我自己的经验可以概括成一句话:

  • 核心链路、短事务、自己能控全链路:优先考虑 Seata
  • 高吞吐、可异步、允许延迟一致:优先考虑 可靠消息最终一致性
  • 长流程、多系统、失败不可避免且需要显式回退动作:优先考虑 补偿机制

这三种方案并不是互斥关系,很多成熟系统其实是组合拳

  • 下单主流程用 Seata 控制订单和库存
  • 发券、发短信、埋点用 MQ 做最终一致
  • 跨境支付或外部供应商对接用补偿机制兜底

方案对比与取舍分析

先把三种方案放到一张表里看。

方案一致性性能侵入性适用场景典型风险
Seata AT较强自研服务、关系型数据库、本地事务改造少全局锁、回滚失败、SQL 限制
Seata TCC中低资金、库存冻结等可预留资源场景开发复杂、幂等空回滚悬挂
可靠消息最终一致性最终一致异步业务、通知类、积分等消息重复、丢失、消费失败
补偿机制 / Saga最终一致长流程、多系统、外部接口补偿失败、状态机复杂、人工介入

选型维度建议

1. 先问业务能接受多长时间不一致

  • 不能接受:往 Seata / TCC 靠
  • 几秒到几分钟可以接受:MQ 最终一致
  • 可能持续更久,需要人处理:补偿机制

2. 再问是否能控制参与方

如果所有服务都在自己团队边界内,统一引入 Seata 难度会小很多。
如果有第三方支付、外部仓储、老系统,就别指望全链路统一事务框架了,补偿往往更现实。

3. 最后看吞吐量和成本

  • 订单高峰每秒几千以上,主流程尽量不要塞太多同步操作
  • Seata 带来的额外分支事务、锁冲突、undo log 开销,需要提前压测
  • MQ 方案扩展性更好,但要建立完整的重试、幂等、死信和对账能力

核心原理

一、Seata 的核心思路

Seata 最常见的是 AT 模式

它的思路不是搞一个真正意义上的跨库两阶段提交,而是:

  1. 业务执行本地事务
  2. 在提交前记录回滚日志 undo_log
  3. 向事务协调器(TC)注册分支事务
  4. 全局事务成功则提交
  5. 全局事务失败则根据 undo_log 反向回滚

角色一般是:

  • TC(Transaction Coordinator):协调全局事务
  • TM(Transaction Manager):开启、提交、回滚全局事务
  • RM(Resource Manager):管理分支事务和本地资源
sequenceDiagram
    participant Client as 调用方
    participant Order as 订单服务(TM/RM)
    participant Stock as 库存服务(RM)
    participant TC as Seata TC

    Client->>Order: 创建订单
    Order->>TC: 开启全局事务
    TC-->>Order: 返回 XID
    Order->>Stock: 扣减库存(XID 透传)
    Stock->>TC: 注册分支事务
    Stock->>Stock: 写业务数据 + undo_log
    Stock-->>Order: 成功
    Order->>Order: 写订单数据 + undo_log
    Order->>TC: 提交全局事务
    TC-->>Order: 全局提交成功

Seata AT 的优点

  • 对业务代码侵入相对较低
  • 基于本地事务,开发心智负担小
  • 很适合“订单 + 库存”这种短流程事务

Seata AT 的边界

  • 对 SQL 和数据源有要求,不是所有写法都适合
  • 热点行容易出现全局锁冲突
  • 长事务体验会很差,锁持有时间长,吞吐会明显下降

二、可靠消息最终一致性的核心思路

这个方案其实是很多电商、营销系统的主力方案。

核心思想很朴素:

  1. 在本地事务中,业务数据待发送消息一起落库
  2. 本地事务提交成功后,异步把消息投递到 MQ
  3. 消费方处理自己的本地事务
  4. 如果某一步失败,就重试、幂等、对账补偿

其中最关键的是 Outbox 模式
不要“先写数据库,再直接发 MQ”而没有中间持久化,否则数据库成功、MQ 失败时,消息就丢了。

flowchart LR
    A[订单服务本地事务] --> B[写订单表]
    A --> C[写 outbox 消息表]
    C --> D[消息投递任务]
    D --> E[MQ]
    E --> F[积分服务消费]
    F --> G[本地事务处理]
    G --> H[更新消费状态/幂等记录]

可靠消息方案的优点

  • 吞吐高,链路解耦明显
  • 很适合非核心派生动作
  • 对跨团队、跨系统协作更友好

边界

  • 天然不是强一致
  • 重试和幂等必须自己兜住
  • 需要完善的可观测性,不然线上很难排查

三、补偿机制的核心思路

补偿机制常用于长事务,也常被归到 Saga 思路里。

它不是要求所有步骤同时成功,而是:

  1. 步骤按顺序执行
  2. 如果某一步失败,就执行之前步骤的“反向补偿动作”

例如:

  • 创建订单
  • 锁库存
  • 扣款
  • 创建发货单

如果扣款失败,就需要:

  • 释放库存
  • 取消订单

这里有个很关键的认知:
补偿不是数据库回滚,它是业务语义上的撤销。

所以补偿动作需要业务自己定义,而且并不总能做到“完全恢复现场”。

stateDiagram-v2
    [*] --> Created
    Created --> StockLocked: 锁库存成功
    StockLocked --> Paid: 扣款成功
    Paid --> ShippingCreated: 创建发货单
    ShippingCreated --> Completed

    StockLocked --> Cancelled: 扣款失败/补偿取消订单
    Paid --> Refunding: 发货创建失败
    Refunding --> Cancelled: 退款并释放资源

实战代码(可运行)

下面我用 Spring Boot 风格给出两个可运行示例:

  1. Seata AT 模式示例
  2. Outbox + 定时投递的可靠消息示例

为了让核心逻辑更清楚,我尽量保持代码短一些。


示例一:Seata AT 模式下的下单扣库存

场景

  • 订单服务:创建订单
  • 库存服务:扣减库存
  • 两者必须一起成功或一起失败

关键依赖

```xml
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>1.5.2</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
</dependencies>

上面这个代码块我特意保留最常见依赖。实际项目中版本要和 Seata Server、Spring Boot 版本一起对齐,不要单独抄。

### 表结构

#### 订单表

```sql
CREATE TABLE t_order (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    product_id BIGINT NOT NULL,
    amount INT NOT NULL,
    status VARCHAR(20) NOT NULL,
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

库存表

CREATE TABLE t_stock (
    product_id BIGINT PRIMARY KEY,
    count INT NOT NULL
);

INSERT INTO t_stock(product_id, count) VALUES (1, 100);

Seata undo_log 表

CREATE TABLE undo_log (
  id BIGINT NOT NULL AUTO_INCREMENT,
  branch_id BIGINT NOT NULL,
  xid VARCHAR(128) 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,
  PRIMARY KEY (id),
  UNIQUE KEY ux_undo_log (xid, branch_id)
);

订单服务代码

package com.example.order.service;

import com.example.order.client.StockClient;
import com.example.order.mapper.OrderMapper;
import com.example.order.model.OrderDO;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    private final OrderMapper orderMapper;
    private final StockClient stockClient;

    public OrderService(OrderMapper orderMapper, StockClient stockClient) {
        this.orderMapper = orderMapper;
        this.stockClient = stockClient;
    }

    @GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class)
    @Transactional
    public Long createOrder(Long userId, Long productId, Integer amount) {
        stockClient.deduct(productId, amount);

        OrderDO order = new OrderDO();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setAmount(amount);
        order.setStatus("CREATED");
        orderMapper.insert(order);

        return order.getId();
    }
}

库存服务代码

package com.example.stock.service;

import com.example.stock.mapper.StockMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class StockService {

    private final StockMapper stockMapper;

    public StockService(StockMapper stockMapper) {
        this.stockMapper = stockMapper;
    }

    @Transactional
    public void deduct(Long productId, Integer amount) {
        int updated = stockMapper.deduct(productId, amount);
        if (updated == 0) {
            throw new IllegalStateException("库存不足");
        }
    }
}

Mapper SQL

package com.example.stock.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

@Mapper
public interface StockMapper {

    @Update("UPDATE t_stock SET count = count - #{amount} " +
            "WHERE product_id = #{productId} AND count >= #{amount}")
    int deduct(@Param("productId") Long productId, @Param("amount") Integer amount);
}

OpenFeign 调用接口

package com.example.order.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "stock-service")
public interface StockClient {

    @PostMapping("/stock/deduct")
    void deduct(@RequestParam("productId") Long productId,
                @RequestParam("amount") Integer amount);
}

这个示例怎么验证

  1. 库存初始 100
  2. 下单 amount=2,订单表新增,库存减 2
  3. 人为让订单插入后抛异常,观察库存是否自动回滚

例如在 createOrder 最后临时加一句:

if (true) {
    throw new RuntimeException("模拟异常");
}

如果 Seata 配置正确,你会看到:

  • 订单数据回滚
  • 库存数据也回滚

这就是 Seata AT 的基本效果。


示例二:Outbox 模式实现可靠消息最终一致性

场景

  • 订单创建成功后,给积分服务发送“订单完成”事件
  • 积分发放允许几秒内到账
  • 主流程不能被积分服务拖慢

表结构

订单表

CREATE TABLE biz_order (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    amount DECIMAL(10,2) NOT NULL,
    status VARCHAR(20) NOT NULL,
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

outbox 消息表

CREATE TABLE outbox_event (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    event_type VARCHAR(64) NOT NULL,
    biz_key VARCHAR(64) NOT NULL,
    payload TEXT NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'NEW',
    retry_count INT NOT NULL DEFAULT 0,
    next_retry_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_biz_key_event (biz_key, event_type)
);

订单创建:业务数据与消息同事务落库

package com.example.outbox.service;

import com.example.outbox.mapper.OrderMapper;
import com.example.outbox.mapper.OutboxMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

@Service
public class OrderAppService {

    private final OrderMapper orderMapper;
    private final OutboxMapper outboxMapper;
    private final ObjectMapper objectMapper;

    public OrderAppService(OrderMapper orderMapper,
                           OutboxMapper outboxMapper,
                           ObjectMapper objectMapper) {
        this.orderMapper = orderMapper;
        this.outboxMapper = outboxMapper;
        this.objectMapper = objectMapper;
    }

    @Transactional
    public Long createOrder(Long userId, BigDecimal amount) throws Exception {
        Long orderId = orderMapper.insertAndReturnId(userId, amount, "CREATED");

        Map<String, Object> event = new HashMap<>();
        event.put("orderId", orderId);
        event.put("userId", userId);
        event.put("amount", amount);

        String payload = objectMapper.writeValueAsString(event);
        outboxMapper.insert("ORDER_CREATED", String.valueOf(orderId), payload);

        return orderId;
    }
}

定时任务投递消息

这里用“打印日志模拟 MQ 发送”,你替换成 RocketMQ / Kafka 都可以。

package com.example.outbox.job;

import com.example.outbox.mapper.OutboxMapper;
import com.example.outbox.model.OutboxEvent;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.List;

@Component
public class OutboxPublishJob {

    private final OutboxMapper outboxMapper;

    public OutboxPublishJob(OutboxMapper outboxMapper) {
        this.outboxMapper = outboxMapper;
    }

    @Scheduled(fixedDelay = 3000)
    public void publish() {
        List<OutboxEvent> events = outboxMapper.findPending(LocalDateTime.now(), 20);
        for (OutboxEvent event : events) {
            try {
                // 这里替换成真正的 MQ 发送逻辑
                System.out.println("send message: " + event.getPayload());

                outboxMapper.markSuccess(event.getId());
            } catch (Exception ex) {
                outboxMapper.markRetry(
                        event.getId(),
                        event.getRetryCount() + 1,
                        LocalDateTime.now().plusSeconds(30)
                );
            }
        }
    }
}

消费端幂等处理

这是可靠消息方案最容易被忽略的点。
消息一定可能重复,所以消费端必须幂等。

CREATE TABLE consumer_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    msg_key VARCHAR(64) NOT NULL,
    consumer_group VARCHAR(64) NOT NULL,
    status VARCHAR(20) NOT NULL,
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_msg_group (msg_key, consumer_group)
);
package com.example.points.service;

import com.example.points.mapper.ConsumerLogMapper;
import com.example.points.mapper.PointsMapper;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class PointsConsumerService {

    private final ConsumerLogMapper consumerLogMapper;
    private final PointsMapper pointsMapper;

    public PointsConsumerService(ConsumerLogMapper consumerLogMapper,
                                 PointsMapper pointsMapper) {
        this.consumerLogMapper = consumerLogMapper;
        this.pointsMapper = pointsMapper;
    }

    @Transactional
    public void onMessage(String msgKey, Long userId, Integer points) {
        try {
            consumerLogMapper.insert(msgKey, "points-service", "DONE");
        } catch (DuplicateKeyException ex) {
            return;
        }

        pointsMapper.addPoints(userId, points);
    }
}

运行逻辑

  • 订单创建成功,一定会写入 outbox
  • 就算 MQ 短暂不可用,定时任务也会重试投递
  • 就算消息重复,消费端也不会重复加积分

这套东西一旦搭好,后续很多异步链路都能复用。


常见坑与排查

这一部分我建议你认真看,因为真正花时间的往往不是“方案选哪一个”,而是“为什么线上它没按预期工作”。

1. Seata 全局事务没生效

现象

  • 订单回滚了,库存没回滚
  • 日志里看不到 XID 透传
  • 子服务压根不知道自己在分布式事务里

常见原因

  1. 数据源没被 Seata 代理
  2. RPC 调用没透传 XID
  3. 入口方法不是代理对象调用
  4. 异常被吃掉,导致没触发全局回滚

排查建议

  • 打印 RootContext.getXID() 看调用链是否有值
  • 查看子服务日志是否注册 branch
  • 检查是否用了正确的 DataSourceProxy
  • 确认异常没有被 try-catch 后静默吞掉

2. Seata AT 模式出现锁冲突、性能抖动

现象

  • 高峰期下单 RT 飙升
  • 日志里出现 lock retry
  • 某个热点商品经常扣库存失败

原因分析

AT 模式本质上会对相关数据形成全局锁语义。
如果所有请求都在更新同一行,例如一个爆款商品库存只有一行数据,那就很容易形成热点竞争。

解决建议

  • 把热点库存拆分桶化
  • 缩短事务时间,不要在全局事务里做远程慢调用
  • 降低单事务包含的步骤
  • 对极热点资源改用预扣 / 异步削峰设计

我当时踩过一个坑:把优惠券校验、活动资格、库存扣减都放在一个全局事务里,结果高峰期 RT 一路飙。后来拆成“主链路强一致 + 非核心异步处理”后才稳定下来。


3. 可靠消息方案出现重复消费

现象

  • 用户积分被加了两次
  • 短信发送重复
  • 库存被重复释放

根因

MQ 的“至少一次投递”语义决定了重复消息是常态,不是异常

排查与处理

  • 检查消费端是否有唯一约束或幂等表
  • 检查业务操作是否天然幂等
  • 消费成功确认时机是否正确
  • 是否存在“业务成功但 ack 失败”导致重复投递

最佳实践

  • 每条消息必须有业务唯一键
  • 消费端落库时使用唯一索引去重
  • 不要只靠 Redis setnx 做幂等,重启和过期后有坑
  • 真正关键链路,幂等记录要放数据库持久化

4. Outbox 消息堆积,最终一致性变成“迟迟不一致”

现象

  • 订单成功了,但积分半小时才到
  • outbox 表积压越来越多
  • 定时任务扫描越来越慢

原因

  • 扫描条件没走索引
  • 单批发送量过小
  • 重试策略过于激进或过于保守
  • 下游消费能力不足

排查思路

EXPLAIN
SELECT * FROM outbox_event
WHERE status IN ('NEW', 'RETRY')
  AND next_retry_time <= NOW()
ORDER BY id
LIMIT 20;

看是否命中索引,如果没有,先补索引。
其次观察:

  • 每分钟新增消息数
  • 每分钟成功投递数
  • 重试消息比例
  • 最老未完成消息滞留时间

这几个指标比单纯盯错误日志更有用。


5. 补偿机制越做越乱

现象

  • 业务状态过多
  • 失败路径没人说得清
  • 人工处理越来越频繁

根因

补偿方案本身就是把复杂度从“数据库事务”转移到了“业务状态机”。

建议

  • 先把状态机画出来,再写代码
  • 每一步都要有明确的前置条件和补偿条件
  • 补偿动作必须幂等
  • 预留人工介入入口,不要幻想 100% 自动化

安全/性能最佳实践

这一节我分成“通用”和“按方案”两部分讲。

通用最佳实践

1. 以业务一致性等级驱动技术方案

不要因为团队刚接入了 Seata,就把所有跨服务调用都包进全局事务。
也不要因为 MQ 很香,就把资金扣减做成异步。

技术方案永远服务业务目标:

  • 钱、库存、额度:先考虑强一致
  • 积分、通知、日志、画像:优先异步化

2. 设计幂等键

无论 Seata、MQ 还是补偿,幂等都是底层生存能力。

建议每个核心业务动作都有一个幂等键,例如:

  • orderId
  • paymentNo
  • bizType + bizId
  • requestId

3. 建立对账机制

只靠事务框架和重试机制还不够。
真正上线稳定的系统,通常都有对账任务:

  • 订单与库存对账
  • 订单与支付对账
  • outbox 与 MQ 投递状态对账
  • 消费记录与业务记录对账

对账是“最后一道保险”。


Seata 的最佳实践

1. 不要把长耗时逻辑塞进全局事务

例如:

  • 调第三方接口
  • 做复杂报表计算
  • 等待人工审批
  • 执行大批量更新

全局事务应该尽量短、小、快。

2. SQL 保持简单可回滚

AT 模式对 SQL 支持不是无限制的。
复杂 SQL、批量更新、非标准写法都要提前验证,不要等上线后再看 undo 是否可用。

3. 注意热点资源

  • 热门商品库存
  • 账户余额主表
  • 单行配置表

这些表如果被大量并发更新,Seata 的锁竞争会很明显。

4. 保护事务上下文

XID 属于敏感上下文信息,跨服务透传要按框架规范来,不要自己乱拼接 Header。
同时,日志里打印时注意脱敏和采样,避免无谓暴露链路细节。


可靠消息方案的最佳实践

1. 一定使用本地消息表或事务消息

最忌讳的是下面这种写法:

// 反例:数据库成功了,MQ 发送失败就丢消息
createOrderInDb();
mqProducer.send(msg);

正确思路至少要做到:

  • 订单成功与消息待发送记录同事务落库
  • 由后台任务可靠投递

2. 消费端优先做“先去重,再处理”

这样即使业务执行到一半失败,也能通过事务边界控制一致性。

3. 重试要有退避和上限

不要固定每秒重试一次,这会把故障放大。
建议:

  • 指数退避或阶梯退避
  • 超过阈值进入死信队列
  • 配合告警和人工处理

4. 保护消息内容

消息里尽量不要塞敏感明文,比如身份证号、银行卡号。
确实需要传,也尽量传业务键,到消费端再查明细。


补偿机制的最佳实践

1. 补偿动作要比正向动作更稳

正向流程可以失败重试,补偿流程如果也不稳定,系统很快就会积累大量脏状态。

2. 状态机驱动,而不是 if-else 满天飞

建议明确状态流转表:

当前状态事件下一个状态动作
CREATED锁库存成功STOCK_LOCKED记录锁单
STOCK_LOCKED扣款失败CANCELLED释放库存
PAID发货失败REFUNDING发起退款

3. 预留人工兜底

有些事情程序确实处理不了,例如外部系统返回成功但网络超时、状态不明确。
这时要有后台页面和运营流程支持人工修复。


容量估算与落地建议

对于 architecture 类型文章,我更想补一段很多团队容易忽略的内容:容量估算

1. Seata 的容量关注点

主要看:

  • 全局事务 TPS
  • 平均事务时长
  • 热点资源竞争程度
  • TC 集群和存储配置

一个简单判断方式:

如果你的核心链路高峰达到几千 TPS,且大量请求都更新相同资源行,那么 Seata AT 很可能先遇到锁竞争瓶颈,而不是 CPU 不够。

2. Outbox 的容量关注点

主要看:

  • 每秒生成消息量
  • 投递任务吞吐
  • 消费积压增长速度
  • 幂等表写入压力

例如每秒 2000 单,每单触发 3 条消息,就是 6000 msg/s。
这时候 outbox 表如果还靠单线程扫描,基本很快就顶不住了,需要:

  • 分片扫描
  • 批量发送
  • 分区表或归档策略
  • 消费端并发扩容

3. 补偿机制的容量关注点

主要看“异常流量占比”。

补偿链路平时可能很轻,但一旦下游大面积失败,补偿任务会暴涨。
所以补偿系统本身也要具备:

  • 限流
  • 重试退避
  • 失败隔离
  • 人工接管

一套比较实用的落地组合

如果让我给中型业务系统一套相对稳妥的默认组合,我会这么建议:

核心主链路

  • 订单、库存、账户这类强一致动作
  • 优先 Seata AT 或 TCC
  • 保持事务短小

派生异步链路

  • 发积分、发通知、更新画像、同步搜索索引
  • 使用 Outbox + MQ 最终一致性
  • 消费端严格幂等

长流程和外部系统协作

  • 支付确认、供应链履约、退款逆向
  • 使用状态机 + 补偿机制
  • 补充对账与人工兜底

这个组合的优点是:
把强一致能力用在最贵的地方,把高吞吐能力用在最合适的地方。


总结

分布式事务没有银弹,只有权衡。

如果把这篇文章压缩成几条可执行建议,我会给出下面这份版本:

  1. 先分级,再选型

    • 强一致核心链路:Seata / TCC
    • 可异步派生链路:可靠消息最终一致性
    • 长流程跨系统:补偿机制
  2. 不要迷信单一方案

    • 真正成熟的微服务系统大多是组合使用
    • 一个系统里同时存在 Seata、MQ、补偿是正常现象
  3. Outbox、幂等、对账,三件套必须有

    • 没有它们,最终一致性很容易退化成“不可控的不一致”
  4. Seata 适合短事务,不适合长事务

    • 尤其是热点资源更新时,要重点压测锁冲突
  5. 补偿不是回滚,是业务撤销

    • 一定要显式设计状态机和人工兜底

最后给一个比较直白的边界判断:

  • 如果你要解决的是“下单时库存不能错”,优先考虑 Seata
  • 如果你要解决的是“订单成功后积分别丢”,优先考虑 可靠消息
  • 如果你要解决的是“跨多个外部系统的履约流程失败后如何收拾残局”,优先考虑 补偿机制

做架构时,最怕的是“为了统一而统一”。
真正好的方案,往往不是最“高级”的,而是团队能理解、能压测、能监控、能兜底的那一种。


分享到:

上一篇
《区块链节点数据索引与查询优化实战:面向中级开发者的架构设计与性能调优》
下一篇
《集群架构实战:基于 Kubernetes 的高可用服务部署与故障自动恢复设计》