分布式事务(2PC,Saga)在系统设计中
Source: Dev.to
请提供您希望翻译的完整文本内容,我会按照要求保留链接、格式和代码块,仅翻译正文部分。
介绍
在现代分布式系统的复杂环境中,保持多个独立服务和数据库之间的数据一致性是系统设计中最具挑战性的问题之一。分布式事务为确保跨多个资源的操作要么全部成功,要么全部失败提供了基础,从而保持原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)这四大 ACID 特性。本文探讨了处理分布式事务的两种主要方法:通常称为 2PC 的两阶段提交(Two‑Phase Commit)协议,以及 Saga 模式。每种方法都旨在协调长时间运行的事务,在传统单数据库事务无法满足的环境中提供解决方案。
分布式事务概述
分布式事务涉及多个参与资源,例如独立的数据库、微服务或外部系统,这些资源必须协同工作以实现统一的结果。与局限于单一资源的本地事务不同,分布式事务必须在处理网络延迟、部分失败以及组件独立扩展的情况下,管理跨服务的一致性。
核心需求与单体系统相同:整个操作对最终用户必须表现为原子性。如果任何部分失败,所有更改都必须被撤销。然而,在分布式环境中实现这一点会引入显著的复杂性,因为每个参与者都是自治的,且通信发生在不可靠的网络上。系统设计者因此必须选择在强一致性、可用性和性能之间取得平衡的协议。
两阶段提交(2PC)
两阶段提交协议,或 2PC,是实现分布式事务强一致性的经典解决方案。该协议在 1970 年代提出,依赖中心协调者和多个参与者,以确保跨异构资源的全有或全无语义。
关键组件
- Coordinator – 负责驱动事务的中心权威。它接收初始事务请求并管理投票和决策过程。
- Participants – 执行本地工作并响应协调者指令的各个资源(数据库或服务)。
- Transaction Manager – 通常使用 XA(eXtended Architecture)等标准实现,用于数据库交互。
协议阶段
Prepare Phase(投票阶段)
- 协调者向所有参与者发送 prepare 消息。
- 每个参与者执行必要的本地操作、获取锁、将更改写入持久日志,并返回 ready(投票 yes)或 abort(投票 no)。
- 若有任何参与者投 no 或未响应,协调者决定中止。
Commit Phase(决策阶段)
- 若所有参与者都投 ready,协调者记录全局提交决策并向每个参与者发送 commit 消息。
- 每个参与者随后永久应用更改并释放锁。
- 若决定中止,协调者发送 rollback 消息,参与者使用已准备的日志条目撤销本地更改。
示例实现
class TwoPhaseCommitCoordinator {
List participants;
TransactionLog log;
void beginTransaction(Transaction tx) {
log.write("BEGIN_TX", tx.id);
boolean allReady = true;
// Prepare Phase
for (Participant participant : participants) {
Response response = participant.prepare(tx);
if (!response.isReady()) {
allReady = false;
break;
}
}
// Decision
if (allReady) {
log.write("GLOBAL_COMMIT", tx.id);
for (Participant participant : participants) {
participant.commit(tx);
}
} else {
log.write("GLOBAL_ABORT", tx.id);
for (Participant participant : participants) {
participant.rollback(tx);
}
}
}
}class DatabaseParticipant implements Participant {
LocalDatabase db;
UndoLog undoLog;
Response prepare(Transaction tx) {
try {
db.acquireLocks(tx.operations);
db.executeOperations(tx.operations); // tentative changes
undoLog.recordUndoInfo(tx);
return new Response(true, "READY");
} catch (Exception e) {
return new Response(false, "ABORT");
}
}
void commit(Transaction tx) {
db.makeChangesPermanent(tx);
db.releaseLocks(tx);
undoLog.clear(tx);
}
void rollback(Transaction tx) {
db.applyUndo(undoLog.getUndoInfo(tx));
db.releaseLocks(tx);
undoLog.clear(tx);
}
}这些代码结构展示了 2PC 的阻塞特性:参与者从准备阶段起一直持有锁,直至最终决策到达。协调者必须在继续之前将其决策持久化,以确保在崩溃后能够恢复。
2PC 的缺点
- 单点故障 – 协调者是瓶颈,也是潜在的宕机来源。
- 阻塞协议 – 若协调者在准备阶段后失效,参与者会无限期保持锁定,直至恢复。
- 网络分区 – 可能导致长时间不可用。
- 性能影响 – 需要同步协调和锁持有,使得 2PC 在高吞吐、长生命周期的微服务场景下不切实际。
Saga 模式
Saga 模式通过采用 最终一致性 而非即时强一致性,提供了一种根本不同的分布式事务处理方法。该模式最初在 1980 年代用于处理长期事务,Saga 将一个大型分布式事务拆分为一系列较小的本地事务。每个本地事务都有一个关联的 补偿事务,如果后续步骤失败,它可以撤销该事务的影响。
- 本地事务 – 每个服务独立执行其部分并立即提交。
- 补偿事务 – 可逆操作,用于恢复之前的状态。
Saga 与两阶段提交(2PC)
关键特性
| 方面 | 两阶段提交(2PC) | Saga |
|---|---|---|
| 锁定 | 全局锁在事务完成前保持 | 无全局锁;资源保持可用 |
| 一致性 | 即时、强一致性(全有或全无) | 最终一致性——系统随时间收敛 |
| 故障处理 | 若任何参与者无法提交,则全局中止 | 对每个失败步骤进行补偿(撤销)操作 |
| 可扩展性 | 受协调开销和锁争用限制 | 高度可扩展;每个服务管理自己的 ACID 事务 |
| 典型用例 | 短期、关键的金融操作 | 跨多个服务的长期业务流程 |
Saga 风格
1. 基于编舞的 Saga
- 服务 直接 通过事件进行通信。
- 每个服务 监听 前一步的事件,并在完成(或失败)后 发布 自己的事件。
- 没有中心控制器 → 松耦合,但随着服务数量增加,追踪可能变得复杂。
2. 基于编排的 Saga
- 一个中心 Saga Orchestrator 通过向服务发送命令并响应其返回的事件来驱动流程。
- Orchestrator 维护整体状态,决定下一步操作,并在需要时触发补偿操作。
- 提供更清晰的可视性和更简洁的错误处理。
示例:在线商店订单工作流
场景: 下单涉及三个服务:
- Order Service
- Payment Service
- Inventory Service
Saga 保证:
- 如果支付失败 → 库存 不 扣减。
- 如果库存不足 → 支付会被退款。
编排器实现(类 Java 伪代码)
class OrderSagaOrchestrator {
OrderService orderService;
PaymentService paymentService;
InventoryService inventoryService;
SagaStateRepository stateRepo;
void startOrderSaga(OrderRequest request) {
SagaInstance saga = new SagaInstance(request.orderId);
stateRepo.save(saga);
// Step 1: Create Order (local transaction)
Order order = orderService.createOrder(request);
saga.updateStep("ORDER_CREATED", order);
try {
// Step 2: Process Payment
Payment payment = paymentService.processPayment(order);
saga.updateStep("PAYMENT_SUCCESS", payment);
// Step 3: Reserve Inventory
InventoryReservation reservation = inventoryService.reserveInventory(order);
saga.updateStep("INVENTORY_RESERVED", reservation);
saga.complete(); // saga finished successfully
return;
} catch (PaymentFailedException e) {
// Compensation: Cancel Order
orderService.cancelOrder(order);
saga.fail("PAYMENT_FAILED");
} catch (InventoryUnavailableException e) {
// Compensation chain
paymentService.refundPayment(payment);
orderService.cancelOrder(order);
saga.fail("INVENTORY_FAILED");
}
}
// ---- Compensating transaction examples ----
void compensatePayment(Payment payment) {
paymentService.refundPayment(payment); // idempotent refund
}
void compensateOrder(Order order) {
orderService.cancelOrder(order); // releases any reservations
}
}库存服务(本地事务 + 补偿)
class InventoryService {
InventoryRepository repo;
// Local transaction – fully committed immediately
InventoryReservation reserveInventory(Order order) {
return repo.withinTransaction(() -> {
Stock stock = repo.findStock(order.productId);
if (stock.quantity {
Stock stock = repo.findStock(reservation.productId);
stock.quantity += reservation.quantity;
repo.save(stock);
});
}
}注意: 每个命令和补偿都应携带 幂等键,以在网络故障后安全重试。
何时使用哪种模式?
| 情形 | 首选方法 |
|---|---|
| 强一致性、即时一致性(例如,银行业务、账本更新) | 2PC |
| 长期运行的多服务业务流程,可以接受临时不一致 | Saga |
| 混合:有界上下文内的关键同步步骤 + 跨上下文编排 | 将 2PC(用于关键部分) + Saga(用于其余部分)结合使用 |
为什么 Saga 在微服务中受欢迎
- 无长时间持有的锁 → 更高的可用性。
- 水平可扩展性 – 每个服务仅处理自己的 ACID 事务。
- 有针对性的补偿 而不是全局中止。
- 自然适配 事件驱动架构 和消息中间件(Kafka、RabbitMQ),以实现可靠投递。
正确的 Saga 实现需要:
- 深思熟虑的 补偿事务 设计。
- 强健的 幂等性 处理。
- 对 Saga 实例进行全面的 监控,以检测并解决卡住的工作流。
进一步阅读
System Design Handbook – 深入探讨分布式系统、事务模式等。
- 📚 购买手册:
- ☕ 支持作者: