
超时告诉了我们什么
设想一个导出报表的接口:客户端发送创建请求,服务器已经生成任务并提交数据库事务,但返回途中连接断开。此时客户端知道自己没有收到响应,却不知道服务端是否完成了写入。把这一情况直接当成“创建失败”,随后生成一个新请求重试,就可能产生两份导出任务。
另一个场景是请求在建立连接前就失败,服务端根本没有看到它。两个场景在界面上都可能显示网络错误,但业务含义不同。因此,接口调用结果与业务执行结果应该分别记录,不宜只靠一个布尔变量描述整个过程。
给一次业务意图一个稳定标识
客户端第一次提交前生成请求标识,同一次业务意图的重试继续使用它。服务端将这个标识与调用主体、操作类型和业务参数关联,并建立唯一性约束。若同一个标识被用于不同参数,应该明确拒绝,而不是返回另一项操作的结果。
例如可以将 tenant_id、operation、request_key 作为约束范围,同时保存 payload_hash。哈希只用于比较参数是否一致,不能代替权限校验。同一个租户内也可能有不同用户权限,每次请求仍需验证调用方能否访问相关业务对象。
把记录和业务变更放进一致的边界
只在内存中记住“见过这个请求”是不够的,进程重启会丢失这条信息。也不能先写业务记录,再独立写去重记录,因为两步之间发生异常时,重试仍可能再次执行业务。对于单数据库场景,可以在同一事务中创建请求记录并执行业务写入,让唯一约束处理并发竞争。
涉及外部服务时,本地事务无法直接覆盖远端。可以通过持久化待发送记录、外部幂等标识与后台查询来缩小不确定范围。具体选择取决于对方是否支持幂等提交、是否提供可靠的结果查询,以及业务是否允许补偿。不能因为本地加了事务,就声称跨系统已经具备严格的一次执行语义。
结果查询需要匹配本次操作
查询时优先使用提交后返回的业务 ID。没有 ID 时,也应综合请求标识、创建时间、业务主体等条件,而不是只按标题或文件名匹配。历史上存在一条同名记录,只能证明以前做过类似操作,不能证明这次提交成功。
查询也可能存在延迟。合理做法是规定查询间隔和截止时间,在截止前保留证据与重试次数;查到明确结果就结束,超过边界仍无法确认则记录具体原因。无限轮询会掩盖故障,立刻重复提交又可能制造重复业务。
重试策略应针对失败位置
参数不合法、权限不足等明确拒绝,通常需要修正输入或权限后再操作。连接失败可以按受控策略重试;已经跨过提交边界但丢失回执时,应先查询或使用同一幂等标识重试。不要将所有异常都交给同一个“再试三次”循环。
日志建议记录请求标识、业务 ID、尝试序号、失败阶段与耗时,避免写入完整凭证。这样即使用户只报告“点了按钮没反应”,开发者也能够判断问题发生在请求准备、服务端接收还是结果回传阶段。
用故障注入检查边界
至少检查四类情况:请求到达前断网、服务端事务提交后断开响应、两个相同请求并发到达、客户端重启后再次查询。预期不只是页面最终显示成功,还应核对数据库中实际创建的记录数量、请求标识是否复用,以及旧响应能否覆盖新尝试的状态。
这些检查能够把重试设计从一段异常捕获代码,变成可解释、可追踪的业务协议。
本文由 AI 辅助整理,场景和字段为设计示例,配图为流程示意,不代表具体系统的实测性能。