幂等、重试、失败补偿
如果说前几篇解决的是“任务怎么跑起来”,那这一篇解决的是“任务跑坏了怎么办”。
长链路系统一定会遇到这些现实问题:
- 用户重复点击提交按钮
- 消息被重复投递
- Worker 执行到一半崩了
- 外部服务暂时超时
- 某一步已经产生副作用,后面步骤却失败了
所以一个能上线的任务系统,必须认真处理三件事:
- 幂等:重复来一次,不能把结果搞乱
- 重试:临时失败,要有机会自动恢复
- 失败补偿:已经做出去的副作用,要能收回来或修正掉
先说幂等:为什么它是基础
幂等的直白定义是:
同一个请求或同一条消息被重复执行多次,最终结果应该和执行一次一致。
这件事在长链路里尤其重要,因为重复非常常见:
- 用户刷新页面又点了一次提交
- 网关重试了 HTTP 请求
- 消息队列把同一条消息投递了两次
- Worker 超时后,任务又被重新拉起
如果没有幂等,结果可能是:
- 建了两条任务
- 扣了两次余额
- 发了两次通知
- 写了两份重复数据
幂等通常要做在哪几层
1. 提交层幂等
比如前端提交任务时带一个 idempotencyKey:
POST /tasks
Idempotency-Key: upload_abc_001
后端如果发现这个 key 已经创建过任务,就直接返回原来的 taskId。
2. 消费层幂等
Worker 消费消息时,即使拿到重复消息,也要先检查:
- 这个任务是不是已经成功了
- 这个步骤是不是已经完成了
- 这个副作用是不是已经落过库了
3. 外部副作用幂等
如果你要调扣费、发券、发送通知、创建第三方任务,也要尽量带业务唯一键,避免重复执行。
一个简单案例:AI 报告生成并扣减额度
假设用户点一次“生成报告”,流程是:
- 创建任务
- 扣减 1 次额度
- 调模型生成报告
- 保存结果
- 通知用户
如果消息重复消费了两次,而系统没有幂等,就可能:
- 扣两次额度
- 生成两份报告
- 发两次通知
所以你至少要保证:
- 同一个
taskId只会扣一次额度 - 同一个
taskId成功后再次执行直接跳过
再说重试:不是所有失败都值得重试
重试的作用是处理暂时性错误,比如:
- 网络抖动
- 外部服务超时
- 短暂的资源不足
- 下游服务瞬时 5xx
但有些错误重试也没用,比如:
- 参数非法
- 文件格式不支持
- 业务校验失败
- 权限不足
可以简单分成两类:
| 错误类型 | 是否重试 |
|---|---|
| 网络超时、临时 5xx | 可以重试 |
| 参数错误、业务拒绝 | 不要重试 |
重试应该怎么做
工程上比较常见的策略:
- 限制最大重试次数
- 使用指数退避
- 加一点随机抖动,避免同一时间雪崩重试
例如:
- 第 1 次失败,5 秒后重试
- 第 2 次失败,30 秒后重试
- 第 3 次失败,2 分钟后重试
- 超过次数后进入死信或人工处理
可以用状态机理解:
stateDiagram-v2
[*] --> QUEUED
QUEUED --> RUNNING
RUNNING --> SUCCESS
RUNNING --> FAILED
FAILED --> RETRYING : 可重试错误
RETRYING --> QUEUED
FAILED --> COMPENSATING : 不可恢复或超限
COMPENSATING --> COMPENSATED
COMPENSATED --> [*]
SUCCESS --> [*]
这里的重点是:
FAILED不等于立刻结束- 有些失败会进入
RETRYING - 有些失败要进入
COMPENSATING
失败补偿是什么
失败补偿不是“数据库事务回滚”的放大版。
在长链路里,很多副作用已经发出去了,比如:
- 已经扣了额度
- 已经创建了第三方任务
- 已经写了部分中间结果
这时做不到真正原子回滚,只能做补偿动作。
例如:
- 扣过额度后失败了,就退回额度
- 创建过第三方任务后失败了,就调用取消接口
- 写过临时文件后失败了,就清理临时文件
所以补偿的本质是:
用一个反向业务动作,把系统尽量拉回一致状态。
一个补偿案例
还是用“AI 报告生成”举例。
假设流程是:
- 创建任务
- 扣减额度
- 调模型生成
- 上传结果文件
如果执行到第 3 步时模型服务长时间失败,且超过最大重试次数,那么系统可以这样补偿:
- 标记任务失败
- 退回之前扣掉的额度
- 清理临时文件
- 给用户一个明确失败原因
这就比“任务失败了,但额度也没了”要合理得多。
幂等、重试、补偿之间的关系
这三者不是孤立的,而是一条链。
flowchart LR
A[收到请求或消息] --> B[先做幂等检查]
B --> C[执行任务]
C --> D{是否成功}
D -- 是 --> E[结束]
D -- 否 --> F{是否可重试}
F -- 是 --> G[按策略重试]
F -- 否 --> H[执行补偿]
G --> C
H --> I[失败结束]
这张图想表达的是:
- 没有幂等,重试越多越容易出错
- 没有重试,暂时失败就会直接放大成业务失败
- 没有补偿,失败后的副作用会长期残留
初学者最容易踩的坑
1. 看到失败就一律重试
这样会把永久错误变成无限资源浪费。
2. 只做接口幂等,不做消费幂等
消息重复投递时照样会出问题。
3. 认为补偿一定能完全回到原点
很多场景只能“尽量恢复一致”,而不是绝对回滚。
4. 重试没有上限
最后死循环占满系统资源。
这一篇要记住的核心点
- 幂等是长链路系统的前提,不是优化项
- 重试只针对暂时性错误,且要有次数、退避和终点
- 失败补偿是对已经发生的副作用做反向修正,不是魔法回滚
最后一篇再把视角拉高一点:当系统真的跑起来之后,如何通过日志、指标、链路追踪来观察它。