Effect Workflow 的幂等要做到哪几层?
我让同一个订单号提交了两次,调用序列里只出现了一遍 reserve > charge > dispatch。而让网关超时两次再成功,charge 被调用了三次,但支付网关只见过一个幂等键。
effect/workflow 里的幂等分三层:流程级的 idempotencyKey 合并重复提交,步骤级的 Activity 记忆化保护已完成的步骤,外部调用级的 Activity.idempotencyKey 让第三方去重。缺一层,就可能重复副作用。
前两层框架替你做了一大半,第三层必须你自己用对。下面逐层看。
流程级:怎么防止同一个订单开出两条流程?
在 Workflow.make 里声明 idempotencyKey,它决定执行 id:
export const Checkout = Workflow.make("Checkout", {
payload: { orderId: Schema.String, cents: Schema.Number },
idempotencyKey: ({ orderId }) => orderId,
success: Schema.Struct({ chargeId: Schema.String }),
error: Schema.Union([PaymentDeclined, GatewayTimeout, OutOfStock])
})我声明的是 orderId,同一个订单号无论提交多少次,都映射到同一次执行。我用内存引擎对同一个订单连续 Checkout.execute 两次,调用序列只有一遍,第二次没有重新执行,直接得到同一个结果。
键必须来自业务身份,而不是随机数或时间戳,否则每次提交都是新流程。
步骤级:重放时已完成的步骤会重做吗?
不会。每个 Activity.make 有稳定的 name,完成后结果被保存,重放时直接取记忆的结果。我在审批场景里看到:流程在审批后被重放,reserve 仍然只出现了一次。
反过来,name 是流程历史的一部分,改名等于改历史。已发布的 Activity,不要随意重命名或调换顺序。
外部调用级:进程恰好崩在中间怎么办?
这是最容易漏的一层。Activity 内部调用第三方时,进程可能在对方已处理、但结果尚未写入历史的瞬间崩溃,重放时这个 Activity 会再执行一次。这是 at-least-once 语义,源码文档也明确提醒:只有已完成的 Activity 结果才会被记忆,副作用必须自己做到幂等。
所以要靠 Activity.idempotencyKey:
Activity.make({
name: "charge",
success: Schema.String,
error: Schema.Union([PaymentDeclined, GatewayTimeout]),
execute: Effect.gen(function* () {
const key = yield* Activity.idempotencyKey("charge")
return yield* payments.charge(key, payload.cents)
}).pipe(Activity.retry({ times: 2, while: (e) => e._tag === "GatewayTimeout" }))
})它由执行 id、名称(可选再加尝试次数)算出确定的摘要,把这个摘要传给支付网关作为幂等键,让对方去重。
重试的时候,幂等键会变吗?
默认不变。Activity.idempotencyKey 默认不含尝试次数,所以 Activity.retry 重试时键保持一致。我让网关前两次返回 GatewayTimeout、第三次成功,charge 被调用了三次,最终成功,记录下来的不同键只有 1 个。这是我要的:重试不能让网关以为是三笔新交易。
如果你的对端要求每次重试都用新键,才加 includeAttempt: true。这个选项我没有单独跑过,只是源码里有这个参数。
重试本身的退避、条件怎么写,见Effect 怎么只重试特定的错误?。
版本是 effect 4.0.5,workflow API 在源码里仍标注为 unstable。重复提交被合并、重试沿用同一个幂等键,是我用内存引擎和假实现脚本跑出来的结果;at-least-once 的说明和 includeAttempt 参数来自源码,崩溃发生在「对方已处理、结果未写入历史」瞬间的重放,我没有真正构造出来过。