必须由Go消费端显式实现,因Kafka的enable.idempotence=true仅约束生产者、RabbitMQ的msg.Redelivered不可靠、Redis PubSub不保证必达,且消息队列均无法覆盖消费者崩溃或rebalance导致的重复fetch;数据库唯一索引仅能拦截写入重复,无法阻止扣款、发短信等已发生的副作用。

消息幂等消费必须由 Go 消费端代码显式实现,RabbitMQ/Kafka/Redis PubSub 都不保证“只投递一次”,靠 msg.Redelivered、enable.idempotence=true 或数据库唯一索引兜底,会漏掉已发生的业务副作用(如扣款、发短信)。
为什么不能依赖消息队列自带的“幂等”配置
Kafka 的 enable.idempotence=true 只约束生产者重试不重复写入,对消费者拉取无影响;RabbitMQ 的 msg.Redelivered 字段常为 false 却仍是重投(比如 channel 重建时);Redis PubSub 甚至不保证消息必达。这些机制覆盖不了消费者崩溃、网络闪断、rebalance 导致的重复 fetch —— 这是常态,不是异常。
常见错误现象包括:同一条 order_created 消息触发两次库存扣减、日志里看到相同 message_id 出现两次但订单表多出记录、用 msg.DeliveryTag 去重失败(它在消费者重启后会变)。
- AMQP 的
MessageId不强制设置,很多客户端默认为空或复用 -
msg.RoutingKey+msg.ExchangeName组合不具备全局唯一性,不同业务消息可能撞路 - 仅拼接
msg.Body做哈希也不安全——若 body 含时间戳、随机 ID、traceID,每次哈希值都不同
Go 消费端用 Redis SETNX 实现原子占坑
核心是用 SET key value EX seconds NX 一次性完成「检查是否存在 + 设置成功」,避免 GET + SET 的竞态窗口。推荐使用 github.com/go-redis/redis/v9 的 rdb.SetNX(ctx, key, "done", ttl) 方法。
key 必须含业务上下文,例如:"idempotent:order_created:789:1a2b3c",其中 789 是 order_id,1a2b3c 是 sha256(msg.Body)[:3] 的十六进制前缀;value 填 "done" 或当前 trace_id,便于排查。
- 过期时间
EX要 ≥ 单次业务最大耗时 × 2.5,比如处理最长 12s,设40 * time.Second - 返回
true才执行业务逻辑;返回false必须立刻msg.Ack()并return,不可panic、不可msg.Nack(false, true) - 若业务 panic,不要依赖 defer 清理 key——更应让业务逻辑本身可重入(比如扣库存用 CAS 而非先查后减)
消息 ID 构造必须可复现且防碰撞
生产者和消费者必须用同一套规则生成 ID,否则去重完全失效。不能依赖消息体原始字段,也不能只拼接路由信息。
推荐组合:业务标识(如 "order_created") + 语义主键(如 order_id) + sha256(msg.Body).Sum(nil) 的前 6 字节(转 hex)。
示例函数:
func calcID(topic string, orderID string, body []byte) string {
h := sha256.Sum256(body)
return fmt.Sprintf("idempotent:%s:%s:%x", topic, orderID, h[:3])
}
- 生产者 publish 前调用该函数生成 ID,并塞入
msg.Headers(Kafka)或 payload(RabbitMQ/Redis) - 消费者从对应位置提取,再拼成 Redis key;若提取失败,应拒绝该消息(不降级为裸 body 哈希)
- 禁止用
time.Now().UnixNano()或rand.Intn()参与构造——它们破坏可复现性
数据库唯一索引只是兜底,不是主力
唯一索引(如 UNIQUE INDEX idx_msg_id (msg_id))只能拦住第二次写入,拦不住已发生的副作用:两个 goroutine 同时通过 Redis 校验(窗口极小),都拿到 true,都执行了扣库存、调支付接口,其中一条最终因唯一索引报错(unique_violation 或 ErrCodeDuplicateEntry),但钱已经扣了。
此时你得额外查库确认该 msg_id 是否真成功,再决定返回缓存结果还是重试——这已超出唯一索引能力范围。
- 唯一索引适合做最终一致性校验,比如插入订单后查
SELECT id FROM orders WHERE client_msg_id = ? - 不要把唯一索引当第一道防线:它无法防止 HTTP 调用、邮件发送、MQ 再投递等外部副作用
- 高一致要求场景(如资金类),需将幂等状态落库并和业务操作同事务,而非仅靠 Redis
真正容易被忽略的是:并发下 SETNX 失败后,必须立刻 Ack 并退出,而不是“等等看”或“记个日志就算了”。这个动作看似简单,却是防止消息反复入队的关键开关;而 ID 构造是否可复现,决定了整套方案在跨服务、滚动发布时会不会突然失效。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











