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 不是魔法。它需要外部副作用也尽量支持幂等。
小练习
- 给
charge-payment生成一个稳定的 payment id,并传给外部支付 API。 - 模拟“API 成功但 Journal 写失败”的窗口,思考如何用幂等 key 降低风险。