Go消费者须用rdb.SetNX(ctx, key, "done", ttl)一行原子占坑,key含事件类型、业务主键与body哈希前6位,ttl≥业务最大耗时×2.5,失败直接ACK不重试,Redis故障返回503而非降级。

Go 消费者怎么用 Redis SetNX 卡住重复消息入口
必须用 rdb.SetNX(ctx, key, "done", ttl) 一行完成原子占坑,不能拆成 EXISTS + SET 或 SETNX + EXPIRE。后者在两个 goroutine 同时查到 key 不存在、都写入的瞬间,就会双扣款。
key 要带全业务上下文:"idempotent:order_created:20260527-ABC123:sha256(body)[:6]" —— 其中 order_created 是事件类型,20260527-ABC123 是业务主键(不是自增 ID),sha256(body)[:6] 防 body 字段顺序/空格微小差异导致哈希不一致。
ttl 至少设为业务最大耗时 × 2.5,比如处理逻辑最长 8s,就用 30 * time.Second;太短会导致重试请求误判为新请求,太长会堆积无效 key。
-
ok, err := rdb.SetNX(ctx, key, "done", ttl).Result(),ok == true才执行业务逻辑,ok == false就直接msg.Ack()并 return - 绝不能
Nack或重试,否则 Kafka/RabbitMQ 会反复推同一条消息 - err != nil 时必须返回错误(如 503),不能静默放行——Redis 故障 ≠ 幂等失效可妥协
为什么数据库唯一索引只是兜底,不是主防线
唯一索引拦不住“两个请求同时通过 Redis 校验,一起走到 INSERT 阶段”的竞态窗口。其中一个成功插入,另一个报 1062(MySQL)或 pgx.ErrCodeUniqueViolation(PostgreSQL),但前者可能已经发了 MQ、扣了余额、改了状态。
所以唯一索引必须是联合的,覆盖关键业务维度,例如:UNIQUE INDEX idx_user_id_idempotency_key (user_id, idempotency_key),而不是单列 idempotency_key。
- INSERT 前必须把幂等记录(如
idempotent_logs表)与主业务逻辑放在同一事务中 - 捕获唯一冲突后,不能直接返回错误,要再
SELECT一次确认状态是否已完成,再决定返回 200 还是 400 - 索引字段别选太宽(比如整个 request body),用客户端传的短 key 最稳妥
消息队列位点提交没配好,重复消费根本防不住
自动提交(enable.auto.commit=true)下,如果业务处理完但位点还没提交,消费者重启或分区重平衡,就会从上一个已提交位点重新拉取——那批刚处理完的消息就又来一遍。你看到日志里同一 delivery_tag 出现两次,但数据库多了一条记录,大概率就是这个原因。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
手动提交才是可控方案:业务逻辑执行成功后,显式调用 msg.Commit()(Kafka)或 ch.Ack()(RabbitMQ),且必须确保它在事务 commit 之后执行。
- 别把位点提交和 DB 提交放在不同 goroutine 里异步做,容易错序
- 监控
consumer_lag,滞后突增往往意味着位点卡住或提交失败 - Kafka 的
__consumer_offsets主题默认 50 分区,消费者组名哈希决定路由,别用太短或易碰撞的 group.id
Go 里怎么避免“先查后改”类操作的竞态
当业务依赖当前状态(比如“只有 pending 订单才能转 paid”),光靠缓存或唯一索引不够,得用数据库行锁堵住窗口。核心是 SELECT ... FOR UPDATE,但它必须和后续 UPDATE 在同一个事务里完成。
WHERE 条件必须命中索引,否则会升级为表锁;事务里禁止 HTTP 调用、time.Sleep、复杂计算——锁持有时间越短越好。
- 别用
SELECT ... FOR UPDATE NOWAIT简单报错,应配合有限重试(最多 3 次,间隔递增)提升成功率 - 别用
time.Now().UnixNano()当幂等 key,纳秒级在容器或高并发下极易重复,且完全脱离业务意图 - Redis 故障时不能 fallback 到“查 DB 状态再决定”,这等于放弃幂等语义——503 是唯一合理响应
真实场景里最常被忽略的是:Redis 原子命令的参数顺序、DB 唯一索引的字段组合、以及位点提交和业务事务的耦合时机——这三个点只要漏掉一个,重复消费就大概率发生。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










