第 9 / 23 篇

第 8 章:让工具记住同一项业务效果

幂等常被缩成一句“重复调用没关系”。对于长任务,这句话需要展开:重复的身份怎样确定,参数变化怎么办,去重记录保存多久,两个请求同时到达怎样处理,以及响应丢失后怎样取得原结果。

8.1 去重对象是业务效果

示例工具接收稳定操作键与已经批准的计划。第一次成功调用在工具账本中保存键、计划指纹、账户、数量和回执身份。后续相同键与相同指纹的调用返回已有结果,不再次增加效果。

这里没有要求 HTTP 请求只发生一次。日志里可以出现两个 POST,甚至出现网络失败;我们检查的是最终账本中同一操作键对应的业务效果是否唯一。把“只看到一次请求”作为唯一验收,无法覆盖响应丢失之后必须重发的路径。

如果使用相同键却改变数量,工具应明确拒绝参数冲突。返回旧回执以后还需要调用方核对它指向原意图。忽略这个核对,会让第二个调用方把旧效果误认为自己新参数的成功。

8.2 唯一键与效果在一个工具事务内

去重表如果先写“正在处理”,之后在另一个事务更新账户,进程可能在中间退出,留下既不能安全跳过、也不能直接重做的状态。去重记录和它保护的本地业务效果,需要共享清楚的原子边界。

教学工具用一张持久账本表达积分效果,依靠工具事务和唯一约束保证同键竞争只产生一项记录。工作流数据库连接不参与这个事务。工具事务提交后,效果已经成立,即使接下来响应没有到达 worker。

现实中若需要更新账户余额并写去重记录,应把两者放在同一本地事务里,或使用接收方本来提供的原子业务接口。若还要调用银行、邮件或另一个数据库,又增加了一个外部副作用边界,不能继续沿用单表实验的结论。

8.3 并发重复调用需要数据库裁决

两个调用同时携带同一个操作键,仅在应用里先查是否存在会产生竞争。两者都可能查不到,然后各自执行效果。示例用数据库唯一约束裁决插入,竞争者在约束结果确定后读取原有记录,并再次比较指纹。

这项保证依赖同一个权威账本。多个实例如果各自在本地内存或各自数据库去重,就可能分别认定自己第一次看到该键。读缓存可以加速查询,但最终副作用准入仍需要符合实际部署的一致性合同。

保留时间也会改变结论。工具若删除了去重记录,却保留了已经发生的业务效果,旧任务恢复时可能重新创建效果。工作流允许恢复多久,至少要与键、指纹和结果查询合同一起评审。单独配置一个重试次数,不能解决生命周期不匹配。

8.4 查询回执也是一项受保护的业务能力

结果查询需要返回稳定操作身份、计划指纹和结果身份。worker 校验这些字段与原计划一致,再决定是否保存成功。返回一个没有关联身份的 ok=true,不足以证明查询到了本次操作。

查询也必须针对权威事实。刚写入主实例就去延迟副本查询,可能暂时得到不存在;缓存负结果同样可能过时。对支持稳定键的幂等工具,按原键重放仍可能安全,但对不支持去重的工具,这种误判就可能直接造成重复业务。

随书查询使用工具账本的同一 PostgreSQL 实例,不验证只读副本延迟。读者接入真实服务时,需要询问结果查询的可见性、查询窗口、授权和错误语义,不能把任何 HTTP 404 都直接理解成“确定没有发生”。

8.5 能力缺失时缩小自动化责任

没有幂等和权威查询的工具,在响应丢失后可能无法自动裁决。本书让这类任务保留 unknown,并要求人工核查。这个结果不是执行引擎忘记了重试,而是它没有足够证据批准新的副作用。

可以通过改造工具合同增加自动化能力,例如加入调用方操作键、提供可查询回执,或者让业务服务创建一个先申请后确认的操作对象。不能仅仅在代理层缓存一次成功响应,就声称解决了工具已经提交但响应尚未进入缓存的窗口。

教学工具使用公开开发令牌和环回 HTTP,目的是隔离并验证执行语义,没有提供生产身份委托或支付安全。让业务效果通过认证授权边界,是接收方必须独立承担的责任;幂等键不能充当访问凭据。

练习。 一个第三方工具支持幂等键,但只保留一小时记录;你的审批最长等待一天,unknown 可能两天后才核查。请写出恢复规则,指出哪些时间窗口允许自动重放,并提出两种避免键过期后重复效果的合同调整。