Skip to content

Demo 02:步骤 Journal

入口幂等只能防止重复创建流程。接下来要解决:流程内部某一步已经成功,重试时如何不重复执行?

答案是步骤 Journal。

Step index 比 step name 更重要

教学版 Journal 使用 (invocation_id, step_index) 做唯一键:

sql
CREATE TABLE journal_entries (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  invocation_id CHAR(36) NOT NULL,
  step_index INT NOT NULL,
  step_name VARCHAR(255) NOT NULL,
  status VARCHAR(32) NOT NULL,
  result JSON NULL,
  error TEXT NULL,
  created_at DATETIME NOT NULL,
  UNIQUE KEY uq_invocation_step (invocation_id, step_index)
);

为什么不用 step_name 做唯一键?因为同一个函数里可能循环调用同名步骤:

python
for item in items:
    ctx.run("reserve-item", lambda: reserve(item))

真正稳定的是执行顺序。Restate 生产版有更复杂的 journal entry 协议;教学版用 step index 可以清楚表达机制。

ctx.run 的核心逻辑

python
def run(self, name, fn):
    index = self.next_step_index()
    existing = self.find_entry(index)
    if existing and existing.status == "COMPLETED":
        return existing.result

    result = fn()
    self.append_completed_entry(index, name, result)
    return result

这个函数把普通副作用变成“先看历史,再决定是否执行”。

提交点

Durable Execution 里最敏感的问题是:什么时候算“这一步已经发生”?

在 Restate 架构里,分区 leader 把 step event 追加到复制日志并获得 quorum ack 后,这一步才成为 durable fact。教学版简化为:Journal 写入 MySQL 并提交事务后,这一步才算完成。

当前 Demo 的边界

这个教学实现仍有一个现实问题:如果外部 API 成功了,但进程在写 Journal 前崩溃,重试仍可能再次调用外部 API。

生产系统通常用组合拳降低风险:

手段作用
外部 API idempotency key外部副作用自身去重
Journal commit运行时恢复时跳过已完成步骤
Outbox / transactional messaging数据库写和消息发送建立可靠交接
补偿逻辑无法避免时提供人工或自动修正

所以工程上要记住:ctx.run 不是魔法。它需要外部副作用也尽量支持幂等。

小练习

  1. charge-payment 生成一个稳定的 payment id,并传给外部支付 API。
  2. 模拟“API 成功但 Journal 写失败”的窗口,思考如何用幂等 key 降低风险。

Teaching project inspired by Restate's public architecture and documentation.