引言:为什么分布式事务是微服务架构的核心挑战
在单体数据库时代,ACID 事务由数据库引擎统一保证。一旦系统演进为微服务架构,一个业务操作往往跨越多个服务、多个数据库,传统的事务边界被打破。2PC、Saga、TCC、事务消息、Outbox……面对这些模式,架构师需要在一致性、可用性、性能之间做出精准权衡。本文从原理到生产实践,全面拆解每一种模式的适用场景、失败处理与工程落地细节。
第一章:分布式事务的本质问题与理论基础
1.1 ACID 在分布式环境下的退化
当操作涉及多个独立数据库时,原子性的保证从单机的"WAL 日志刷盘"升级为"跨网络的协调共识"。网络分区、节点故障、时钟偏差让原子提交变成了一个分布式系统问题。
1.2 CAP 与 PACELC 的工程启示
CAP 定理告诉我们网络分区下 C 与 A 不可兼得。PACELC 进一步细化:无分区时需在延迟(L)和一致性(C)之间权衡。实际工程中,大部分微服务系统选择了"最终一致性 + 补偿机制"的长事务路径。
1.3 分布式事务模式全景分类
| 模式 | 一致性级别 | 性能 | 复杂度 | 典型场景 |
|---|---|---|---|---|
| 2PC/XA | 强一致 | 低(阻塞) | 中 | 同城双库转账 |
| 3PC | 强一致 | 极低(高延迟) | 高 | 理论多于实践 |
| TCC | 最终一致(强) | 高 | 高 | 电商下单、金融扣款 |
| Saga | 最终一致 | 高 | 中 | 旅行预订、长流程工单 |
| 事务消息(半消息) | 最终一致 | 高 | 中 | 异步通知、积分发放 |
| Outbox + CDC | 最终一致(至少一次) | 极高 | 低 | 事件驱动架构通用 |
第二章:2PC 两阶段提交——经典但受限
2.1 协议原理
2PC 将提交拆为两个阶段:第一阶段(Prepare)协调者询问所有参与者是否可以提交,全部同意后进入第二阶段(Commit)执行真正的提交。任一参与者否决则进入 Abort。
2.2 阻塞问题与协调者单点
2PC 最大的问题是阻塞:参与者在 Prepare 后持有锁等待协调者指令,如果协调者宕机,参与者将无限期阻塞并持有资源。这在微服务环境下不可接受——支付服务不可能因为协调者故障而永远锁住用户余额。
2.3 XA 规范与数据库支持现状
X/Open XA 定义了事务管理器(TM)和资源管理器(RM)之间的接口。MySQL InnoDB、PostgreSQL、Oracle 均支持 XA,但性能代价显著:一次 XA COMMIT 至少需要 3 次 fsync(Binlog + Redo Log + 网络往返),延迟比单机事务高 5-10 倍。
2.4 Seata AT 模式:2PC 的工程改良
Seata AT 模式通过解析 SQL 自动生成补偿 SQL,无需业务编写 Try/Confirm/Cancel。它要求业务表必须有主键,并在本地事务中同时写入 undo_log。原理是:一阶段本地提交 + 异步回滚日志。AT 模式对业务侵入小但仅支持关系型数据库。
-- Seata AT 模式核心:undo_log 表
CREATE TABLE undo_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
branch_id BIGINT NOT NULL,
xid VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL,
UNIQUE KEY uk_xid_branch (xid, branch_id)
);
第三章:TCC(Try-Confirm-Cancel)——业务层面的两阶段
3.1 三阶段语义
TCC 将分布式事务分解为三个业务操作:Try(资源预留/冻结)、Confirm(确认执行/扣减)、Cancel(回滚释放/解冻)。每个操作必须实现幂等、悬挂、空回滚三大防御机制。
3.2 电商下单场景的 TCC 实现
用户下单操作涉及:订单服务(创建订单)、库存服务(扣减库存)、积分服务(扣减积分)、优惠券服务(核销优惠券)。各服务的 TCC 逻辑如下:
// 库存服务 TCC 接口
public interface InventoryTccService {
// Try: 冻结库存,不实际扣减
@Transactional
boolean tryDeduct(String productId, int quantity, String xid, String branchId);
// Confirm: 从冻结中实际扣减
@Transactional
boolean confirmDeduct(String xid, String branchId);
// Cancel: 释放冻结库存
@Transactional
boolean cancelDeduct(String xid, String branchId);
}
// 实现关键点
// 1. 幂等:通过 xid + branchId 唯一键确保同一分支事务只执行一次
// 2. 防悬挂:Cancel 执行后记录分支状态,若后续 Try 到来则拒绝并回滚
// 3. 空回滚:若 Try 未执行则 Cancel 到来,Cancel 正常返回成功但不做操作
3.3 TCC 三大异常场景的工程处理
| 异常 | 现象 | 解决方案 |
|---|---|---|
| 空回滚 | Cancel 先于 Try 到达 | Cancel 时检查是否有 Try 记录,无则直接成功 |
| 悬挂 | Try 在 Cancel 后到达 | Cancel 后将分支状态标记为已回滚,Try 到来时拒绝 |
| 幂等 | 网络重试导致重复调用 | 通过 xid+branchId 唯一约束实现天然幂等 |
3.4 TCC 实践建议
TCC 适合对一致性要求较高且性能敏感的场景(支付、电商交易)。关键是 Try 阶段只"冻结"不"消耗",确保 Confirm 可以顺利执行。设计 TCC 接口时要确保 Try 和 Confirm 在语义上是对称的——Try 冻结 100 元,Confirm 才能真正扣减 100 元。
第四章:Saga 模式——长事务的补偿艺术
4.1 核心思想
Saga 将一个长事务拆分为一系列本地事务,每个本地事务有对应的补偿事务。如果某个步骤失败,则按逆序执行前面所有步骤的补偿操作。与 2PC 不同,Saga 永不持有长期锁。
4.2 编排式 Saga(Choreography)
各服务通过事件驱动协作:服务 A 完成后发布事件,服务 B 监听事件后执行自己的操作并发布新事件。实现简单但链路追踪困难。
// 旅行预订 Saga - 编排式
// 1. BookingService 创建订单 -> 发布 BookingCreated 事件
// 2. HotelService 监听 -> 预留房间 -> 发布 RoomReserved 事件
// 3. FlightService 监听 -> 锁定航班 -> 发布 FlightLocked 事件
// 4. PaymentService 监听 -> 处理扣款
// 失败补偿(监听前一步事件)
// FlightService 锁定失败 -> 发布 FlightUnavailable 事件
// BookingService 监听 -> 释放房间(RoomService.cancelReservation)
// -> 取消订单(BookingService.cancel)
4.3 协调式 Saga(Orchestration)
引入 Saga Orchestrator(编排器)集中协调。编排器显式调用各服务的操作,根据返回结果决定下一步动作或补偿路径。优点是链路清晰、可集中管理状态;缺点是编排器成为中心点。
// 协调式 Saga 编排器伪代码
public class OrderSagaOrchestrator {
public void execute(CreateOrderRequest request) {
SagaDefinition saga = SagaBuilder.start()
.step("createOrder")
.invoke(orderService::create, request)
.withCompensation(orderService::cancel, request.getOrderId())
.step("reserveInventory")
.invoke(inventoryService::reserve, request.getItems())
.withCompensation(inventoryService::release, request.getItems())
.step("processPayment")
.invoke(paymentService::charge, request.getPayment())
.withCompensation(paymentService::refund, request.getPayment())
.step("confirmOrder")
.invoke(orderService::confirm, request.getOrderId())
.build();
sagaExecutor.execute(saga);
}
}
4.4 Saga 隔离性缺失与对策
Saga 没有隔离性,并发 Saga 可能读到中间状态。工程上采用三种对策:(1)语义锁——在数据上加 flag 字段标识正在被 Saga 修改;(2)交换式更新——将增加和抵消操作设计为可交换,避免读脏数据;(3)乐观锁 + 版本号——并发冲突检测。
第五章:事务消息与最终一致性
5.1 RocketMQ 半消息模式
RocketMQ 的事务消息是 2PC 在消息队列层面的实现:先发送半消息(对消费者不可见),执行本地事务,然后提交或回滚半消息。Broker 通过回查接口确认本地事务状态。
// RocketMQ 事务消息生产者
TransactionMQProducer producer = new TransactionProducer(" order_group");
producer.setTransactionListener(new TransactionListener() {
// 执行本地事务(创建订单)
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
orderService.createOrder(msg); // 本地数据库操作
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
// 回查本地事务状态(Broker 在未收到确认时调用)
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
String orderId = msg.getKeys();
Order order = orderService.findById(orderId);
if (order != null) {
return LocalTransactionState.COMMIT_MESSAGE;
}
return LocalTransactionState.ROLLBACK_MESSAGE;
}
});
5.2 Kafka 事务消息
Kafka 从 0.11 版本引入事务支持,通过 Transactional API + 幂等生产者实现跨分区的原子写入。消费者通过 isolation_level=read_committed 仅读取已提交的事务消息。
// Kafka 事务消息生产者
producer.initTransactions();
try {
producer.beginTransaction();
producer.send(new Record<>("orders", orderKey, orderValue));
producer.send(new Record<>("order-events", eventKey, eventValue));
producer.commitTransaction(); // 原子提交两个分区的写入
} catch (Exception e) {
producer.abortTransaction(); // 全部回滚
}
第六章:Outbox 模式——事件驱动架构的基石
6.1 双重写入问题
服务既要更新数据库又要发布消息,如果两者不一致(数据库更新了但消息未发,或消息重发导致重复消费),系统将出现数据不一致。Outbox 模式将消息作为业务数据的一部分写入同一数据库事务。
6.2 实现原理
-- Outbox 表设计
CREATE TABLE outbox (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
aggregate_type VARCHAR(64) NOT NULL,
aggregate_id VARCHAR(64) NOT NULL,
event_type VARCHAR(64) NOT NULL,
payload JSON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
published TINYINT DEFAULT 0,
published_at TIMESTAMP NULL,
UNIQUE KEY uk_aggregate_event (aggregate_type, aggregate_id, event_type)
);
-- 业务操作与 Outbox 写入在同一事务中
BEGIN;
UPDATE orders SET status='PAID' WHERE id=1001;
INSERT INTO outbox(aggregate_type, aggregate_id, event_type, payload)
VALUES ('order', '1001', 'OrderPaid', '{"orderId":1001,"amount":299.00}');
COMMIT;
6.3 CDC 投递:Debezium + Kafka Connect
Outbox 表写入后,通过 CDC(Change Data Capture)工具读取 binlog 并投递到消息队列。Debezium 支持 Outbox Router 模式,可自动将 outbox 表的行转换为主题消息并路由到对应 Topic。
6.4 轮询发件箱(Polling Publisher)
不依赖 CDC 的简单方案:通过定时任务轮询 outbox 表中未发布的记录,处理后发送并标记已发布。缺点是延迟和重复发送(需消费者实现幂等),优点是不需要额外的 CDC 基础设施。
第七章:Seata 框架全景与模式选型
7.1 Seata 三种模式对比
| 模式 | 原理 | 侵入性 | 数据库支持 | 适用场景 |
|---|---|---|---|---|
| AT | 自动生成补偿 SQL | 低 | 关系型 DB | CRUD 为主的业务 |
| TCC | 自定义三阶段 | 高 | 无限制 | 跨异构存储、金融 |
| Saga | 状态机引擎 | 中 | 无限制 | 长流程、多服务 |
| XA | 数据库原生 XA | 低 | 支持 XA 的 DB | 强一致、低并发 |
7.2 Seata Saga 状态机设计器
Seata Saga 通过 JSON 定义状态机的状态转换,支持并行分支、条件路由、补偿触发和异步调用,并内置持久化到数据库的实例恢复能力。
第八章:生产级分布式事务的工程实践
8.1 幂等性设计:贯穿所有模式的底层要求
无论 TCC、Saga 还是事务消息,网络重试或服务重启都可能导致同一操作被调用多次。幂等性设计的核心是:为每个业务操作分配唯一请求 ID,在处理前检查该 ID 是否已处理过。
// 幂等性检查伪代码
public Result processPayment(String requestId, PaymentRequest req) {
// 1. 检查 requestId 是否已处理(利用 Redis SETNX 或数据库唯一约束)
if (idempotencyCache.isProcessed(requestId)) {
return idempotencyCache.getResult(requestId); // 直接返回上次结果
}
// 2. 执行实际业务逻辑
Result result = doProcess(req);
// 3. 缓存结果并标记为已处理
idempotencyCache.markProcessed(requestId, result, Duration.ofHours(24));
return result;
}
8.2 分布式事务监控告警
生产环境中需要对分布式事务进行全方位监控:事务悬挂率(长时间未 Confirm/Cancel 的 Try 记录)、补偿超时率、回查调用频率、Saga 执行耗时分布。Prometheus 指标建议涵盖:txn_status_total(各状态计数)、txn_duration_seconds(延迟分布)、txn_suspended_gauge(当前悬挂事务数)。
8.3 模式选型决策树
面对业务场景选择事务模式时,可遵循以下决策路径:操作跨几个数据库?是否要求强一致?能否接受补偿延迟?是否涉及异构存储?这些问题的答案直接决定了 2PC/AT、TCC、Saga 或 Outbox 中哪种模式最适合。
总结
分布式事务没有银弹。强一致性与高性能不可兼得,架构师的任务是根据业务特征选择最匹配的模式:金融核心用 TCC 保高一致,电商异构存储用 Saga 处理长链路,混合栈选 AT 降低改造成本,事件驱动优先 Outbox+CDC。理解每种的失败模式与补偿边界,才是生产级设计的核心。

发表评论 取消回复