跳到主要内容

分布式事务

问题

跨服务的事务如何保证一致性?有哪些分布式事务方案?

面试速答版

跨服务的事务如何保证一致性? 微服务下下单 = 订单服务 + 库存服务 + 支付服务,单库事务管不到跨服务。该问题的本质是在「一致性、可用性、复杂度」三者间取舍。

  • 互联网场景首选最终一致(Saga / Outbox),金融场景才考虑强一致(2PC / TCC)。

有哪些分布式事务方案?

  • 2PC(两阶段提交):协调者先「prepare」再「commit/rollback」,强一致但同步阻塞、协调者单点、性能差,传统 XA 事务在用。
  • TCC(Try-Confirm-Cancel):业务层手写三个接口(预冻 → 确认 → 释放),强一致但侵入业务、开发量大,金融净价顶/账户场景。
  • Saga:拆成多个本地事务,每个事务带一个补偿动作,某步失败就反向补偿之前的,适合长事务。微服务首选。
  • Outbox Pattern:业务事务内写 DB + 写事件到同库 outbox 表(保证原子),后台任务轮询 outbox 发 MQ,解决「双写 DB 和 MQ」的原子问题。
  • 幂等 + 重试 + 对账是万能补丁:所有下游接口要能重复调用不出错,异常走重试,极端场景靠定时对账提醒人工处理。

答案

为什么需要分布式事务

微服务架构下,一个业务操作可能涉及多个服务的数据库。例如下单:

  1. 订单服务:创建订单
  2. 库存服务:扣减库存
  3. 支付服务:扣款

单个数据库的 ACID 事务无法跨服务使用。

常见方案对比

方案一致性复杂度性能适用
2PC(两阶段提交)强一致传统数据库
Saga最终一致微服务
Event Sourcing最终一致事件驱动架构
Outbox Pattern最终一致消息可靠投递
TCC强一致极高金融场景

Saga 模式

saga.ts
// 编排式 Saga(协调者管理流程)
class OrderSaga {
private steps: SagaStep[] = [];

addStep(execute: () => Promise<void>, compensate: () => Promise<void>) {
this.steps.push({ execute, compensate });
}

async run() {
const executedSteps: SagaStep[] = [];

try {
for (const step of this.steps) {
await step.execute();
executedSteps.push(step);
}
} catch (error) {
// 逆序执行补偿操作
for (const step of executedSteps.reverse()) {
try {
await step.compensate();
} catch (compensateError) {
// 补偿失败,记录日志,人工介入
logger.error('Compensation failed', compensateError);
}
}
throw error;
}
}
}

// 使用
const saga = new OrderSaga();
saga.addStep(
() => orderService.create(orderId), // 执行
() => orderService.cancel(orderId), // 补偿
);
saga.addStep(
() => inventoryService.deduct(productId, qty),
() => inventoryService.restore(productId, qty),
);
saga.addStep(
() => paymentService.charge(userId, amount),
() => paymentService.refund(userId, amount),
);
await saga.run();

Outbox Pattern

outbox.ts
// 在同一个数据库事务中写业务数据和 Outbox
async function createOrder(order: CreateOrderDto) {
await prisma.$transaction(async (tx) => {
// 1. 写业务数据
const created = await tx.order.create({ data: order });

// 2. 写 Outbox 表(同一事务,保证原子性)
await tx.outbox.create({
data: {
aggregateType: 'Order',
aggregateId: created.id,
eventType: 'OrderCreated',
payload: JSON.stringify(created),
},
});
});
}

// 后台任务:轮询 Outbox 表,发布消息
async function publishOutboxEvents() {
const events = await prisma.outbox.findMany({
where: { published: false },
take: 100,
});

for (const event of events) {
await messageQueue.publish(event.eventType, event.payload);
await prisma.outbox.update({
where: { id: event.id },
data: { published: true },
});
}
}
为什么不直接发消息?

"写数据库 + 发消息" 不是原子操作。数据库写成功但消息发送失败,会导致数据不一致。Outbox Pattern 将消息写入数据库同一事务,保证了可靠性。


常见面试问题

Q1: Saga 的编排模式和协调模式有什么区别?

答案

维度编排(Orchestration)协调(Choreography)
协调方式中心协调者管理各服务通过事件通信
耦合度协调者知道所有步骤服务间松耦合
可观测性好(流程集中)差(流程分散)
适用流程复杂流程简单(2-3 步)

Q2: 补偿操作失败怎么办?

答案

  1. 重试:补偿操作设计为幂等,可安全重试
  2. 记录:持久化到数据库/日志
  3. 人工介入:告警通知,人工处理

Q3: 分布式事务和本地事务怎么选?

答案

尽量避免分布式事务:

  1. 优先通过服务设计避免跨服务事务
  2. 能用单库事务就不要分布式事务
  3. 接受最终一致性时用 Saga/Outbox
  4. 强一致性场景考虑 TCC 或 2PC

相关链接