Redis SETNX必须原子执行,不可拆为GET+SET;否则并发时两goroutine均判空后写入,导致双写。正确做法是单行调用rdb.SetNX(ctx, key, "done", ttl),对应底层原子命令SET key val NX EX seconds。

Redis SETNX 必须原子执行,不能拆成 GET + SET
常见错误是先 rdb.Get(ctx, key) 判断是否存在,再 rdb.Set(ctx, key, "done", ttl)。这中间存在竞态窗口:两个 goroutine 同时查到 key 不存在,接着都写入,导致双写。正确做法只用一行:rdb.SetNX(ctx, key, "done", ttl),它底层对应 Redis 的 SET key val EX seconds NX 命令,天然原子。
使用 github.com/go-redis/redis/v9 时,注意返回值:ok, err := rdb.SetNX(ctx, key, "done", ttl).Result(),仅当 ok == true 才执行业务逻辑;ok == false 表示已被其他实例抢先占坑,必须立刻 msg.Ack() 并 return,绝不 Nack 或重试。
Key 必须带业务上下文,不能只依赖消息 ID
msg.MessageId 在 Kafka 和 RabbitMQ 中常为空;msg.DeliveryTag 在消费者重启后会重置;msg.Key 可能缺失或语义不唯一。直接拿这些字段做 key,去重必然失效。
推荐格式:idempotent:order_created:20260527-ABC123:sha256(body)[:6],其中:
-
order_created是事件类型,隔离不同业务场景 -
20260527-ABC123是稳定业务主键(如 order_id),不是自增 ID 或随机 UUID -
sha256(body)[:6]防止消息体微小差异(空格、字段顺序)导致哈希不一致
TTL 要 ≥ 业务最大耗时 × 2.5,且不能靠“续租”补救
TTL 设太短(比如 5s),而实际业务链路(扣库存 + 写 DB + 发通知)最长要 8s,第二次请求进来时 key 已过期,就会被误判为新消息。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
设为 30 * time.Second 是更安全的下限。不要幻想在业务中定期 EXPIRE 续租——任务若 panic、卡死或网络中断,续租就断了,反而让 key 永久残留,后续所有同 key 请求全被拦截。
唯一兜底是数据库唯一索引:UNIQUE KEY idx_order_id (order_id),它不依赖外部组件,事务回滚也生效,但仅作最后防线,不能替代 Redis 占坑。
失败路径必须收敛到 Ack 跳过,而非 Nack 重试
多个消费者实例或多个 goroutine 同时拿到同一条消息,SetNX 必然只有一个成功。关键是怎么处理失败者:
- 成功者:执行业务逻辑,完成后无需清理 Redis key(TTL 自动回收)
- 失败者:立刻
msg.Ack()并return—— 这是硬性要求
一旦失败者调 msg.Nack(true, false),消息会重回队列头部,下一轮又被分发,可能触发无限循环。更危险的是,如果业务逻辑内部 panic,必须 recover 后仍调 msg.Ack(),否则 delivery tag 悬挂,Broker 以为还在处理,不再投递新消息。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










