cswork / 系统设计

第 38 章 / 共 57 章 · 约 4 分钟

设计模式:多步骤流程

本页目录
  1. 1. 问题是什么
  2. 2. 基础构件
  3. 3. 方案
  4. 4. 怎么选
  5. 5. 常见追问
  6. 6. 本站用到这个模式的案例
  7. 章节测验

下单这件事背后往往是好几步:占库存、扣款、生成订单、发通知,分别属于不同的服务和数据库。任何一步都可能失败、超时或者重复执行,而跨服务又没有一个能回滚一切的数据库事务。这一章讲怎样让这类流程“要么走完,要么干净地撤回”,并且在重试时不出错。

1. 问题是什么

  • 部分失败:扣款成功了,生成订单时服务挂了。钱扣了,订单没有。
  • 超时的不确定性:调用支付超时,你不知道对方是成功了还是没收到请求。
  • 重复执行:为了不丢,所有步骤都会重试,于是同一步可能执行两次。
  • 长时间等待:有的步骤要等外部回调或人工审核,流程可能持续几分钟到几天。

2. 基础构件

2.1 幂等

每一步都要能安全地重复执行:调用方带上幂等键(例如 order_id 或客户端生成的请求 id),被调用方记住处理过的键和结果,再次收到时直接返回第一次的结果。没有幂等,重试就等于重复扣款。

2.2 事务性发件箱(outbox)

“改数据库”和“发消息”是两个系统,先后执行总有一步可能失败。outbox 的做法是:在同一个数据库事务里,既更新业务表,又往 outbox 表插入一条待发事件;再由一个中继进程读取 outbox,发到 Kafka 或队列,发成功后标记已发送。这样事件不会丢,最多重复,消费方用幂等去重。也可以用 CDC 直接读数据库日志代替中继。

3. 方案

3.1 两阶段提交(2PC)

协调者先让所有参与者“准备”,全部同意后再“提交”。能做到原子性,但协调者故障时参与者会一直锁着资源等待,而且要求所有参与者都支持 XA 之类的协议。跨服务、跨公司的流程(例如外部支付网关)基本用不了,面试里说清楚为什么不用即可。

3.2 Saga

把流程拆成一串本地事务,每一步都有对应的补偿操作。某一步失败时,按相反的顺序执行已完成步骤的补偿:退款、释放座位、取消订单。Saga 不提供隔离性:中间状态对外可见(座位显示“已预占”),设计时要接受并处理这一点。

  • 编排式(orchestration):一个协调服务按状态机驱动各步,知道整个流程走到哪里,失败时负责补偿。流程清楚、好排查,推荐作为默认答案。
  • 协同式(choreography):各服务监听上一步的事件,自己决定下一步。耦合低,但流程散落在各个服务里,步骤一多就很难看清全貌。
订票 Saga 与补偿
滚动到此处后加载图表…
图表可横向滚动,展开后可缩放查看。
查看图表源码
flowchart LR
  accTitle: 订票 Saga 与补偿
  start["开始"] --> hold["1. 预占座位"]
  hold --> pay["2. 扣款"]
  pay --> confirm["3. 确认订单"]
  confirm --> notify["4. 发送通知"]
  pay -.->|"扣款失败"| release["补偿:释放座位"]
  confirm -.->|"确认失败"| refund["补偿:退款"]
  refund -.-> release

3.3 持久化工作流引擎

Temporal、AWS Step Functions 这类引擎把“流程走到哪一步、每一步的结果”持久化,进程崩溃后从断点继续;还自带重试、超时、定时器(“30 分钟未付款就取消”)。写起来像普通代码,适合步骤多、等待长、需要人工介入的流程。代价是多一个要运维(或付费)的系统。

3.4 数据库里的状态机

规模不大时,一张 orders 表加 status 字段(PENDING → PAID → CONFIRMED,或 → CANCELLED),配合定时任务扫描超时的记录,往往就够了。状态迁移用条件更新(WHERE status='PENDING'),防止并发的两次迁移。

4. 怎么选

场景推荐
一个服务、一个数据库内直接用本地事务
改数据库并通知其他服务outbox(或 CDC)加幂等消费者
三四步、跨服务、需要回滚编排式 Saga,状态存数据库
步骤多、等待长、有定时和人工环节工作流引擎
需要跨库强原子性、参与者都可控2PC,但要说明可用性代价

5. 常见追问

  • 调用支付超时了,要不要重试? 带同一个幂等键重试,或者先查询这笔支付的状态;绝不能换一个新键重试。
  • 补偿也失败了怎么办? 补偿操作同样要幂等、可重试;多次失败后进入人工处理队列,并且有对账任务兜底。
  • 怎么防止消息丢失? outbox 保证“数据库改了就一定会发消息”;消费方处理完再提交 offset,保证“消息一定会被处理”。
  • 流程的中间状态用户能看到吗? 能,所以要设计成用户可以理解的状态(“处理中”“等待付款”),并给出超时时间。

6. 本站用到这个模式的案例

章节测验

5 道选择题
  1. 订单服务提交了数据库事务,然后在发布“订单已创建”事件之前崩溃。下游永远收不到这个事件。怎么修?
  2. 调用第三方支付接口超时,不知道扣款是否成功。正确的处理是?
  3. 订票流程是“预占座位 → 扣款 → 确认订单”。扣款失败时,Saga 应该做什么?
  4. 为什么跨公司的支付流程(例如调用外部支付网关)一般不用两阶段提交?
  5. 一个退款流程要等人工审核(可能几天),中间还有“48 小时未审核自动升级”的定时规则。哪种实现最合适?

章节讨论

正在加载讨论…