队列重试策略:失败了别立刻原地蹦迪
消息队列失败重试很常见,但重试策略写不好,会把故障放大。下游已经超时,你还每秒重试一百次;第三方限流了,你还热情冲锋;消息本身参数错了,你重试到天荒地老。失败了别立刻原地蹦迪,先判断它值不值得重试。
我见过一次"重试级事故":某个订单同步服务因为数据库运维短暂不可用,队列里积压了大约 2000 条失败消息。消费者设置的重试策略是"失败立即重试,最多 100 次",每次重试间隔只有 100ms。这 2000 条消息同时启动重试,每秒产生了近两万次数据库连接请求。数据库刚恢复上线就被再次打挂,而这次不是运维的问题,是我们自己的重试策略。
队列重试要分错误类型、设置退避、限制次数、保留死信和告警。它不是简单 for retry++。
一、先区分可重试和不可重试
网络超时、下游 5xx、临时限流通常可重试;参数错误、权限错误、数据不存在通常不可重试。
flowchart TD
A[任务失败] –> B{错误类型判断}
B –>|临时错误| C[退避重试]
B –>|永久错误| D[直接进入死信]
B –>|限流错误| E[延长退避重试]
C –> F{超过最大次数?}
F –>|是| G[进入死信 + 告警]
F –>|否| C
不可重试错误继续重试,只是在浪费资源,还会污染日志。我习惯的把永久错误直接丢死信,不消耗重试次数——因为重试一万次也不会成功。
区分可重试和不可重试有一个实用技巧:看错误是否与"时间"相关。网络超时——时间相关的临时问题,可重试。下游返回"参数缺少必填字段"——与时间无关的代码问题,不可重试。下游返回 429 Too Many Requests——时间相关的限流,可重试但要加长退避。
二、指数退避加随机抖动
import (
"math"
"math/rand/v2"
"time"
)
func NextDelay(retry int) time.Duration {
// 指数退避:1s, 2s, 4s, 8s, 16s, 32s, 上限 64s
base := time.Second * time.Duration(math.Pow(2, float64(min(retry, 6))))
// 随机抖动:±50%,防止"惊群"同时重试
jitter := time.Duration(float64(base) * (0.5 + rand.Float64()*0.5))
return base + jitter – time.Duration(float64(base)*0.5)
}
没有抖动,大量失败任务会在同一时间重试,形成新的尖峰。退避是礼貌,抖动是排队秩序。抖动范围我一般选 ±50%——太小没效果,太大退避形同虚设。
三、重试次数要有限
retry_policy:
max_attempts: 6
initial_delay: 1s
max_delay: 5m
dead_letter_after: 6
retry_backoff: exponential
jitter: 50%
超过次数进入死信队列。死信不是垃圾桶,而是待处理问题池。要记录任务 payload、错误原因、最后失败时间。死信队列要有专门的看板,每周复盘死信堆积情况——如果某个死信越来越多,说明它对应的业务代码可能有系统性问题。
四、重试要有业务幂等
队列消息可能重复处理。消费者必须幂等。比如发券、扣库存、生成文件,都要有业务唯一键。重试策略和幂等设计是绑在一起的,不要只做一半。
另外,重试量要计入容量规划。故障期间正常流量加重试流量,可能把系统推倒。必要时暂停低优任务。
还要防止同一任务被多个消费者同时处理。队列本身可能保证一段时间不可见,但 worker 超时或崩溃后消息会再次出现。消费者必须拿业务锁或幂等键确认自己是否还能处理。
consumer_guard:
idempotency_key: task_id
visibility_timeout: 60s # 队列的消息不可见时间
handler_timeout: 45s # 业务处理超时,必须小于 visibility_timeout
heartbeat_interval: 15s # 处理中每 15 秒续期
处理超时时间要小于可见性超时,给系统留出确认和提交结果的余量。heartbeat 机制是防止长时间任务被队列误判为失败——worker 定期告诉队列"我还在处理,别把消息再分给别人"。
队列还要区分优先级。用户在线等待的任务、后台批处理任务、补偿任务,不应该都排在同一条队列里。下游故障时,先保在线链路,低优任务延后执行。
queues:
realtime:
max_retry: 2
initial_delay: 500ms
priority: high
batch:
max_retry: 6
initial_delay: 10s
priority: low
compensation:
max_retry: 10
priority: lowest
active_hours: "01:00-06:00" # 低峰期执行
重试策略如果不分优先级,故障时大家一起挤门,谁也出不去。
最后,重试要有全局开关。下游明确故障时,可以暂停某类任务重试,只保留新任务入队或直接降级。这个开关要有权限和审计,不能谁手滑一下把核心队列停了。
retry_switch:
queue: batch
action: pause_retry
reason: downstream_maintenance
operator: oncall
expires_at: "2026-07-03T06:00:00Z"
有开关、有审计、有恢复时间,故障处理才不会靠临场手速。
五、总结
队列重试策略要先区分错误类型,再用退避和抖动控制节奏,限制最大次数,进入死信,并保证消费者幂等。失败不可怕,瞎重试才可怕。系统恢复要靠秩序,不靠热情。
网硕互联帮助中心



评论前必须登录!
注册