重试不雪崩的关键是指数退避+错误分类+最大次数限制:首次等待100ms,后续翻倍(200ms、400ms),最多3次,超限进死信或告警;HTTP调用需区分可重试错误(如net.ErrClosed、5xx)与不可重试错误(如400、403)。

任务失败后怎么重试才不雪崩
重试不是简单地循环调用,关键在控制节奏和边界。Go里常见错误是直接 for + time.Sleep 硬等,导致瞬时并发飙升、下游被打穿。
必须用指数退避(exponential backoff),且限制总重试次数。比如第一次等 100ms,第二次 200ms,第三次 400ms,最多 3 次。超过阈值就进死信队列或告警,别卡在线程里。
-
asynq默认提供MaxRetry和RetryDelay配置,底层自动做退避 - 自己手写时别用
time.Sleep(1 * time.Second)这种固定值,改用time.Sleep(delay); delay *= 2 - HTTP 调用重试要额外判断错误类型:
net.ErrClosed、context.DeadlineExceeded可重试;http.StatusForbidden或http.StatusBadRequest就不该重试
多个服务实例怎么避免重复消费同一任务
分布式环境下,Redis 的 BRPOP 或 XREAD 本身不保证“恰好一次”,靠的是消费端加锁 + ACK 机制。
典型做法:任务被取出来后,先用 SET task:123 lock EX 30 NX 占位,执行完再 DEL。如果 worker 崩溃,锁自动过期,其他实例能捡起。
立即学习“go语言免费学习笔记(深入)”;
-
asynq和beeclaw都内置了基于 Redis 的租约(lease)机制,超时自动释放 - 别依赖消息队列的“at-least-once”语义就不管幂等——必须在业务层校验:
if taskProcessed(task.ID) { return } - 用数据库唯一索引或 Redis
SETNX做去重,比纯内存 map 更可靠
任务状态怎么查、怎么追踪
用户问“我的导出任务好了没”,你不能回答“正在跑”,得返回可验证的状态。纯 channel 实现的任务队列没有状态存储,一重启就丢,生产环境不可行。
状态至少得落盘:Redis 存 task:123 的 JSON,字段包括 status、started_at、result、error;或者用 PostgreSQL 的 jsonb 字段存。
-
asynqmon提供 Web UI 查队列长度、失败率、单个任务详情,路径是/asynq - 自己实现时,HTTP 接口如
GET /api/v1/task/{id}应返回完整状态,不要只返回status - 别把日志当状态——
log.Printf("task %s done")不等于任务成功,它可能写到一半进程就挂了
Worker panic 了怎么办
一个 goroutine panic 不该让整个 worker 进程退出,否则队列就卡死。但 Go 默认不会 recover,必须显式兜底。
每个 worker 的执行逻辑外层包一层 defer func() { if r := recover(); r != nil { log.Error(r) } }(),否则一次 panic 就停掉整个 goroutine 池。
-
beeclaw的Worker.Run内部已做 recover,但自定义 handler 里仍要单独处理 - panic 后别静默吞掉——至少记录 error、上报 metrics、触发告警
- recover 后建议把当前任务标记为
failed并入重试队列,而不是直接丢弃
真实场景里最常被忽略的,是任务上下文的生命周期管理:context 超时没传下去、DB 连接没 close、文件句柄没释放。这些不会立刻报错,但压测一跑就泄漏。可靠性不只是重试和幂等,更是每个 goroutine 的收尾干净。


















