2026 年,微服务架构已经成为中大型系统的标配,但分布式事务问题依然困扰着 73% 的后端团队(据 CNCF 2025 微服务调查报告)。当一个业务操作需要跨多个服务修改数据时,如何保证数据一致性?两阶段提交(2PC)性能差、锁竞争严重;TCC 模式侵入性太强、开发成本高——而 Saga 模式与 Outbox 模式的组合,正在成为业界公认的「最优解」。
本文将从真实生产经验出发,带你走通 Saga 与 Outbox 的核心原理、两种 Saga 实现方式的对比、Spring Boot 完整代码实现,以及在高并发场景下的踩坑与避坑指南。
🏗️ 一、分布式事务的核心挑战与模式选型
为什么传统的 2PC 在微服务中行不通?
两阶段提交(Two-Phase Commit)在单体应用中尚可使用,但在微服务架构下存在致命缺陷:
- ❌ 同步阻塞:所有参与者在 Prepare 阶段必须持有锁,直到 Commit 或 Rollback,高并发下吞吐量断崖式下降
- ❌ 单点故障:协调者(Coordinator)宕机后,所有参与者陷入阻塞状态
- ❌ 跨服务事务管理器:2PC 需要一个全局事务管理器,但在微服务中每个服务有独立的数据库,无法使用本地事务管理器
// ❌ 错误写法:在微服务中使用分布式事务注解(Spring @Transactional 跨服务无效)
@Transactional // 这个注解只对本地数据库有效!
public void createOrder(OrderDTO orderDTO) {
orderService.create(orderDTO); // 服务 A 的数据库
inventoryService.deduct(orderDTO); // 服务 B 的数据库 — 不在同一事务中!
paymentService.charge(orderDTO); // 服务 C 的数据库 — 不在同一事务中!
// 如果 paymentService 失败,inventoryService 的扣减不会回滚!
}
⚠️ **警告:**永远不要用
@Transactional注解试图管理跨微服务的事务。Spring 的事务管理器只能控制本地数据库连接,跨服务调用已经是在不同的进程和数据库中了。
主流分布式事务模式对比
| 模式 | 一致性 | 性能 | 侵入性 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|
| 2PC/XA | 强一致 | ❌ 低 | 低 | 中 | 传统企业应用、低并发 |
| TCC | 强一致 | ✅ 高 | ❌ 高 | 高 | 金融转账、强一致要求 |
| Saga | 最终一致 | ✅ 高 | 低 | 中 | 电商订单、大多数业务场景 |
| Outbox | 最终一致 | ✅ 高 | 低 | 中 | 事件发布、跨服务通知 |
| 本地消息表 | 最终一致 | ✅ 高 | 中 | 低 | 简单场景、小团队 |
⚡ **关键结论:**Saga + Outbox 组合是 2026 年微服务分布式事务的最佳实践。Saga 负责「跨服务的业务流程编排」,Outbox 负责「保证事件可靠发布」,两者互补,覆盖了 90% 以上的业务场景。
🔄 二、Saga 模式实战:编排式 vs 协调式
Saga 的核心思想:用补偿代替回滚
Saga 的本质是将一个长事务拆分成一系列本地事务,每个本地事务都有对应的补偿操作。如果某一步失败,就逆序执行前面所有步骤的补偿操作。
正常流程:T1 → T2 → T3 → T4(成功)
失败回滚:T1 → T2 → T3 → T4(失败)→ C3 → C2 → C1
💡 **提示:**补偿操作 ≠ 回滚。回滚是「恢复到原来的状态」,补偿是「执行一个语义上相反的操作」。例如,扣减库存的补偿是「增加库存」,而不是数据库 rollback。
两种 Saga 实现方式
编排式(Orchestration):由一个中央协调器(Orchestrator)负责指挥整个流程,告诉每个参与者「下一步该做什么」。
协调式(Choreography):没有中央协调器,每个服务监听事件并决定自己的行为,像一场「编舞」。
| 维度 | 编排式(Orchestration) | 协调式(Choreography) |
|---|---|---|
| 控制流 | 集中,易于理解 | 分散,难以追踪 |
| 耦合度 | 协调器依赖所有参与者 | 服务间通过事件解耦 |
| 调试难度 | ✅ 低(集中日志) | ❌ 高(分布式追踪) |
| 适用步骤 | 步骤多(>3)、流程复杂 | 步骤少(2-3)、流程简单 |
| 单点风险 | 协调器是单点 | 无单点 |
📌 **记住:**步骤超过 3 步的业务流程,强烈推荐编排式 Saga。协调式在 2-3 步时简单优雅,但步骤一多就会变成「分布式意大利面条」。
编排式 Saga 完整实现(Spring Boot)
下面是一个电商「创建订单」的编排式 Saga 实现,涵盖订单创建 → 库存扣减 → 支付处理三个步骤:
// Saga 协调器:负责编排整个订单创建流程
@Component
public class CreateOrderSagaOrchestrator {
private final OrderServiceClient orderService;
private final InventoryServiceClient inventoryService;
private final PaymentServiceClient paymentService;
private final SagaStateRepository stateRepository;
public CreateOrderSagaOrchestrator(
OrderServiceClient orderService,
InventoryServiceClient inventoryService,
PaymentServiceClient paymentService,
SagaStateRepository stateRepository) {
this.orderService = orderService;
this.inventoryService = inventoryService;
this.paymentService = paymentService;
this.stateRepository = stateRepository;
}
@Transactional
public SagaResult execute(CreateOrderCommand command) {
// 创建 Saga 状态记录,用于故障恢复
SagaState state = SagaState.builder()
.sagaId(UUID.randomUUID().toString())
.type("CREATE_ORDER")
.status(SagaStatus.STARTED)
.payload(toJson(command))
.build();
stateRepository.save(state);
try {
// 步骤 1:创建订单(PENDING 状态)
OrderResult orderResult = orderService.createOrder(command);
state.setCurrentStep(1);
state.addCompletedStep("CREATE_ORDER", orderResult.getOrderId());
stateRepository.save(state);
// 步骤 2:扣减库存
InventoryResult invResult = inventoryService.deduct(
command.getItems());
state.setCurrentStep(2);
state.addCompletedStep("DEDUCT_INVENTORY", invResult);
stateRepository.save(state);
// 步骤 3:处理支付
PaymentResult payResult = paymentService.charge(
orderResult.getOrderId(), command.getPaymentInfo());
state.setCurrentStep(3);
state.addCompletedStep("PROCESS_PAYMENT", payResult);
state.setStatus(SagaStatus.COMPLETED);
stateRepository.save(state);
// 激活订单
orderService.activateOrder(orderResult.getOrderId());
return SagaResult.success(orderResult.getOrderId());
} catch (Exception e) {
// 补偿已完成的步骤(逆序)
compensate(state);
state.setStatus(SagaStatus.COMPENSATED);
stateRepository.save(state);
return SagaResult.failure(e.getMessage());
}
}
private void compensate(SagaState state) {
List<CompletedStep> steps = state.getCompletedSteps();
// 逆序补偿
for (int i = steps.size() - 1; i >= 0; i--) {
CompletedStep step = steps.get(i);
try {
switch (step.getName()) {
case "PROCESS_PAYMENT":
paymentService.refund(step.getResultId());
break;
case "DEDUCT_INVENTORY":
inventoryService.restore(step.getResultId());
break;
case "CREATE_ORDER":
orderService.cancelOrder(step.getResultId());
break;
}
} catch (Exception compensateEx) {
// 补偿失败需要记录并人工介入
log.error("Saga 补偿失败: step={}, sagaId={}",
step.getName(), state.getSagaId(), compensateEx);
state.setStatus(SagaStatus.COMPENSATION_FAILED);
// 发送告警,进入人工处理队列
alertService.sendCompensationAlert(state, step, compensateEx);
}
}
}
}
⚠️ **警告:**补偿操作本身也可能失败!生产环境中必须为补偿失败设计降级方案:重试队列 + 人工介入 + 告警通知。不要假设补偿一定会成功。
Saga 状态机设计
为了支持故障恢复(服务重启后能从断点继续),Saga 状态必须持久化:
-- Saga 状态表设计
CREATE TABLE saga_state (
saga_id VARCHAR(64) PRIMARY KEY,
saga_type VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL, -- STARTED, IN_PROGRESS, COMPLETED, COMPENSATED, COMPENSATION_FAILED
current_step INT NOT NULL DEFAULT 0,
payload JSON NOT NULL, -- 初始请求参数
completed_steps JSON NOT NULL DEFAULT '[]', -- 已完成步骤记录
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
version INT NOT NULL DEFAULT 0 -- 乐观锁,防止并发修改
);
-- 索引:用于定时任务扫描卡住的 Saga
CREATE INDEX idx_saga_status_created ON saga_state(status, created_at);
💡 **提示:**使用乐观锁(version 字段)防止并发执行同一个 Saga。分布式环境下多个实例可能同时尝试恢复同一个 Saga,乐观锁确保只有一个实例能成功。
📨 三、Outbox 模式:保证事件可靠发布的终极方案
双写问题:为什么直接发消息不靠谱?
在微服务中,一个操作完成后通常需要发布事件通知其他服务。最常见的「坑」就是双写问题(Dual Write Problem):
// ❌ 错误写法:先写数据库,再发消息(存在不一致风险)
@Transactional
public void createOrder(Order order) {
orderRepository.save(order); // 1. 写数据库成功
// 如果这里进程崩溃或 MQ 宕机...
rabbitTemplate.convertAndSend( // 2. 发消息失败!
"order-exchange", "order.created", order);
// 数据库有订单记录,但其他服务不知道订单已创建!
}
这个问题有三种失败场景:
- 数据库成功,消息失败 → 数据库有数据,下游服务不知道
- 数据库失败,消息不会发 → 这种情况没问题(可以不处理)
- 数据库成功,消息成功,但消息先到 → 下游服务查询订单时找不到
场景 1 和 3 都会导致数据不一致。
Outbox 模式的核心原理
Outbox 模式的思路非常巧妙:不直接发消息,而是将消息写入数据库的一张 outbox 表(与业务数据在同一个事务中),然后由一个独立的进程(CDC 或定时轮询)将 outbox 表中的消息发送到消息队列。
┌─────────────┐ ┌──────────────────────────────────┐
│ 应用服务 │ │ 数据库 │
│ │ │ ┌──────────┐ ┌──────────────┐ │
│ createOrder├────►│ │ orders │ │ outbox_events│ │
│ (一个事务) │ │ │ │ │ │ │
│ │ │ └──────────┘ └──────┬───────┘ │
└─────────────┘ └───────────────────────┼───────────┘
│
┌───────▼───────┐
│ Debezium CDC │
│ 或轮询进程 │
└───────┬───────┘
│
┌───────▼───────┐
│ Kafka / RabbitMQ │
└───────┬───────┘
│
┌───────▼───────┐
│ 下游服务 │
└───────────────┘
Outbox 表设计与实现
-- Outbox 事件表
CREATE TABLE outbox_events (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
aggregate_type VARCHAR(128) NOT NULL, -- 聚合根类型,如 "Order"
aggregate_id VARCHAR(64) NOT NULL, -- 聚合根 ID
event_type VARCHAR(128) NOT NULL, -- 事件类型,如 "OrderCreated"
payload JSON NOT NULL, -- 事件数据
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
published BOOLEAN NOT NULL DEFAULT FALSE,
published_at TIMESTAMP NULL,
retry_count INT NOT NULL DEFAULT 0,
INDEX idx_published_created (published, created_at)
);
// ✅ 正确写法:使用 Outbox 模式,业务数据和事件在同一个事务中
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final OutboxEventRepository outboxRepository;
@Transactional // 一个事务搞定业务数据 + 事件
public Order createOrder(CreateOrderCommand command) {
// 1. 写业务数据
Order order = Order.builder()
.orderId(generateOrderId())
.userId(command.getUserId())
.items(command.getItems())
.totalPrice(calculateTotal(command.getItems()))
.status(OrderStatus.CREATED)
.build();
orderRepository.save(order);
// 2. 写 Outbox 事件(同一个事务!)
OutboxEvent event = OutboxEvent.builder()
.aggregateType("Order")
.aggregateId(order.getOrderId())
.eventType("OrderCreated")
.payload(toJson(Map.of(
"orderId", order.getOrderId(),
"userId", order.getUserId(),
"items", order.getItems(),
"totalPrice", order.getTotalPrice(),
"createdAt", Instant.now()
)))
.build();
outboxRepository.save(event);
// 事务提交后,CDC 进程会自动将事件发送到消息队列
return order;
}
}
两种 Outbox 事件投递方式
方式一:定时轮询(Polling Publisher)
简单直接,适合中小规模系统:
// 定时轮询 Outbox 表,发送未发布的事件
@Component
public class OutboxPollingPublisher {
private final OutboxEventRepository outboxRepository;
private final KafkaTemplate<String, String> kafkaTemplate;
@Scheduled(fixedDelay = 1000) // 每秒轮询一次
@Transactional
public void publishPendingEvents() {
// 使用 SELECT ... FOR UPDATE SKIP LOCKED 避免集群中多个实例重复消费
List<OutboxEvent> events = outboxRepository
.findUnpublishedForUpdate(100); // 每次最多处理 100 条
for (OutboxEvent event : events) {
try {
kafkaTemplate.send(
"events." + event.getAggregateType(),
event.getAggregateId(),
event.getPayload()
).get(5, TimeUnit.SECONDS); // 同步等待确认
event.setPublished(true);
event.setPublishedAt(Instant.now());
outboxRepository.save(event);
} catch (Exception e) {
event.setRetryCount(event.getRetryCount() + 1);
outboxRepository.save(event);
log.error("Outbox 事件发送失败: id={}, retryCount={}",
event.getId(), event.getRetryCount(), e);
}
}
}
}
💡 **提示:**使用
SELECT ... FOR UPDATE SKIP LOCKED可以让集群中多个实例安全地并发处理 Outbox 事件,避免重复发送。这是 PostgreSQL 和 MySQL 8.0+ 都支持的特性。
方式二:CDC(Change Data Capture)— Debezium
高吞吐场景下的首选,通过监听数据库 binlog/WAL 捕获变更:
# Debezium connector 配置(Kafka Connect)
# docker-compose.yml 中配置 Debezium 连接器
name: outbox-connector
config:
connector.class: io.debezium.connector.mysql.MySqlConnector
database.hostname: mysql
database.port: 3306
database.user: debezium
database.password: ${MYSQL_PASSWORD}
database.server.id: 184054
topic.prefix: outbox
table.include.list: mydb.outbox_events
transforms: outbox
transforms.outbox.type: io.debezium.transforms.outbox.EventRouter
transforms.outbox.table.field.event.key: aggregate_id
transforms.outbox.table.field.event.type: event_type
transforms.outbox.table.field.event.payload: payload
transforms.outbox.route.by.field: aggregate_type
| 投递方式 | 延迟 | 吞吐量 | 运维复杂度 | 适用规模 |
|---|---|---|---|---|
| 定时轮询 | 1-2 秒 | 中(千级 TPS) | ✅ 低 | 中小系统 |
| CDC/Debezium | 毫秒级 | ✅ 高(万级 TPS) | ❌ 高 | 大型系统 |
| 事务日志尾随 | 毫秒级 | ✅ 高 | 中 | 中大型系统 |
⚠️ 四、生产踩坑与避坑指南
坑 1:幂等性设计被忽略
Saga 的补偿操作和 Outbox 的重试机制都可能导致消息重复投递。如果下游服务不支持幂等处理,就会产生重复数据。
// ✅ 正确写法:下游服务必须做幂等处理
@Service
public class InventoryEventListener {
@KafkaListener(topics = "events.Order")
public void handleOrderCreated(String message,
@Header("idempotency-key") String idempotencyKey) {
// 检查是否已处理过(幂等性检查)
if (processedEventRepository.existsById(idempotencyKey)) {
log.info("事件已处理,跳过: {}", idempotencyKey);
return;
}
OrderCreatedEvent event = fromJson(message, OrderCreatedEvent.class);
// 扣减库存
inventoryService.deduct(event.getItems());
// 记录已处理的事件(与扣减在同一个事务中)
processedEventRepository.save(new ProcessedEvent(idempotencyKey));
}
}
📌 **记住:**所有事件消费者都必须实现幂等处理。推荐使用「事件 ID + 已处理事件表」的方式做幂等检查,而不是依赖数据库唯一约束(唯一约束在高并发下性能较差)。
坑 2:Saga 步骤顺序设计不当
Saga 中步骤的顺序至关重要。应该把最容易失败的步骤放在最前面,把最难补偿的步骤放在最后面。
❌ 错误顺序:创建订单 → 发货 → 扣减库存 → 支付
(发货后如果支付失败,补偿发货的成本极高)
✅ 正确顺序:支付预授权 → 扣减库存 → 创建订单 → 确认支付
(每一步的补偿成本递增,最贵的放最后)
坑 3:补偿操作的「空补偿」和「悬挂」问题
在 TCC 模式中特别常见,但 Saga 中也需要注意:
- 空补偿:Try 还没执行,Cancel 先到了(网络延迟导致)。解决:补偿操作必须检查原操作是否已执行。
- 悬挂:Cancel 先执行完了,Try 才到达。解决:Try 操作也要检查是否已被取消。
// 兼容空补偿和悬挂的库存扣减
public void compensateDeductInventory(String orderId) {
// 空补偿检查:如果原扣减操作不存在,直接返回
DeductionRecord record = deductionRepository.findByOrderId(orderId);
if (record == null) {
log.warn("空补偿:未找到扣减记录, orderId={}", orderId);
return; // 直接返回,不执行补偿
}
if (record.isCompensated()) {
log.warn("重复补偿:已补偿过, orderId={}", orderId);
return;
}
// 执行补偿:恢复库存
inventoryRepository.addStock(record.getSkuId(), record.getQuantity());
record.setCompensated(true);
deductionRepository.save(record);
}
坑 4:缺少可观测性
分布式事务的调试难度远超单体事务。必须在设计阶段就考虑可观测性:
- ✅ 每个 Saga 都有唯一 ID,贯穿所有步骤
- ✅ 使用 MDC(Mapped Diagnostic Context)将 Saga ID 注入日志
- ✅ 使用 OpenTelemetry 追踪跨服务调用链
- ✅ 监控 Saga 状态表中的「卡住」状态(STARTED 超过 5 分钟未完成)
- ✅ 为补偿失败设置告警阈值
坑 5:Outbox 事件膨胀
Outbox 表如果不清理,会随着时间无限增长。必须定期归档已发布的事件:
-- 定期清理已发布的 Outbox 事件(保留 7 天)
DELETE FROM outbox_events
WHERE published = TRUE
AND published_at < NOW() - INTERVAL 7 DAY
LIMIT 10000; -- 分批删除,避免长事务
⚠️ **警告:**不要一次性删除大量 Outbox 记录,这会导致长事务和锁竞争。使用
LIMIT分批删除,每次删除后 sleep 一段时间。
🎯 总结与实践建议
选型建议:
- ✅ 步骤 ≤ 3、流程简单 → 协调式 Saga + Outbox(轮询方式)
- ✅ 步骤 > 3、流程复杂 → 编排式 Saga + Outbox(Debezium CDC)
- ❌ 需要强一致、资金相关 → 考虑 TCC 模式(但开发成本高 3-5 倍)
- ❌ 简单 CRUD、单库操作 → 直接用本地事务,不要过度设计
推荐技术栈:
- Saga 编排器:Axon Framework、Temporal(推荐)、Camunda、自研轻量级编排器
- Outbox 实现:Debezium + Kafka Connect(高吞吐)、自研轮询(简单场景)
- 消息队列:Apache Kafka(推荐,天然支持顺序消费和幂等)、RabbitMQ
- 监控追踪:OpenTelemetry + Jaeger + Prometheus
⚡ **关键结论:**分布式事务没有银弹,但 Saga + Outbox 组合是当前工程实践中平衡一致性、性能和开发成本的最优方案。核心原则是:用最终一致性替代强一致性,用补偿操作替代回滚,用 Outbox 保证事件可靠投递。记住,设计分布式事务时,补偿逻辑的复杂度往往比正常流程更高——一定要在设计阶段就把所有失败场景想清楚。
相关工具推荐:
- 🔧 Temporal — 分布式工作流引擎,原生支持 Saga 编排
- 🔧 Debezium — 基于 CDC 的 Outbox 事件投递
- 🔧 jsjson.com JSON 格式化工具 — 在线格式化和校验 JSON 数据
- 🔧 jsjson.com JSON 数据转换 — 快速转换事件载荷格式