跳到主要内容

与数据库对比

查看 Markdown

大多数后端把状态存在数据库里,再围绕数据库搭建业务流程。下面先用这种方式一步步搭一个订单流程,再用 Dex Flow 实现同一个流程。

完整的订单流程先向买家扣款,再发货。发货前,它要等卖家审批,等待期间提醒卖家。发货失败持续一小时后,它给买家退款。两种设计为此需要的部件如下:

数据库加队列

14个部件要你自己搭建和运行

表
  • 带 status 的订单表
  • 审批表
队列
  • charge-requests
  • ship-requests
  • refund-requests
  • retry topic 或 visibility timeout
  • 死信队列(DLQ)
服务和任务
  • 扣款消费者
  • 发货消费者
  • 退款消费者
  • 提醒 cron job
  • 审批接口
  • 状态轮询接口
  • DLQ 查看和修复工具

运行在数据库和队列 broker 上(不计入)

Dex

5个部件要你自己编写和运行

一个 Flow
  • Charge Step
  • Ship Step
  • Refund Step
  • approve RPC
服务
  • 运行这个 Flow 的 Worker

运行在 Dex Server 上,它提供(不计入)

  • 每个 Step 自己的 retry
  • Durable Timer
  • 等待 Channel
  • Client 等待 API
  • Step 历史和重新执行

数据库做不到什么​

PostgreSQL、MySQL、S3 这类数据库和存储服务,负责保存数据,并在你查询时返回数据。它们是被动的:只有你的代码调用它们时,它们才会做事。

可靠的后端需要的不只是存储。它要跑在多台机器上,这样一台出故障不会让服务停下,也能承接更多流量。一个 API 常常要调用好几个别的 API 和服务,一次数据库事务覆盖不了。

一个典型的后端 API 还要:

  • 串行或并行地调用多个 API。
  • 安排之后要做的事,比如提醒和业务超时。
  • 等待其他系统,比如人工审批或外部事件。
  • 处理失败:retry、回滚,以及用于人工修复的死信队列(DLQ)。每一步的策略可能都不一样。
  • 能看到每一步的输入、错误,以及还没做完的工作。
  • 流量增长时,依然可用、够快、成本可控。

这些数据库都不提供。每个服务都要把这些代码重新写一遍。

例子:分两步处理订单​

订单流程只有两步,每一步是一次 API 调用:

  • 向买家扣款。
  • 发货。

做原型时,会直接依次调用这两个 API:

process_order.pyPython
def process_order(order):
call_charge_api(order)
call_ship_api(order)

只要不出错,这样就能用。但如果扣款成功后进程崩溃,或者发货调用超时,买家已经付了钱,货却永远不会发出。

方案 1:数据库行加轮询 worker​

下一个想法是:插入一行 status = pending 的订单,立即返回给客户端,再让后台 worker 去取这一行来处理。

订单保存下来了,但新问题也来了:

  • 多个 worker 怎么取行,才不会抢到同一行?
  • worker 取到一行之后、处理完之前崩溃了,怎么办?

方案 2:步骤之间用消息队列​

有经验的团队通常会改用消息队列,每一步一个队列:charge-requests 和 ship-requests。订单先作为一条消息进入 charge-requests。扣款消费者处理完这条消息,再往 ship-requests 发一条消息。

队列解决了方案 1 的问题,但带来了更难的问题:

  • 大多数队列无法把「扣款完成」和「发出发货消息」一起提交。如果发货消息已经发出,但扣款消息的确认超时了,队列会再投递一次扣款消息。于是同一笔订单在发货的同时,又在重复扣款。
    • 团队通常再加一行数据库记录作为唯一可信来源,用来去重。
  • 带退避的 retry 和 DLQ 往往要自己补。
    • 比如 Kafka 要额外建 retry topic,SQS 要靠 visibility timeout 绕过去。
    • 查看和恢复 DLQ 里消息的工具,也得自己做。
  • 前端可能要在扣款成功后马上返回(「已付款,订单处理中」)。这时它只能轮询数据库,既别扭又浪费。

业务还会不断变化。订单流程常常还要:

  • 发货 API 重试后仍然失败时给买家退款,比如连续失败一小时后退款。
  • 发货前等卖家审批,或者让卖家手动填写运单号。
  • 提醒卖家去审批或填写运单号。

在队列方案里,每一项都要加队列、消费者、表或 cron job。

用 Dex Flow 实现同一个流程​

在 Dex 里,你只写业务逻辑。retry、timer、等待、崩溃后恢复,以及每个 Step 的记录,都由 Dex 提供。

扣款、审批、发货的流程写成:

  1. Charge Step:向买家扣款,有自己的 retry 策略。扣款成功后,Dex 把订单推进到 Ship Step。Dex 把结果和这次推进一起记录下来,所以崩溃不会让订单卡在两个 Step 之间。
  2. Ship Step:
    1. 等待卖家审批,或者等一个 durable timer。
    2. timer 先到,就发提醒并继续等;审批到了,就发货。
    3. 发货调用会 retry。retry 用尽后,进入 Refund Step。
  3. Refund Step:给买家退款。
  4. 前端调用一个内置的 Client API,等 Charge Step 完成后显示「已付款」。

Dex 记录每个 Step 的输入、输出和错误。运维人员可以查看每一笔订单,并在修复后从之前的某个 Step 重新执行(time travel)。

问题数据库加队列Dex Flow
两步之间崩溃买家可能已扣款,货却没发Dex 把每个 Step 的结果和到下一个 Step 的推进一起记录
多个 worker自己实现抢行,或依赖队列投递Dex 把每个任务派给你的某个 Worker

需求变化时​

每个新需求都会给队列方案加部件。在 Dex 里,它最多加一个 Step 或 RPC,再改一下 Ship Step。

发货失败一小时后给买家退款

数据库加队列要加
  • refund-requests
  • 退款消费者

发货消费者记下第一次失败的时间,一小时后放弃。

Dex 要加
  • Refund Step

Ship Step:retry 一小时,然后进入 Refund Step。

发货前等卖家审批

数据库加队列要加
  • 审批表
  • 审批接口

改由审批接口写入 ship-requests,扣款消费者不再写。

Dex 要加
  • approve RPC

Ship Step 等待卖家 Channel 上的消息。

提醒卖家

数据库加队列要加
  • 提醒 cron job

这个 job 扫描审批表,找出还在等的订单。

Dex 要加

Ship Step 同时等一个 Timer。Timer 到了,就发提醒,然后继续等。

5个新部件数据库加队列2个新部件Dex

为什么是 Dex? 给出这条 Flow 的 Dex 代码,Quick Start 会把它跑起来。