超时只能说明客户端没看到结果
代理调用创建工单、发送消息或扣费工具时,网络超时可能发生在服务端提交之前,也可能发生在提交之后、响应到达之前。这两种情况在客户端都是同一个超时异常,外部事实却完全相反。因此状态机需要一个“结果未知”分支,它不等于可重试失败,更不能让模型在下一轮换一个参数后重新创建。
工具契约应声明动作是纯读取、天然幂等写入、通过操作键幂等的写入,还是无法安全重放的外部动作。只有第一类可以在临时网络故障后直接再请求。其他类型都要携带可查询的稳定身份,或在结果未知时转交人工对账。客户端重试库不知道业务副作用,所以不应对所有方法统一开启自动重试。
设计时先画出提交边界:本地幂等记录何时写入,外部系统何时受理,返回的资源标识在哪一步持久化。对每个边界分别注入进程终止、连接中断和响应丢失,观察恢复程序是查状态、安全接管,还是产生了第二个外部资源。如果团队无法指出提交瞬间,就无法对超时语义做可靠承诺。
操作键应从业务意图稳定派生
每次重试都生成新随机键,只是给重复副作用加了一个新标识。操作键应在任务首次决定某个业务意图时创建,并跨模型轮次、工作进程重启和网络重试持久化。它的作用域至少包含调用主体、工具或端点与任务意图,避免不同用户的同名键互相碰撞。键本身只需是高熵不透明值,业务不应从它解析价格或资源类型。
同一任务可能包含多个独立外部动作,例如先创建工单、再上传附件、最后发通知。这三步不能共用一个只记录“任务完成”的粗粒度键,否则无法识别哪一步可恢复。为每个副作用节点生成稳定子键,将依赖的上游资源标识写入请求指纹。计划修改导致业务意图真正改变时应创建新键,而不是在旧键下安静替换参数。
操作键需要长度和字符集上限,并在入库前经过规范化。无限长客户端键会成为存储和索引攻击面,把敏感信息编进键又会令它出现在日志与监控维度中。服务端可以对外部键再做定长哈希作为索引值,同时保留严格作用域。日志仅记录截断指纹与内部操作标识,不回显用户提供的完整键。
请求指纹用来拒绝同键异义
幂等不是“同键就返回旧结果”这么简单。首次受理时,服务端对经规范化的业务请求计算指纹,与操作键一起原子写入。后续相同键只有在指纹相同时才能查询或重放结果,如果金额、收件人、目标资源或其他业务字段改变,应返回明确冲突。这一冲突不能通过覆盖旧指纹解决,否则客户端会失去判断哪个意图已执行的依据。
指纹应包含影响业务结果的参数,排除追踪标识、请求时间和可每次变化的传输头。对货币数值、时间和对象键顺序使用唯一规范化函数,并为函数设版本,防止升级后同一业务请求产生新指纹。鉴权凭据不进入指纹,但经鉴权得到的调用主体必须进入操作键作用域,否则一个用户可能重放另一个用户的记录。
指纹冲突响应不应返回先前请求的完整正文,因为它可能包含当前调用者不再有权读取的数据。可返回操作已被占用、当前状态和安全的结果资源引用,再由正常资源接口重新鉴权。内部调查可以对比两份指纹的字段级差异类型,但日志中只保留“金额冲突”等分类,避免记录具体敏感值。
数据库唯一约束承担并发裁决
两个工作进程可能同时查询“没有这个键”,因此先查后插的应用逻辑不能保证单次执行。在调用主体、端点与幂等键上建数据库唯一约束,首个插入成功的请求成为执行者,其他请求在唯一冲突后重读记录并校验指纹。这个裁决必须在发生外部副作用前完成,否则两个进程仍可能先发送后争抢记录。
幂等记录可以包含已接收、处理中、已完成、可确定失败和结果未知状态。状态转换用版本或预期旧状态作条件,让迟到工作进程不能覆盖已经恢复的结果。处理中记录带有租约截止时间和工作者标识,但租约过期只意味着可以开始恢复调查,并不自动证明外部动作没有成功。
如果业务变更和幂等记录在同一数据库,可以在一个短交易中原子创建资源、保存结果引用并将操作设为完成。若副作用位于外部服务,本地交易无法包裹网络世界,就必须依赖对方的幂等键或状态查询。不要在持有长交易锁时等待外部响应,这会把网络延迟传导为数据库阻塞。
恢复处理中记录前先查外部证据
租约过期后,接管者首先根据原操作键或业务唯一属性查询目标系统。如果已找到资源,就验证它的请求指纹或关键字段,将本地记录收敛为完成;如果对方明确返回未受理,才能以同一键重试。查询本身超时时继续保持未知,不得因为恢复程序也遇到网络问题就假设原请求失败。
有些外部接口没有幂等支持,也无法按客户端标识查询。对付款、对外发送和不可逆权限变更这类高损失动作,应在结果未知时暂停自动化,展示请求时间、目标摘要和对账指引给操作人。可以实现本地业务唯一约束来减少风险,但不能宣称这会让一个不透明的外部副作用变成恰好一次。
恢复程序要有并发上限和稳定排序,避免故障恢复时大量悬挂操作同时冲击外部服务。对每条记录保存查询次数、最近结果、下次检查时间和人工接管状态,并使用原子租约防止多个恢复者同时对账。当人工已开始处理时,自动工作者必须停止外部请求,只允许更新可见状态。
结果重放要重新执行当前授权
幂等记录保存的响应可能包含已经不再适合当前用户的敏感数据。重放时先确认操作键的调用主体与当前身份一致,再对结果指向的资源执行现时授权。如果用户权限已被收回,服务端仍可以回答“此操作曾完成”,但不能把旧资源正文一并返回。对过大响应保存资源标识、状态码和必要响应头,由正常接口获取最新表示。
不是所有错误都应永久缓存。请求指纹冲突、已确定的业务拒绝和已完成结果通常可重放;短暂的上游不可用不应占用一个操作键到业务重试窗口结束。但删除失败记录前必须确认没有产生外部副作用,否则清理会把未知结果重新变成可执行请求。保留期应覆盖客户端可能的最长重试周期和对账周期。
响应重放要保持原操作的业务结果,但可以使用当前的安全头、追踪标识和格式封装。不要原样保存与重放整个传输响应,其中可能有一次性凭据、旧的跨域头或内部调试信息。将业务结果与传输层分开持久化,为记录设定结构版本,当接口响应格式升级时可以明确地兼容或拒绝旧结果,而不是将旧字节冒充新契约。
补偿不是幂等的替代品
某些动作需要在后续步骤失败时补偿,例如删除已上传但未关联的附件。补偿操作自身也是外部副作用,需要独立稳定键、请求指纹、状态查询和重试策略。不能在原动作超时时立即发起补偿,因为原动作可能已成功而结果尚未确认,这会创造“创建成功后又被删除”的新竞态。
补偿前先收敛原动作的终态,再根据当前业务状态判断是否仍需要撤销。如果外部资源已被合法用户后续修改,原任务的自动补偿就可能破坏新状态,此时应转为冲突并请求人工决定。补偿记录要引用原操作标识,但保留自己的版本和审计链,以便区分原业务失败与恢复失败。
对不可完全逆转的动作,界面和工具契约不应声称存在“回滚”。发送邮件后可以再发澄清,但无法撤回已被阅读的内容;订单退款是一笔新财务事件,不是删除旧事件。把这些后续行为建模为明确的纠正流程,使用自身审批和幂等规则,才能避免代理因为一次错误而自动创造第二次高影响动作。
幂等验证要穿过所有故障时点
核心并发测试使用数十个客户端同时提交相同主体、端点、操作键和指纹,断言只有一个执行者进入外部调用,所有客户端最终收敛到同一资源标识。再使用同键与改动过的金额、收件人或正文并发请求,确认全部稳定返回冲突且不更改原记录。还要测试不同主体使用同文本键,保证作用域既隔离又不会跨用户泄露结果。
故障注入覆盖幂等记录插入后、外部服务受理前、受理后回应前、本地结果持久化前后以及客户端收到响应前的进程终止。每个时点都要从空白工作进程恢复,检查外部副作用数量始终为一。对无法查状态的接口,断言流程进入人工对账,而不是试图通过反复调用让测试偶然通过。
运维矩阵则验证悬挂租约批量恢复、数据库重启、外部限流、时钟偏差和记录清理边界。指标至少区分首次执行、结果重放、指纹冲突、未知结果、人工接管和补偿失败,并按工具类型观察。测试还要证明日志中没有请求正文、鉴权头和完整幂等键,让可追溯性不会将操作记录变成新的敏感数据库。
实现片段
await operations.createOrReturn(operationKey, requestHash)
评论 · 0