应选用asynq而非裸goroutine:当任务需至少执行一次、支持延迟执行、需失败重试与历史追溯、或多实例避免重复消费时。asynq基于redis提供原子pop/ack机制,天然保障分布式可靠性。

Go 里做异步任务,别一上来就写 Worker 池或自研队列——90% 的场景用 goroutine + channel 或成熟库(如 asynq、machinery)就够了,自研调度器反而容易在重试、幂等、监控上翻车。
什么时候该用 asynq 而不是裸 goroutine
裸 goroutine 适合瞬时、无状态、失败可丢的任务(比如发个日志、触发个埋点)。一旦涉及以下任一条件,就得考虑持久化队列:
- 任务必须保证至少执行一次(如扣库存、发通知)
- 需要延迟执行(
asynq.NewTask("send_email", payload, asynq.ProcessIn(5 * time.Minute))) - 要查看失败历史、手动重试、设置最大重试次数(
asynq.MaxRetry(3)) - 有多个服务实例,需避免重复消费(
asynq基于 Redis 的原子 pop + ack 机制天然支持)
asynq 启动时连不上 Redis 怎么办
默认情况下 asynq.NewServer 会立即尝试连接 Redis 并阻塞,导致服务启动失败。正确做法是预检 + 可配置超时:
- 用
redis.DialTimeout设置连接超时(如 3 秒),避免卡死 - 在
asynq.NewServer前加一层健康检查:redis.Ping()成功后再初始化 Server - 把
asynq.Server的启动放到独立 goroutine,并监听os.Signal实现优雅关闭 - 错误日志里若出现
"redis: can't marshal type",大概率是 task payload 里含了未导出字段或函数类型,只允许 JSON 序列化的结构体
如何让一个任务真正“幂等”
异步任务的幂等不靠框架自动实现,得自己设计防重逻辑。常见且有效的组合是:
- 任务入队前生成唯一 ID(如
uuid.NewSHA1(uuid.Nil, []byte(fmt.Sprintf("%s:%s", orderID, "notify")))),作为asynq.TaskID - 消费者执行前先查 DB 或 Redis:若
task_id:status已为"success",直接 return - 关键操作用数据库
INSERT ... ON CONFLICT DO NOTHING(PostgreSQL)或REPLACE INTO(MySQL)兜底 - 避免用时间戳或随机数当幂等键——它们无法跨实例全局唯一
本地开发调试时怎么跳过 Redis
别 mock Redis 客户端,太重。推荐两种轻量方案:
- 用
asynq.RedisClientOpt{Addr: "localhost:6379", Password: "", DB: 15}指向本地 Redis,DB 设为 15 避免污染其他环境 - 测试时换用内存版驱动:
asynq.NewMemoryMux()+asynq.NewClient(asynq.MemoryBackend()),但注意它不支持延迟任务和重试策略,仅限单元测试 - 切记:CI 环境里禁用
MemoryBackend,否则会漏掉真实 Redis 的序列化/连接问题
真正难的从来不是把任务扔进队列,而是定义清楚“什么算成功”——是 DB 写入完成?还是第三方 API 返回 200?这个判断点一旦模糊,重试、超时、告警全都会失准。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











