代表性面试主题

如何设计 Saga 编排多服务订单履约并处理补偿?

系统设计困难
OfferCC 编辑团队发布 更新

题干

一个订单需要依次完成库存预留、支付授权、配送创建和优惠券扣减,但每个服务拥有独立数据库,不能使用两阶段提交。请设计 Saga,并说明编排、重试、补偿、超时、人工介入和状态查询。

1. 题目

用户提交订单后,系统要在库存、支付、配送和优惠券四个服务中完成履约。任一步骤可能因业务规则或网络故障失败,系统不能回滚已经提交的远程数据库事务。请设计一套 Saga,让订单最终进入 CONFIRMED 或 CANCELLED,并能解释每个局部事务和补偿动作。

2. 约束与澄清

  • 每个服务只对自己的数据库做本地 ACID 事务。
  • 流程允许最终一致,但不能无限占用库存或支付授权。
  • 重试可能重复执行步骤;服务必须使用幂等键和状态机保护。
  • 需要区分可重试的技术失败、不可重试的业务拒绝和需要人工处理的补偿失败。

3. 核心思路

Saga 把一个长事务拆成一系列有序的局部事务 T1 ... Tn。每个步骤成功后记录进度并触发下一步;若后续步骤失败,则按逆序执行已完成步骤的补偿 Ck ... C1。补偿不是数据库回滚,而是新的业务操作,例如释放库存、撤销支付授权、取消配送和返还优惠券。

编排式 Saga 由一个持久化协调器保存状态和下一动作,适合需要明确流程、超时和人工介入的履约链路。事件驱动的 choreography 可以减少中心协调,但流程可见性和循环控制更难。无论模式如何,步骤和补偿都应携带 saga_id、step_id、版本和幂等键。

4. 参考实现

text
start(order):
  saga = create_saga(order.id, state="RESERVE_STOCK")
  dispatch(saga, "ReserveStock")

on_step_result(saga_id, step_id, result):
  saga = load_and_lock(saga_id)
  require result.version == saga.version + 1
  if result.success:
    saga.completed_steps.append(step_id)
    saga.version += 1
    next = next_step(saga)
    persist(saga)
    dispatch(next) if next else finish_confirmed(saga)
  else if result.business_rejection:
    saga.state = "COMPENSATING"
    persist(saga)
    dispatch(compensation_for_last_completed(saga))
  else:
    schedule_retry_or_timeout(saga, step_id)

on_compensation_result(saga_id, step_id, result):
  record_attempt(saga_id, step_id, result)
  if result.success:
    dispatch(previous_compensation(saga))
  else:
    mark_manual_intervention(saga, reason=result.error)

5. 一致性与正确性

协调器状态必须持久化,否则进程崩溃会丢失下一步。每个命令和事件使用幂等键;消费者先记录已处理的 step_id,再提交业务变化,避免重试造成双扣库存。步骤完成和发布下一命令之间也有发送失败窗口,应使用 Outbox、可靠队列或 CDC 保证命令最终可见。

补偿顺序通常与成功步骤相反,但不要求每个动作都能严格撤销;有些动作只能做业务上的反向调整,例如退款而非撤销已发出的通知。状态查询应返回当前步骤、已完成步骤、重试次数和人工原因,避免把“处理中”误报为成功。

6. 追问与陷阱

  • 不要把 Saga 描述为跨数据库的原子回滚;它提供的是可恢复的最终一致性。
  • 补偿动作也会失败,必须有重试、死信、告警和人工接管,而不是无限自动循环。
  • 仅靠全局分布式锁会放大故障域,无法解决远程服务已经提交后的业务撤销。
  • 需要明确库存预留和支付授权的 TTL,超时后由定时任务或事件驱动释放资源。

7. 延伸阅读

可以比较编排式与 choreography:前者集中记录状态、顺序和超时,后者通过事件解耦服务但更难追踪全链路。还应讨论“不可补偿的副作用”、业务语义锁、版本冲突、审计日志,以及何时把流程改回单体数据库事务。

8. 面试评分点

能拆分局部事务

应明确列出库存、支付、配送和优惠券步骤,说明每个服务只提交自己的本地事务。

能设计补偿状态机

应展示成功路径、失败路径和逆序补偿,并区分业务拒绝、技术重试和人工接管。

能处理幂等与消息可靠性

应使用 saga_id、step_id、幂等键和 Outbox/可靠队列,覆盖协调器崩溃与命令发送窗口。

能说明业务边界

应承认补偿不是回滚,讨论 TTL、不可补偿副作用、状态查询和最终一致性的用户体验。

公开来源

同类题目

资源

产品对比

每份对比均基于官方资料,并标注复核日期。

查看完整对比

相关面试工具

用 Solve 整理系统设计回答

从澄清需求开始,展开规模、架构、组件选择和取舍。

查看工具