kafka等消息队列不保证“只投一次”,消费者端必须自行实现幂等逻辑;enable.idempotence=true仅防生产者重复发,不解决消费者重复处理;redis setnx需原子执行(带nx+ex)、key须含业务上下文、db唯一索引为最终兜底。

消息幂等不是中间件自动开关,而是消费端必须自己写代码实现的逻辑——Kafka、RabbitMQ、Redis PubSub 都不保证“只投一次”,只要网络抖动、消费者重启、分区重平衡发生,同一条消息就可能被推两次。你得在 Go 代码里显式拦截。
为什么不能靠消息队列自带的“幂等”配置
enable.idempotence=true 只防生产者重复发,不拦消费者重复处理;RabbitMQ 的 msg.Redelivered 字段常不准(比如 channel 关闭重建后仍为 false);Redis PubSub 根本没重投概念,全靠你兜底。这些机制覆盖不了业务链路全程,而扣款、发券、改状态一旦执行就不可逆。
常见错误现象:Duplicate entry for key 'idx_order_id'、日志里同一 delivery_tag 出现两次但数据库多了一条记录、用户投诉“明明只点了一次,却扣了两次钱”。
-
msg.MessageId常为空,msg.Key在 Kafka 里可能缺失或语义不唯一 - 只用
GET + SET检查 Redis,存在竞态窗口:A 查无,B 查无,A 写入,B 写入 → 双写 - TTL 设太短(如 5s),业务实际耗时 8s,导致第二次请求误判为新请求
Go 里怎么用 Redis SetNX 实现原子占坑
核心是 rdb.SetNX(ctx, key, "done", ttl) 一行完成“检查+占位”,不能拆开。
key 必须带业务上下文,推荐格式:idempotent:order_created:20260527-ABC123:sha256(body)[:6]
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
-
order_created是事件类型,避免不同业务共用 key 空间 -
20260527-ABC123是业务主键(如 order_id),不是自增 ID 或 UUID -
sha256(body)[:6]防 body 微小差异(空格、字段顺序)导致哈希不一致 - ttl 至少设为
业务最大耗时 × 2.5,比如最长 8s 就用30 * time.Second
用 github.com/go-redis/redis/v9 时:ok, err := rdb.SetNX(ctx, key, "done", ttl).Result(),ok == true 才执行业务逻辑;ok == false 就直接 msg.Ack() 并 return,绝不 Nack、不重试、不 panic。
数据库唯一索引为什么是必须的兜底
Redis 可能宕机、网络分区、写入成功但业务 panic 导致未清理——单靠缓存永远无法 100% 保证。DB 层必须用唯一约束做最终防线。
别只建 UNIQUE INDEX idx_order_id (order_id),要结合业务语义:
- 订单创建:建
UNIQUE INDEX idx_user_id_idempotency_key (user_id, idempotency_key) - 支付回调:建
UNIQUE INDEX idx_trade_no_out_trade_no (trade_no, out_trade_no) - 插入失败时,精准捕获错误:
pgx.ErrCodeUniqueViolation == err.(pgconn.PgError).Code或 MySQL 的errno 1062 - 捕获到唯一冲突后,查库确认是否真已成功;不是所有 error 都算幂等成功,只有唯一键冲突才算
并发场景下,多个 goroutine 同时拿到同一条消息,SetNX 必然只有一个成功,但 DB 唯一索引会把漏网之鱼也拦住——这是最后一道物理防线。
最容易被忽略的是:key 的业务上下文必须稳定。用时间戳、随机数、body 哈希当 key,要么冲突高,要么无法区分重试和新请求。客户端生成的 X-Idempotency-Key 要复用,服务端不生成、不修改;Redis TTL 要大于整个调用链最慢路径;DB 索引字段宽度要控制,太宽影响性能和内存占用。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










