分布式事务实战:微服务跨服务数据一致性解决方案

如何保证事务?

事务一般都如何保证,在项目中,单体项目大概用注解都可以解决。对于微服务或者跨服务的呢?常用的解决方案。

微服务里的事务,核心不是"有没有一个注解可以解决",而是要先接受一个事实:

@Transactional 只能保证当前服务、当前数据库连接里的本地事务。

一旦涉及:

  • 服务 A 调服务 B
  • 服务 A 写自己的库,服务 B 写自己的库
  • Feign / HTTP / MQ / RPC 参与业务链路
  • 一个业务动作要跨多个数据库完成

就不能靠一个 @Transactional 注解解决了。一般会从"强一致事务"转向"最终一致性 + 幂等 + 补偿 + 对账"。


1. 单体项目里事务怎么保证?

单体项目大多数情况确实可以靠@Transactional:

scss 复制代码
@Transactional
public void createOrder(CreateOrderRequest request) {
    orderMapper.insert(order);
    stockMapper.deductStock(request.getSkuId(), request.getCount());
    accountMapper.freezeAmount(request.getUserId(), request.getAmount());
}

因为这些操作通常满足几个条件:

条件 说明
一个应用 都在同一个 Spring Boot 进程里
一个数据库或同一个事务管理器 Spring 能拿到同一个数据库连接
一个方法边界 方法成功提交,异常回滚
本地 ACID 由数据库保证原子性、一致性、隔离性、持久性

流程大概是:

xml 复制代码
```mermaid
flowchart TD
    A["Controller 请求"] --> B["Service 方法"]
    B --> C["@Transactional 开启事务"]
    C --> D["insert 订单"]
    D --> E["扣库存"]
    E --> F["冻结金额"]
    F --> G{"是否异常?"}
    G -- "否" --> H["commit 提交"]
    G -- "是" --> I["rollback 回滚"]
```

在单体里,只要你注意几个坑,@Transactional 基本能覆盖大部分场景:

  • 方法必须是 public
  • 不能同类方法内部调用导致代理失效
  • 默认只回滚 RuntimeException
  • 异常不能被吞掉
  • 数据库表要支持事务,比如 MySQL 用 InnoDB
  • 多数据源时要确认用的是正确的事务管理器

2. 微服务里为什么不能简单靠 @Transactional

假设现在有三个服务:

css 复制代码
订单服务 order-service
库存服务 stock-service
账户服务 account-service

订单服务里这样写:

scss 复制代码
@Transactional
public void createOrder(CreateOrderRequest request) {
    orderMapper.insert(order);

    stockFeign.deductStock(request.getSkuId(), request.getCount());

    accountFeign.freezeAmount(request.getUserId(), request.getAmount());
}

很多人一开始会误以为:

这个方法加了 @Transactional,只要后面出错,所有东西都会回滚。

这是错的。

@Transactional 只能回滚订单服务自己的数据库:

css 复制代码
order-service 的数据库:可以回滚
stock-service 的数据库:回滚不了
account-service 的数据库:回滚不了

因为 Feign 调用的是远程 HTTP/RPC 请求,远程服务已经自己提交了自己的数据库事务。

举个问题场景:

markdown 复制代码
1. 订单服务插入订单成功
2. 库存服务扣库存成功
3. 账户服务冻结金额失败
4. 订单服务本地事务回滚

最后结果可能是:

复制代码
订单没有了
库存却被扣了
金额没有冻结

这就是典型的跨服务一致性问题。


3. 微服务事务的本质问题是什么?

本质问题是:

一个业务动作被拆到了多个服务、多个数据库、多个事务里。

单体里是:

复制代码
一个业务动作 = 一个本地事务

微服务里变成:

复制代码
一个业务动作 = 多个本地事务 + 网络调用 + 失败重试 + 超时 + 消息延迟

网络调用会带来很多不确定性:

问题 例子
超时 服务 B 实际成功了,但服务 A 没收到响应
重试 服务 A 重试调用,服务 B 可能重复扣库存
部分成功 订单成功,库存成功,支付失败
消息延迟 MQ 消息晚几秒才被消费
服务宕机 执行到一半服务挂了
回滚困难 远程服务已经提交,没法被本地事务直接回滚

所以微服务事务一般不是简单追求"全部一起提交、全部一起回滚",而是设计成:

复制代码
每个服务保证自己的本地事务
服务之间通过消息、状态机、补偿、幂等来保证最终一致

4. 真实项目里常用的解决方案

常见方案主要有这几类:

方案 一致性 常用程度 适合场景
本地事务 + MQ 最终一致性 最终一致 很常用 订单、通知、审批、异步处理
事务消息 最终一致 很常用 下单后发消息、支付后改状态
Outbox Pattern 本地消息表 最终一致 很常用 对可靠性要求高的业务事件
Saga 最终一致 常用 跨多个服务的长流程
TCC 较强一致 有一定使用 资金、库存冻结、资源预留
Seata AT 近似分布式事务 国内项目常见 改造成本较低的 Java 微服务
Seata TCC / Saga / XA 取决于模式 特定场景 强一致或流程型事务
XA / 2PC 强一致 不太常用 银行、金融核心、低并发强一致
人工对账 + 补偿任务 最终一致 非常常见 支付、结算、财务、三方系统

一、本地事务 + MQ 最终一致性

这是企业项目里最常见、最实用的方案之一。

比如用户提交报销单:

markdown 复制代码
报销服务:
1. 保存报销单
2. 提交审批
3. 发送"报销单已提交"消息

审批服务:
1. 消费消息
2. 创建审批流程

不要让报销服务同步强依赖审批服务一定成功,而是通过 MQ 解耦。

流程:

rust 复制代码
```mermaid
sequenceDiagram
    participant A as 报销服务
    participant DB1 as 报销库
    participant MQ as 消息队列
    participant B as 审批服务
    participant DB2 as 审批库

    A->>DB1: 保存报销单
    A->>DB1: 本地事务提交
    A->>MQ: 发送报销单已提交消息
    MQ->>B: 推送消息
    B->>DB2: 创建审批流程
```

但是这里有一个关键问题:

复制代码
如果数据库提交成功了,但是发送 MQ 失败怎么办?

所以直接这样写是不够严谨的:

typescript 复制代码
@Transactional
public void submitExpense(Long expenseId) {
    expenseMapper.updateStatus(expenseId, "SUBMITTED");

    mqProducer.send("expense.submitted", expenseId);
}

这段代码的问题是:

复制代码
DB 提交成功,MQ 发送失败,审批服务永远不知道这个单据已提交。

所以更可靠的做法是:本地消息表,也叫 Outbox Pattern。


二、Outbox Pattern:本地消息表

核心思想:

业务数据和待发送消息放在同一个本地事务里提交。

比如报销服务提交单据时,不是直接发 MQ,而是先写业务表和消息表:

python 复制代码
@Transactional
public void submitExpense(Long expenseId) {
    expenseMapper.updateStatus(expenseId, "SUBMITTED");

    OutboxMessage message = new OutboxMessage();
    message.setBizType("EXPENSE_SUBMITTED");
    message.setBizId(expenseId.toString());
    message.setPayload("""
        {
          "expenseId": %d
        }
        """.formatted(expenseId));
    message.setStatus("PENDING");

    outboxMessageMapper.insert(message);
}

这两个操作在一个本地事务里:

复制代码
更新报销单状态
插入待发送消息

要么都成功,要么都失败。

然后有一个后台任务专门扫描消息表:

scss 复制代码
@Scheduled(fixedDelay = 3000)
public void publishPendingMessages() {
    List<OutboxMessage> messages = outboxMessageMapper.findPendingMessages();

    for (OutboxMessage message : messages) {
        try {
            mqProducer.send(message.getTopic(), message.getPayload());
            outboxMessageMapper.markSent(message.getId());
        } catch (Exception e) {
            outboxMessageMapper.increaseRetryCount(message.getId());
        }
    }
}

流程:

xml 复制代码
```mermaid
flowchart TD
    A[&#34;提交报销单&#34;] --> B[&#34;@Transactional 本地事务&#34;]
    B --> C[&#34;更新报销单状态&#34;]
    B --> D[&#34;插入 outbox_message&#34;]
    C --> E[&#34;事务提交&#34;]
    D --> E
    E --> F[&#34;定时任务扫描待发送消息&#34;]
    F --> G[&#34;发送 MQ&#34;]
    G --> H[&#34;发送成功,标记 SENT&#34;]
    G --> I[&#34;发送失败,等待重试&#34;]
```

这个方案的好处:

优点 说明
可靠 业务数据和消息数据在一个事务里
解耦 下游服务挂了也不影响当前服务提交
可重试 消息发送失败可以继续扫表重试
可追踪 消息表里能看到发送状态
适合企业项目 订单、审批、报销、通知、AI任务都能用

这个方案的代价:

代价 说明
最终一致 不是马上完成所有动作
需要幂等 下游可能重复收到消息
需要补偿 下游处理失败要能重试或人工处理
需要清理 历史消息表要归档

三、事务消息

有些 MQ 自身支持事务消息,比如 RocketMQ 的事务消息。

流程大概是:

markdown 复制代码
1. 先发送半消息
2. 执行业务本地事务
3. 本地事务成功,提交消息
4. 本地事务失败,回滚消息
5. 如果状态不确定,MQ 反查业务服务

流程:

rust 复制代码
```mermaid
sequenceDiagram
    participant A as 订单服务
    participant MQ as RocketMQ
    participant DB as 订单库
    participant B as 库存服务

    A->>MQ: 发送半消息
    A->>DB: 创建订单
    DB-->>A: 本地事务成功
    A->>MQ: 提交消息
    MQ->>B: 投递消息
    B->>B: 扣库存
```

适合场景:

复制代码
本地事务成功后,需要可靠通知其他服务。

比如:

  • 支付成功后通知订单服务改状态
  • 订单创建后通知库存服务预占库存
  • 报销单提交后通知审批服务创建流程
  • 文件上传成功后通知 Python AI 服务做 RAG 入库

优点:

优点 说明
MQ 原生支持 不用自己维护部分消息表逻辑
可靠性高 有事务回查机制
适合异步业务 订单、支付、审批、AI入库

缺点:

缺点 说明
依赖 MQ 能力 不是所有 MQ 都支持事务消息
业务仍需幂等 消息可能重复投递
仍是最终一致 不是全局强一致事务

四、Saga 模式

Saga 是微服务里很经典的分布式事务方案。

它的思想是:

把一个大事务拆成多个本地事务,每一步都有对应的补偿动作。

例如下单流程:

markdown 复制代码
正向流程:
1. 创建订单
2. 扣减库存
3. 冻结余额
4. 创建物流单

补偿流程:
1. 取消物流单
2. 解冻余额
3. 释放库存
4. 取消订单

流程:

xml 复制代码
```mermaid
flowchart TD
    A[&#34;创建订单&#34;] --> B[&#34;扣减库存&#34;]
    B --> C[&#34;冻结余额&#34;]
    C --> D[&#34;创建物流单&#34;]
    D --> E[&#34;下单成功&#34;]

    B -->|失败| B1[&#34;取消订单&#34;]
    C -->|失败| C1[&#34;释放库存&#34;]
    C1 --> C2[&#34;取消订单&#34;]
    D -->|失败| D1[&#34;解冻余额&#34;]
    D1 --> D2[&#34;释放库存&#34;]
    D2 --> D3[&#34;取消订单&#34;]
```

Saga 有两种常见方式。

1. 编排式 Saga

有一个"流程编排服务"控制每一步。

markdown 复制代码
订单编排器:
1. 调订单服务创建订单
2. 调库存服务扣库存
3. 调账户服务冻结金额
4. 如果中途失败,按反方向调用补偿接口

代码形态可能是:

scss 复制代码
public void createOrderSaga(CreateOrderCommand command) {
    Long orderId = orderClient.createOrder(command);

    try {
        stockClient.deduct(command.getSkuId(), command.getCount());
        accountClient.freeze(command.getUserId(), command.getAmount());
        orderClient.markSuccess(orderId);
    } catch (Exception e) {
        accountClient.unfreeze(command.getUserId(), command.getAmount());
        stockClient.release(command.getSkuId(), command.getCount());
        orderClient.cancel(orderId);
        throw e;
    }
}

真实项目里不会这么裸写,通常会配合:

  • 状态机
  • MQ
  • 工作流引擎
  • 任务表
  • 重试机制
  • 幂等控制
  • 补偿记录

2. 事件驱动 Saga

没有中心编排器,每个服务监听事件后推进下一步。

复制代码
订单服务发布 OrderCreated
库存服务消费后扣库存,发布 StockDeducted
账户服务消费后冻结金额,发布 AccountFrozen
订单服务最终标记成功

流程:

rust 复制代码
```mermaid
sequenceDiagram
    participant O as 订单服务
    participant MQ as MQ
    participant S as 库存服务
    participant A as 账户服务

    O->>O: 创建订单
    O->>MQ: 发布 OrderCreated
    MQ->>S: 库存服务消费
    S->>S: 扣库存
    S->>MQ: 发布 StockDeducted
    MQ->>A: 账户服务消费
    A->>A: 冻结余额
    A->>MQ: 发布 AccountFrozen
    MQ->>O: 订单服务消费
    O->>O: 标记订单成功
```

Saga 适合:

  • 订单流程
  • 审批流程
  • 报销流程
  • 开票流程
  • 出库入库流程
  • AI 长任务处理流程

Saga 不适合:

  • 必须瞬间强一致的资金核心扣款
  • 每一步都无法补偿的操作
  • 业务没有明确状态机的场景

五、TCC 模式

TCC 是:

vbnet 复制代码
Try
Confirm
Cancel

它比 Saga 更强一些,适合"资源预留"类业务。

以库存为例。

Try:尝试阶段

先冻结库存,不是真的扣掉。

typescript 复制代码
商品库存 100
冻结库存 10
可用库存 90
public void tryDeductStock(Long skuId, Integer count) {
    stockMapper.freezeStock(skuId, count);
}

Confirm:确认阶段

业务整体成功后,把冻结库存转成真正扣减。

typescript 复制代码
public void confirmDeductStock(Long skuId, Integer count) {
    stockMapper.confirmDeduct(skuId, count);
}

Cancel:取消阶段

业务失败后,释放冻结库存。

typescript 复制代码
public void cancelDeductStock(Long skuId, Integer count) {
    stockMapper.releaseFrozenStock(skuId, count);
}

流程:

xml 复制代码
```mermaid
flowchart TD
    A[&#34;订单服务发起交易&#34;] --> B[&#34;库存 Try: 冻结库存&#34;]
    B --> C[&#34;账户 Try: 冻结余额&#34;]
    C --> D{&#34;所有 Try 是否成功?&#34;}
    D -- &#34;是&#34; --> E[&#34;库存 Confirm: 确认扣减&#34;]
    E --> F[&#34;账户 Confirm: 确认扣款&#34;]
    F --> G[&#34;交易成功&#34;]
    D -- &#34;否&#34; --> H[&#34;库存 Cancel: 释放库存&#34;]
    H --> I[&#34;账户 Cancel: 解冻余额&#34;]
    I --> J[&#34;交易失败&#34;]
```

TCC 的优点:

优点 说明
一致性更强 先预留资源,再统一确认
适合核心资源 库存、余额、额度、券码
可控 每个服务明确 Try/Confirm/Cancel

TCC 的缺点:

缺点 说明
开发成本高 每个接口要写三套逻辑
业务侵入强 表结构和接口都要支持冻结/确认/取消
幂等要求高 Confirm 和 Cancel 可能被重复调用
悬挂问题 Cancel 先到,Try 后到,要处理
空回滚问题 Try 没成功,Cancel 也被调用,要能正常返回

TCC 常见表字段设计:

diff 复制代码
account_balance
- user_id
- total_amount
- available_amount
- frozen_amount

stock
- sku_id
- total_stock
- available_stock
- frozen_stock

真实项目里,TCC 一定要考虑三个问题:

问题 含义
幂等 Try/Confirm/Cancel 重复执行,结果不能错
空回滚 Try 没执行成功,Cancel 调过来也不能报错
防悬挂 Cancel 已执行,后来的 Try 不能再成功

六、Seata 分布式事务

Seata 支持几种模式:

模式 说明 适合场景
AT 模式 自动代理数据库操作,生成 undo_log 普通数据库 CRUD
TCC 模式 业务自己实现 Try/Confirm/Cancel 核心资源控制
Saga 模式 状态机 + 补偿事务 长流程
XA 模式 基于数据库 XA 协议 强一致但性能较低

最常见的是 AT 模式。

代码大概是这样:

scss 复制代码
@GlobalTransactional
public void createOrder(CreateOrderRequest request) {
    orderMapper.insert(order);

    stockFeign.deductStock(request.getSkuId(), request.getCount());

    accountFeign.freezeAmount(request.getUserId(), request.getAmount());
}

@Transactional 不同,@GlobalTransactional 是 Seata 提供的全局事务注解。

大致流程:

xml 复制代码
```mermaid
flowchart TD
    A[&#34;订单服务 @GlobalTransactional&#34;] --> B[&#34;开启全局事务 XID&#34;]
    B --> C[&#34;订单服务写订单库&#34;]
    C --> D[&#34;库存服务扣库存&#34;]
    D --> E[&#34;账户服务冻结金额&#34;]
    E --> F{&#34;全局是否成功?&#34;}
    F -- &#34;是&#34; --> G[&#34;全局提交&#34;]
    F -- &#34;否&#34; --> H[&#34;根据 undo_log 回滚各服务本地变更&#34;]
```

Seata AT 模式优点:

优点 说明
接入相对简单 业务代码改动比 TCC 少
对 CRUD 友好 普通 insert/update/delete 比较适合
Java 生态常见 Spring Cloud 项目里资料多

Seata AT 模式缺点:

缺点 说明
有性能损耗 要记录 undo_log、加全局锁
有数据库侵入 每个库通常需要 undo_log 表
不适合长事务 全局事务时间长会影响并发
不适合所有 SQL 复杂 SQL、批量更新、存储过程要谨慎
运维成本 需要 Seata Server,高可用部署

什么时候可以考虑 Seata AT?

markdown 复制代码
1. Java 微服务体系
2. 多服务、多库,但是业务希望接近本地事务体验
3. 并发量不是特别夸张
4. SQL 类型比较常规
5. 团队愿意接受 Seata 的部署和治理成本

什么时候不建议优先上 Seata AT?

markdown 复制代码
1. 高并发核心链路
2. 长流程业务,比如审批、报销、AI任务处理
3. 跨外部系统,比如三方支付、短信、OSS、Python AI服务
4. 业务天然可以异步最终一致
5. 团队还没做好幂等、重试、补偿、对账

七、XA / 2PC 强一致事务

XA / 2PC 是比较传统的分布式事务方案。

它分两阶段:

sql 复制代码
第一阶段:prepare,问所有参与者能不能提交
第二阶段:commit / rollback,统一提交或回滚

流程:

rust 复制代码
```mermaid
sequenceDiagram
    participant TC as 事务协调器
    participant A as 订单库
    participant B as 库存库
    participant C as 账户库

    TC->>A: prepare
    TC->>B: prepare
    TC->>C: prepare
    A-->>TC: OK
    B-->>TC: OK
    C-->>TC: OK
    TC->>A: commit
    TC->>B: commit
    TC->>C: commit
```

优点:

复制代码
强一致。

缺点也很明显:

复制代码
性能差
锁持有时间长
协调器复杂
参与方阻塞
对数据库和中间件要求高

所以普通互联网业务、普通企业管理系统里,不是首选

更常见的选择是:

复制代码
本地事务 + MQ + 幂等 + 补偿 + 对账

八、最大量使用的通用方案:本地事务 + 状态机 + MQ + 幂等

项目里,我更建议你建立这个默认思路:

复制代码
不要一上来就想全局事务。
先看能不能拆成状态流转。
每个服务只保证自己的本地事务。
跨服务通过事件推动。
失败靠重试、补偿、对账解决。

以报销审批为例。

报销服务

diff 复制代码
expense_bill
- id
- user_id
- amount
- status

状态:

复制代码
DRAFT 草稿
SUBMITTED 已提交
APPROVING 审批中
APPROVED 审批通过
REJECTED 审批拒绝
PAYING 打款中
PAID 已打款
FAILED 失败

提交报销单时:

java 复制代码
@Transactional
public void submit(Long billId) {
    ExpenseBill bill = expenseBillMapper.selectById(billId);

    if (!"DRAFT".equals(bill.getStatus())) {
        throw new BusinessException("当前状态不允许提交");
    }

    expenseBillMapper.updateStatus(billId, "SUBMITTED");

    outboxMessageMapper.insert(
        OutboxMessage.of("EXPENSE_SUBMITTED", billId.toString())
    );
}

审批服务消费消息:

csharp 复制代码
@Transactional
public void handleExpenseSubmitted(ExpenseSubmittedEvent event) {
    if (approvalFlowMapper.existsByBizId(event.getBillId())) {
        return;
    }

    approvalFlowMapper.createFlow(event.getBillId(), "EXPENSE");
}

这里有几个关键点:

作用
状态机 控制业务不能乱跳
本地事务 保证当前服务自己的数据正确
Outbox 保证消息一定能发出去
MQ 重试 下游失败可以自动重试
幂等 消息重复消费也不会重复创建审批
补偿任务 长时间异常的数据可以修复
对账任务 定期发现不一致数据

九、幂等是微服务事务的底座

微服务里你必须假设:

复制代码
同一个请求可能来多次。
同一条 MQ 消息可能消费多次。
同一个 Feign 调用可能因为超时被重试。

所以每个关键接口都要支持幂等。

常见幂等方案:

方案 例子
唯一业务单号 request_noorder_nobiz_id
唯一索引 (biz_type, biz_id)
状态判断 只有 DRAFT 才能提交
幂等表 记录每次请求是否处理过
去重表 MQ 消费时记录 message_id
Redis 锁 防止短时间重复提交
乐观锁 version 字段控制并发更新
数据库条件更新 where status = 'DRAFT'

例如审批服务消费消息:

arduino 复制代码
@Transactional
public void createApprovalFlow(ExpenseSubmittedEvent event) {
    boolean exists = approvalFlowMapper.existsByBizId(event.getBillId());

    if (exists) {
        return;
    }

    approvalFlowMapper.insert(event.getBillId(), "EXPENSE");
}

更稳的做法是加唯一索引:

sql 复制代码
alter table approval_flow
add unique key uk_biz_type_biz_id (biz_type, biz_id);

然后代码里即使并发消费两次,也不会创建两条审批流。


十、补偿机制是什么?

补偿不是简单的"回滚数据库"。

补偿是:

当一个流程后续失败时,通过反向业务动作把系统恢复到合理状态。

例如:

正向动作 补偿动作
创建订单 取消订单
冻结余额 解冻余额
预占库存 释放库存
创建审批流 关闭审批流
创建 RAG 入库任务 标记任务失败或重新入库
调用三方支付 发起退款

比如 AI 文件入库:

复制代码
Java 服务上传文件成功
Python 服务开始切片、向量化、入库
如果 Python 入库失败
Java 文件不能回滚删除
正确做法是标记知识库文档状态为 INDEX_FAILED
用户可以重试入库
后台任务可以自动补偿

这就是典型的最终一致性。

状态可能是:

复制代码
UPLOADED 已上传
INDEXING 入库中
INDEXED 已入库
INDEX_FAILED 入库失败
DELETED 已删除

流程:

xml 复制代码
```mermaid
flowchart TD
    A[&#34;Java 上传文件&#34;] --> B[&#34;保存文件元数据&#34;]
    B --> C[&#34;写入 outbox 消息&#34;]
    C --> D[&#34;Python 消费文件入库事件&#34;]
    D --> E[&#34;下载 OSS 文件&#34;]
    E --> F[&#34;切片&#34;]
    F --> G[&#34;向量化&#34;]
    G --> H{&#34;入库成功?&#34;}
    H -- &#34;是&#34; --> I[&#34;回调 Java 标记 INDEXED&#34;]
    H -- &#34;否&#34; --> J[&#34;回调 Java 标记 INDEX_FAILED&#34;]
    J --> K[&#34;等待重试或人工处理&#34;]
```

这个场景就不适合强分布式事务。

因为你无法把这些动作放进一个数据库事务里:

复制代码
OSS 上传文件
Python 切片
调用 embedding 模型
写向量库
回调 Java

正确方案就是:

复制代码
状态机 + MQ + 重试 + 幂等 + 失败可见 + 补偿任务

十一、Feign 调用能不能放在事务里?

可以写,但要非常谨慎。

比如:

scss 复制代码
@Transactional
public void createOrder(CreateOrderRequest request) {
    orderMapper.insert(order);
    stockFeign.deductStock(request.getSkuId(), request.getCount());
}

这段代码有几个风险:

1. 本地事务时间变长

数据库事务打开后,又去等远程服务响应。

如果远程服务慢,本地数据库锁就会持有更久。

复制代码
开启事务
插入订单,占用数据库连接和锁
等待库存服务 3 秒
事务才提交

这会影响并发。

2. 远程调用不能跟着本地事务回滚

库存服务扣减成功后,订单服务后面异常,订单服务可以回滚,但是库存服务不会自动回滚。

3. 超时结果不确定

订单服务调用库存服务超时:

复制代码
订单服务看到:失败
库存服务实际:可能成功了

如果订单服务重试,可能导致重复扣库存。

所以项目一般不建议在本地事务里随便放远程调用。

更推荐:

复制代码
本地事务里只做本服务数据库操作
事务提交后再发消息或触发下游流程

如果必须同步调用远程服务,要注意:

要求 说明
远程接口必须幂等 防止重复调用
超时时间要短 防止本地事务持锁过久
有明确状态 不能只靠异常判断
有补偿接口 失败后能撤销
有对账任务 修复不一致状态
不要长事务 尽量缩小事务范围

十二、怎么选方案?

可以按这个顺序判断。

1. 能不能变成本地事务?

这是第一优先级。

如果两个数据强相关,经常要一起提交、一起回滚,说明它们可能不该拆到两个服务里。

例如:

复制代码
订单主表
订单明细表
订单扩展表

这些一般应该属于订单服务自己的数据库,本地事务解决即可。

不要为了"微服务"把强一致数据硬拆开。


2. 能不能接受最终一致?

大多数业务其实可以接受几秒内一致。

比如:

复制代码
提交报销单后,审批流晚 1 秒创建
文件上传后,知识库晚 10 秒可检索
订单创建后,库存稍后预占
支付成功后,订单状态稍后更新

这种场景优先用:

复制代码
本地事务 + Outbox + MQ + 幂等消费

3. 是否需要资源预留?

如果是库存、余额、额度这类资源,不能简单异步处理,因为可能超卖或超扣。

这时考虑:

复制代码
TCC
冻结库存 / 冻结余额
Confirm / Cancel

4. 是否是长流程?

如果流程比较长,比如:

复制代码
申请
审批
复核
打款
归档
通知

这类流程不适合数据库长事务。

应该用:

复制代码
Saga
状态机
工作流引擎
事件驱动

5. 是否真的需要强一致?

如果是金融核心记账、资金清结算、账户余额核心链路,可能需要更强一致。

可以考虑:

复制代码
TCC
Seata TCC
XA
数据库层强约束
账务流水 + 对账

但即使是金融系统,也不是所有地方都用强分布式事务。很多时候会用:

复制代码
流水不可变
状态流转
冲正
对账
补偿

十三、常见业务场景推荐

场景 推荐方案
单体项目内多个表一起更新 @Transactional
一个服务内一个库 @Transactional
一个服务内多数据源 尽量避免;必要时 JTA/XA 或拆业务
Java 上传文件后通知 Python RAG 入库 Outbox + MQ + 状态机
报销单提交后创建审批流程 Outbox + MQ,或同步调用 + 补偿
支付成功后改订单状态 事务消息 / MQ + 幂等
下单扣库存 TCC / 预占库存 / Saga
发短信、发通知 MQ,不能放核心事务里强依赖
调三方支付 本地流水 + 状态机 + 回调 + 对账
AI 长任务编排 任务表 + MQ + 状态机 + 重试
强资金一致性 TCC / XA / 账务流水 / 对账
普通后台管理系统跨服务修改 优先最终一致,不要滥用分布式事务
老系统想快速接分布式事务 可评估 Seata AT,但要压测和验证 SQL 支持

十四、举例说明 Java + Python AI 项目

架构是:

复制代码
Java 微服务:业务系统、权限、文件元数据、单据模板、审批、用户、租户
Python AI 服务:智能体编排、RAG 入库、RAG 检索、LLM 调用

这里不应该追求 Java 和 Python 之间的强分布式事务。

比如上传知识库文件:

markdown 复制代码
Java:
1. 接收文件上传
2. 上传到 OSS
3. 保存文件元数据
4. 写入 outbox 消息:DOCUMENT_UPLOADED
5. 返回前端:上传成功,入库中

Python:
1. 消费 DOCUMENT_UPLOADED
2. 调 Java 接口获取文件元数据和权限信息
3. 下载 OSS 文件
4. 切片
5. embedding
6. 写入向量库
7. 回调 Java 更新状态:INDEXED / INDEX_FAILED

这就是标准的最终一致方案。

状态设计:

复制代码
UPLOADED
INDEX_PENDING
INDEXING
INDEXED
INDEX_FAILED
DELETE_PENDING
DELETED

Java 文档表可以这样设计:

sql 复制代码
create table knowledge_document (
    id bigint primary key,
    tenant_id bigint not null,
    file_name varchar(255) not null,
    file_url varchar(1000) not null,
    category varchar(100) not null,
    index_status varchar(50) not null,
    version int not null default 0,
    created_at datetime not null,
    updated_at datetime not null
);

Outbox 消息表:

sql 复制代码
create table outbox_message (
    id bigint primary key,
    topic varchar(100) not null,
    biz_type varchar(100) not null,
    biz_id varchar(100) not null,
    payload text not null,
    status varchar(50) not null,
    retry_count int not null default 0,
    next_retry_time datetime null,
    created_at datetime not null,
    updated_at datetime not null,
    unique key uk_biz_type_biz_id (biz_type, biz_id)
);

Python 入库服务要保证幂等:

复制代码
同一个 document_id + version 重复入库,不应该产生重复向量数据。

向量库里可以存:

复制代码
chunk_id
document_id
document_version
tenant_id
permission_scope
chunk_index
chunk_text
embedding
metadata

如果重新入库:

复制代码
先按 document_id + document_version 删除旧 chunk
再写入新的 chunk
或者新版本写入成功后切换 active_version

这里最重要的不是"分布式事务",而是:

复制代码
状态清楚
消息可靠
消费幂等
失败可重试
数据可对账

十五、一个项目里比较稳的标准模板

你可以把跨服务事务都拆成这个模板思考:

markdown 复制代码
1. 当前服务先完成自己的本地事务
2. 本地事务里写业务表 + 消息表
3. 后台任务或事务消息把事件发出去
4. 下游服务消费事件
5. 下游服务本地事务处理自己的数据
6. 下游服务保证幂等
7. 失败自动重试
8. 多次失败进入失败表或死信队列
9. 定时任务对账
10. 必要时人工补偿

流程图:

xml 复制代码
```mermaid
flowchart TD
    A[&#34;业务请求&#34;] --> B[&#34;服务 A 本地事务&#34;]
    B --> C[&#34;写业务数据&#34;]
    B --> D[&#34;写 outbox 消息&#34;]
    C --> E[&#34;提交事务&#34;]
    D --> E
    E --> F[&#34;消息发布器发送 MQ&#34;]
    F --> G[&#34;服务 B 消费消息&#34;]
    G --> H{&#34;是否已处理?&#34;}
    H -- &#34;是&#34; --> I[&#34;直接返回成功&#34;]
    H -- &#34;否&#34; --> J[&#34;服务 B 本地事务&#34;]
    J --> K[&#34;写服务 B 业务数据&#34;]
    K --> L[&#34;记录消费成功&#34;]
    L --> M[&#34;流程结束&#34;]
    G --> N[&#34;消费失败&#34;]
    N --> O[&#34;自动重试&#34;]
    O --> P{&#34;超过重试次数?&#34;}
    P -- &#34;否&#34; --> G
    P -- &#34;是&#34; --> Q[&#34;进入死信队列或失败表&#34;]
    Q --> R[&#34;补偿任务或人工处理&#34;]
```

相关推荐
IT界的老黄牛1 小时前
限流命中后该怎么办:直接丢、阻塞等待、延迟重投三种姿势的取舍
java·rocketmq·redisson·令牌桶·削峰·分布式限流
犀利豆2 小时前
Claude code tools 研究系列(二)EnterPlanMode
人工智能·后端
用户69371750013842 小时前
AI时代,程序员该往哪走?
前端·后端
niaiheni2 小时前
红队实战:记一次Spring Cloud Gateway SpEL表达式注入到哥斯拉内存马(CVE-2022-22947)
web安全·spring cloud·网络安全
daad7772 小时前
记录matlab状态机demo
java·网络·matlab
Revolution612 小时前
第一次运行 Node.js:终端里的 JavaScript 怎样执行
后端·面试·node.js
牡丹雅忻13 小时前
AES 加密模式演进:从 ECB、CBC 到 GCM 的 C# 深度实践
java·开发语言·c#
学者猫头鹰3 小时前
Java 8 核心新特性
java
Python私教3 小时前
Django 6.1 RC1 实测:FETCH_PEERS 两条 SQL 解决 N+1,select_related 还需要吗?
后端·python·django