分布式事务
问题
跨服务的事务如何保证一致性?有哪些分布式事务方案?
答案
为什么需要分布式事务
微服务架构下,一个业务操作可能涉及多个服务的数据库。例如下单:
- 订单服务:创建订单
- 库存服务:扣减库存
- 支付服务:扣款
单个数据库的 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: 补偿操作失败怎么办?
答案:
- 重试:补偿操作设计为幂等,可安全重试
- 记录:持久化到数据库/日志
- 人工介入:告警通知,人工处理
Q3: 分布式事务和本地事务怎么选?
答案:
尽量避免分布式事务:
- 优先通过服务设计避免跨服务事务
- 能用单库事务就不要分布式事务
- 接受最终一致性时用 Saga/Outbox
- 强一致性场景考虑 TCC 或 2PC