【背景】
我带过一个 40 人的后端团队,负责支付回调 + 订单状态机链路。去年一次故障复盘,我算了一笔账:
- 自动重试配置:3 次,指数退避 1s / 2s / 4s,第 3 次失败进 DLQ(死信队列);
- 那次故障的根因是下游风控服务发布后一个字段名改错了(
risk_level→riskScore),重试 3 次全部 400,DLQ 在 15 分钟内积压了 11,000 条; - 业务方从第 5 分钟开始打电话,SRE 从第 12 分钟开始查日志,第 28 分钟定位到字段名变更,第 41 分钟回滚 + 手动重放 DLQ;
- 全程用户侧"支付成功但订单状态没更新"的投诉:327 条。
复盘时我提了一个问题:如果重试次数从 3 次改成 8 次,这 15 分钟的 DLQ 积压还能不能避免? 答案是:不能。因为根因不是"瞬时抖动",而是"对端状态已经变更",多重试 5 次只会把 400 从 3 次变成 8 次,DLQ 照样炸,只是炸得更慢。
这件事让我开始认真想:重试是"技术视角"的自愈手段,补偿是"业务视角"的兜底手段,两者的边界到底该由谁、依据什么来划?
【常见分歧:三种做法,三种哲学】
派系 A ——「重试给够,DLQ 只是极端 case」
- 重试 5~8 次,指数退避 base=2s(2s / 4s / 8s / 16s / 32s / 64s),总窗口约 3 分钟;
- 理由:绝大多数瞬时故障(GC、网络抖动、连接池耗尽)在 30s 内自愈,多给几次重试是"免费"的;
- DLQ 只留给"真失败",人工处理是最后手段;
- 风险:如果根因是"对端状态已变更"(字段改名、权限回收、对端已回滚),多重试 5 次 = 多 5 次无意义调用 + 延迟发现根因。
派系 B ——「3 次就该停,第 4 次大概率是"真失败"」
- 重试 3 次(1s / 2s / 4s),第 3 次失败立刻进 DLQ + 触发告警;
- 理由:瞬时故障的自愈窗口通常在 5~10s 以内,超过 3 次(约 7s)还没恢复,大概率不是"瞬时"问题;
- 继续重试是"用 CPU 和带宽掩盖问题",而且每次重试都可能产生副作用(非幂等接口);
- 风险:如果下游真的只是慢(比如批处理任务占满 CPU,10s 后恢复),3 次重试全部失败,本可以自愈的 case 进了 DLQ。
派系 C ——「重试和补偿并行,不是串行」
- 重试在跑的同时,补偿逻辑(回滚、对账、通知)已经开始准备;
- 理由:重试是"乐观"的,补偿是"悲观"的,两者不互斥;
- 风险:如果重试最终成功了,补偿逻辑已经执行了一半,回滚补偿本身又引入新的不一致——"补偿的补偿"问题。
【我的倾向:3 次 + 第 3 次失败后 200ms 内启动"补偿评估",但不是立刻执行补偿】
具体设计(我们线上跑了 2 年的方案):
- 重试策略:3 次,指数退避 1s / 2s / 4s,总窗口约 7s;
- 第 3 次失败时的动作(不是"进 DLQ 就完了"):
- 立刻写一条
failure_event到内部事件总线(不是 DLQ,DLQ 是最后手段); failure_event携带:原始请求快照、3 次重试的时间戳和响应码、调用链 traceId;- 触发一个补偿评估器(一个轻量异步任务),在 200ms 内完成以下判断:
- 错误码是否命中"可重试白名单"(如 503、504、连接超时)→ 如果是,额外给 1 次"宽限重试"(间隔 30s),总计 4 次;
- 错误码是否命中"不可重试黑名单"(如 400、401、403、422)→ 直接进 DLQ + 告警;
- 其他错误码 → 进 DLQ,但标记为"待人工评估",告警级别降一级;
- 立刻写一条
- DLQ 消费:不是"等人工去捞",而是有一个定时任务每 5 分钟扫一次 DLQ,自动重放标记为"宽限重试"的条目;真正需要人工的(黑名单命中)才推工单。
为什么是 3 次而不是 5 次或 8 次? 核心数字:我们统计了过去 12 个月的 2.3 万次重试事件,其中:
- 第 1 次重试就成功的:71.2%;
- 第 2 次重试才成功的:18.4%;
- 第 3 次重试才成功的:6.1%;
- 3 次都失败的:4.3%(其中 82% 是"对端状态已变更"类,继续重试无意义)。
也就是说,3 次重试已经覆盖了 95.7% 的"瞬时故障"场景,第 4 次及以后的重试,ROI 急剧下降。
【想听大家聊的:】
-
你们的 DLQ 平均积压时间是多少? 从"进 DLQ"到"人工处理完",中位数和 P95 分别是多少?如果 P95 > 30 分钟,你的补偿策略是不是该提前介入?
-
补偿逻辑的触发条件,你们卡在哪几个参数上? 是重试次数、时间窗口、错误码白名单、还是业务 key 的"未确认消息量"?有没有"重试成功但数据已脏"的 case(比如第 2 次重试成功,但第 1 次重试已经在下游写了一半)?
-
派系 C(重试和补偿并行)你们试过吗? 如果试过,"补偿的补偿"问题怎么解的?有没有用"补偿幂等键"来兜底?
-
一个更尖锐的问题:你的团队里,"重试次数"这个参数是谁定的?是写代码的开发者、SRE、还是业务方?如果三个人定出来的数字不一样,最终以谁为准?
这个话题没有标准答案,但一定有"踩坑后的共识"。欢迎把你们的数字和 case 贴出来,尤其是那些"重试成功了但数据不对"的——这种 case 最值钱。