别在超时后盲重试:用 Go 写一个可恢复的非幂等 POST 账本

举报
yd_299903619 发表于 2026/10/27 10:40:42 2026/10/27
【摘要】 用 Go 标准库实现一个可恢复的非幂等 POST 任务账本:先持久化发送意图,超时后进入结果不明状态,恢复时查询远端而不盲目重发,并用三个测试验证单次写入、延迟可见和重复候选场景。

后台任务服务最危险的时刻,往往不是接口明确返回失败,而是请求已经发出、进程却不知道远端是否写入成功。特别是第三方 POST 接口没有幂等键、也不能与本地数据库组成事务时,“超时后重试”可能创建两份记录,“永不重试”又可能漏掉真实失败。

我用 Go 标准库写了一个可运行的最小账本原型,重点不是包装 HTTP 请求,而是把不确定性显式持久化。

一、状态机先于重试逻辑

原型使用六个状态:

PENDING -> SENDING -> SUCCEEDED
                    -> SENT_UNKNOWN -> SUCCEEDED
                                    -> MANUAL_REVIEW
  • PENDING:任务已入库,尚未发送。
  • SENDING:当前 worker 已取得执行权。
  • SENT_UNKNOWN:请求可能已到达远端,但没有可靠响应。
  • SUCCEEDED:已确认远端记录及 ID。
  • MANUAL_REVIEW:查询到多个候选或证据矛盾。

必须强调:网络超时不是业务失败。因此代码不会把超时直接映射成 FAILED,而是进入 SENT_UNKNOWN,恢复时先查询远端。

二、发送前先记账

账本在发送前完成两次持久化:先创建 PENDING,再转为 SENDING。写入采用临时文件加重命名,并以 0600 权限保存,避免进程崩溃后留下半写 JSON。

每次尝试都记录:

  • task_id:本地任务唯一标识;
  • fingerprint:请求业务指纹;
  • 状态、尝试次数、远端 ID;
  • 最后错误与更新时间。

这些字段让恢复进程知道“请求是否可能已经发出”,而不是仅凭 HTTP 客户端返回 context deadline exceeded 就再次发送。

三、恢复只查询,不盲目重发

核心恢复逻辑如下:

for _, entry := range ledger.UnknownEntries() {
    matches, _ := remote.FindByFingerprint(ctx, entry.Fingerprint)
    switch len(matches) {
    case 1:
        ledger.MarkSucceeded(entry.TaskID, matches[0].ID)
    case 0:
        // 不把“暂时查不到”当成可安全重试的证明
        continue
    default:
        ledger.MarkManual(entry.TaskID, "multiple matches")
    }
}

这里故意保留了一个保守分支:远端暂时查不到时,任务继续停留在 SENT_UNKNOWN。因为第三方查询接口可能存在最终一致性延迟,零结果不能自动证明写入没有发生。生产系统应在此基础上设置观察窗口、分页核对规则和人工复核阈值。

四、实际运行结果

代码只用 Go 标准库,go test -v ./... 验证了三个关键场景:

TestRecoverWithoutResending            PASS
TestUnknownWithoutRemoteMatchStaysUnknown PASS
TestMultipleRemoteMatchesRequireManualReview PASS

演示程序模拟“远端已经提交记录,但客户端收到结果不明错误”:

status=SUCCEEDED remote_id=remote-demo-1 create_calls=1 remote_records=1

结果说明:重启后通过账本恢复状态并查询远端,Create 只调用一次,远端只有一条记录,没有因为恢复而重复创建。

五、这套方案能保证什么

本地账本能保证:

  1. 每次发送意图有持久化记录;
  2. 恢复时先核对而不是盲目重试;
  3. 多条候选会进入人工复核;
  4. 进程重启后仍保留“结果不明”这一事实。

但如果第三方没有幂等键,也没有可靠的唯一业务查询能力,本地账本不能单方面保证远端恰好写入一次。系统只能在以下边界中选择:

  • 为安全起见不自动重发未知请求;
  • 接受可能重复,并在查询到多条记录时进入人工处理;
  • 推动第三方补充幂等键或唯一业务标识。

真正可靠的方案,不是给重试加一个计数器,而是把未知结果当成一等状态:先持久化意图,再发送;恢复时先查询,证据不足就停下来。这样即使无法在所有外部条件下实现“恰好一次”,也能避免系统在不知情的情况下继续制造重复数据。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。