接口重试与幂等:一次超时到底引发了多少件小事

举报
早睡早起的Mikey 发表于 2026/11/05 21:47:03 2026/11/05
【摘要】 网络调用有第三种结局:超时,而此时下游可能已经执行成功。本文从一次超时引发的问题出发,整理幂等的三个层次(天然幂等、幂等键、状态机加版本号)、带抖动和预算的退避策略、三个常见陷阱,以及上线前的六条自检清单。

写服务间调用的时候,我们习惯把结果分成成功和失败两类。但真实的网络调用有三种结局:成功、明确失败、以及在约定的时限内没有回答。第三种最麻烦,因为你不知道对面到底做没做。

一、超时不是一种错误,是一种状态

举个例子。A 调 B 下单,客户端超时设了 500 毫秒,B 的数据库写入加风控校验实际用了 800 毫秒。对 A 来说这是一个 TimeoutError;对 B 来说订单已经落库了。此时 A 按照可重试异常的定义又发了一次请求,就有了两笔订单。如果把场景换成扣款,就是重复扣款。

所以第一步不是把超时时间调大,而是承认:超时之后,对面处于未知状态,任何自动重试都必须建立在重复执行无害的前提上。

二、幂等的三个层次

第一层是天然幂等。查询、设置类操作属于这层。注意语义区别:把库存设为 3 是幂等的,把库存加 1 不是。很多接口名字里写着 set,实现里却是累加,这是重复扣减的源头。

第二层是幂等键。由调用方生成一个请求标识,随请求一起发出去;服务端记住这个键和它的处理结果,同一个键再来就直接返回第一次的结果,不再执行。

def with_idempotency(store, key, payload, handler):
    if key in store:
        rec = store[key]
        if rec[payload_hash] != payload_hash_of(payload):
            raise Conflict(key)
        return rec[result]
    result = handler(payload)
    store[key] = {payload_hash: payload_hash_of(payload), result: result}
    return result

两个细节最容易漏:一是键必须和参数指纹一起校验,同一个键配不同参数要拒绝,不能默默返回旧结果;二是存储要有过期时间,否则这张表会无限长大,而过期太短又会让慢重试失去保护。

第三层是状态机加版本号。用一条条件更新语句把重复执行挡在数据库里:

update orders set status = PAID, version = version + 1
  where id = ? and status = UNPAID

受影响行数为 0,说明这单已经被处理过或者状态不对,调用方把它当成成功而不是异常。这层的代价是并发可见性最好,好处是不需要额外的存储。

三、退避不是重试次数那么简单

固定间隔重试会在下游刚恢复的时候再来一波洪峰。指数退避缓解了这一点,但如果所有客户端用同样的底数和同样的初始时刻,退避之后的时间点仍然会撞在一起。加一个随机抖动把它打散:

import random, time

def backoff(attempt, base=0.2, cap=8.0):
    raw = min(cap, base * (2 ** attempt))
    return random.uniform(0, raw)

还有一条经常被忽略的:重试要有预算。比较实用的做法是只允许一小部分请求携带重试,比如同一个接口在一分钟窗口内最多 10% 的流量是重试流量,超了就快速失败。这样下游故障时不会被上游的自救行为打死。

四、三个真实的坑

  • 超时值小于下游的 p99。看上去是偶发,实际是每百次请求稳定超出一两次,重试逻辑长期开着,故障时放大成雪崩。
  • 把非幂等接口包成幂等。在客户端记录我已经发过了并不能替代服务端幂等键,进程重启、多实例部署、并发副本都会绕过这层保护。
  • 重试边界和事务边界不一致。一次调用写了主表成功、写流水失败,重试时主表那步又被执行了一遍。跨多张表的写操作要么收敛到一个事务里,要么每一步都自带幂等键。

五、上线前自检清单

  • 这个接口重复执行一次,结果和执行一次是否完全一致?
  • 幂等键由谁生成,参数变了键变不变?
  • 幂等记录的生命周期多长,和重试的最大时间窗是否匹配?
  • 超时值是照抄默认配置,还是从下游的实际耗时分布算出来的?
  • 重试有没有次数上限、总时长上限和抖动?
  • 重试风暴时,下游有没有被保护住的手段(限流、熔断、重试预算)?

这几条里最值得先做的是第二和第三条。幂等键是调用方和被调方之间的一份约定,早定比晚定便宜;等真的出了一次重复扣款再去补,就要同时处理历史脏数据和线上兼容了。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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