为了防偶发错误加了重试,为什么订单和消息反而开始频繁重复

你可能遇到过这种场景:

订单接口偶尔超时,本来只想加个重试兜底,结果同一笔订单下了两三单
消息投递怕丢,加了多次重试,用户的通知和优惠券收到了两份
日志里看起来都是超时后重试成功,业务侧却一边忙着退款,一边手动去重

很多团队以为自己在提升可靠性,实际上是在把问题从偶发失败,变成高频重复。
问题不在重试这个动作,而在于没有区分能重试的东西,没有设计幂等边界,也没有按错误类型拆策略。

这篇文章解决三件事
一是搞清楚重试为什么会把单次错误放大成重复请求
二是订单和消息这种场景下幂等和去重应该怎么落地
三是重试策略该怎么按错误类型拆开,让系统既能救急又不制造新坑

================================

一、现象:错误没少,重复反而变多了

1、接口层看着正常,业务数据一团糟

常见表现包括:

  • 支付或下单接口偶尔超时,调用方启动重试,最后同一笔业务生成多条订单
  • 消息系统偶发网络抖动,多次重发同一条消息,用户手机连续跳相同通知
  • 后台统计时发现订单数和消息数偏高,和实际成交与实际推送记录对不上

技术侧只看到接口成功率提升
业务侧只看到重复扣款、重复发货、重复通知

2、日志只记成功,不记成功了几次

另一个隐性问题是观测维度太粗:

  • 监控只看接口成功率和平均延迟
  • 同一笔业务可能被执行多次,却没有任何指标反映
  • 重试一加,成功率确实变高了,重复率也跟着往上窜

久而久之,系统在监控数据上越来越好看,实际体验却越来越怪。

================================

二、根本原因:没有幂等边界的重试天然制造重复

1、把多次调用当成一次业务

多数重试逻辑默认了一个危险前提:
上一次调用没成功,所以这次补一次就好。

现实网络里,超时只能说明客户端没收到结果,并不能说明服务端没处理:

  • 第一次请求已经在服务端写入了订单
  • 客户端等不到返回,以为失败,开始第二次重试
  • 服务端再次处理,生成另一条合法订单,两条都看起来正常

没有业务侧幂等设计,底层根本分不清哪条是多余的。

2、缺少业务级唯一标识

不少系统只有技术层的请求编号,业务层却没有真正的一笔业务号:

  • 重试时重新生成请求标识
  • 底层把每次请求都当成全新的业务
  • 上游觉得是重试,下游只看到多条独立请求

没有业务级唯一键
任何重试其实都是在默许重复执行。

================================

三、设计思路:先让业务幂等,再谈怎么重试

1、给订单和消息加业务幂等键

对订单、支付、消息这种关键场景,至少要有一个稳定的业务幂等键:

  • 订单可以基于用户标识、业务场景和时间窗口生成业务请求号
  • 支付可以直接用业务订单号做幂等键
  • 消息可以用上游业务事件号或专门的消息键

实现上可以遵循这样一条主线:

  • 请求第一次到达时,基于幂等键在存储中插入一条记录
  • 插入成功说明是首个请求,正常执行业务并记录结果
  • 后续同一幂等键再次到达时,直接返回已有结果,不再重复执行逻辑

这样同一笔业务就算重试多次,底层只会处理一次
重试变成补拿结果,不再是多次执行。

2、消息消费端加一层去重表

对消息队列类场景,可以采用生产端尽力投递、消费端保证幂等的模式:

  • 为每条业务消息分配唯一业务号
  • 消费端收到后先查去重表
  • 若没有记录,执行业务处理,成功后写入去重表
  • 若已有记录,直接视为已处理成功返回

这样就算消息被重复投递,队列重试多次,甚至消费方自己重试,最终业务只会落地一次。

================================

四、重试策略:按错误类型拆开,而不是一键多次

1、网络错误可以重试,但要有限度

针对超时、连接失败、短暂不可达这类网络问题,可以允许重试,但建议:

  • 单次调用内限制重试次数,例如最多两次
  • 在重试之间加入递增延迟,避免瞬间放大压力
  • 总耗时超过上限时向上返回错误,交给人工或后续批处理

重试的目标是对抗短暂波动,而不是强行挤过一条已经拥塞的路。

2、业务错误不要重试,要改请求或改数据

诸如参数不合法、库存不足、状态不匹配、业务已关闭这一类错误,重试毫无意义,只会制造垃圾数据:

  • 接口层只需立即返回失败
  • 引导用户调整输入或等待状态变化
  • 不要让重试机制去自动再试一轮

把暂时性故障和最终性错误分开,重试只发生在前者。

3、监控要加上重复率和平均调用次数

判断重试策略是否健康,不能只看成功率,还要看:

  • 单笔业务平均被调用的次数
  • 幂等键命中重复记录的比例
  • 真实发生的重复扣款、重复通知、重复写入事件数量

一旦重复率明显上升,即便错误率下降,也说明重试策略在制造副作用,而不是改善可靠性。

================================

五、落地顺序:重试逻辑和出口稳定性一起改

1、三步改造你现在的重试

可以按这样的顺序动手:

  • 第一步,列出所有涉及钱和重要通知的链路
    给这些链路优先加业务幂等键和去重逻辑
  • 第二步,按错误类型拆分重试策略
    网络波动和服务暂时不可用可以有限重试
    明确的业务失败直接返回,不进入重试机制
  • 第三步,建立业务级观测
    不仅看接口成功率,还要看平均调用次数和幂等重复命中率

先把最关键的几条链路改到这个标准,再一点点推广到其他模块。

2、用更稳定的出口,让重试只对抗真正的抖动

有时候重试乱飞,是因为底层线路本身太不稳定:

  • 某些出口延迟抖得厉害,经常引发超时
  • 某批节点间歇性丢包,重试都撞在有问题的那几条线上
  • 服务端早已处理完,客户端却因为超时一次次重复请求

这时候,只改重试逻辑效果有限,还需要把底层出口做得足够可控,让重试只对付偶发问题。

易路代理在这一块能帮你承担一部分工程活:

  • 线路类型丰富,可以把对幂等极其敏感的链路放在更稳的住宅线路上,把大量批量请求和数据采集放在机房线上,物理上先隔离风险
  • 支持按业务建立线路组和标签,例如订单写入用一组出口,通知推送用一组出口,采集和报表用一组出口,应用只认标签不认具体地址,改线和扩容都在面板完成
  • 面板里有各线路组的延迟和成功率统计,可以看出到底是哪一组线路逼出了最多重试,方便你同时调参重试策略和出口分配

比较务实的做法是:

  • 先把最怕重复的一两条关键链路接到易路上稳定的出口组
  • 配合幂等和重试改造一起上线
  • 再通过易路面板的统计观察超时比例和重试次数变化,逐步调整参数

这样一来,重试不再是靠感觉乱撞,而是和幂等设计、出口质量一起组成一套有边界的可靠性机制,而不是隐形炸弹。