Kafka幂等生产者是最省心的防重方案,需开启idempotence并满足单生产者单分区条件;语义去重则需应用层指纹+Redis原子校验。

用 Kafka 幂等生产者是最省心的防重发送方案
Kafka 0.11.0+ 原生支持幂等性,只要开启配置,就能从协议层杜绝「同一条消息重复入队」。它不依赖业务逻辑、不增加中间件、也不需要你手写去重逻辑——前提是你的场景满足「单生产者实例 + 单分区语义」。
-
config.Producer.Idempotent = true必须设为true,这是开关;同时config.Producer.Retry.Max要 ≥ 1(幂等依赖重试机制) - 不能手动指定
Partition后再启用幂等——否则会 panic:Kafka 要求幂等生产者必须由 broker 自动分配分区,否则无法保证 PID + SeqNum 的连续性 - 每条消息的
Key不影响幂等性,但会影响分区路由;如果你靠Key做语义分组(比如 user_id),那幂等只在该分区维度生效,跨分区不保序也不防重 - 注意 client ID:同一
client.id下重启的生产者能延续 PID,不同 client ID 视为全新生产者,历史 SeqNum 不继承
自己生成唯一消息指纹 + Redis 校验适合自定义去重逻辑
当你要防的是「语义重复」而非「字节重复」时(比如两条内容完全一样的订单创建请求),就得在应用层做指纹提取+缓存校验。这时候 Kafka 幂等没用——它只认 byte-level 相同,不理解 JSON 字段含义。
- 指纹建议拼接:
method + topic + userID + orderID + body_hash[:12],别用纯时间戳或time.Now().UnixNano(),容器里纳秒级极易碰撞 - Redis 写入必须用原子命令:
SET key "processing" EX 300 NX,返回OK才继续发消息;返回(nil)就直接跳过,避免多协程并发写入同一条 - 过期时间要大于消息端到端最大耗时(含网络延迟、broker 处理、消费者拉取),建议设为 5–10 分钟;太短会导致“刚发完还没消费,key 就过期了”,下次重试仍被放行
- 不要把整个
body存 Redis,只存指纹和状态;结果数据另存(如用idempotent:result:{fingerprint}),避免大 value 拖慢 SET
sync.Map 在单机轻量场景下可替代 Redis,但得自己管过期
如果你是单实例部署、QPS 不高(sync.Map 是零依赖、低延迟的选择。但它不提供 TTL,所有清理逻辑都得你补全。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 键名格式推荐:
fmt.Sprintf("%s:%s:%s", topic, userID, hex.EncodeToString(hash[:8])),避免不同 topic 冲突 - 值类型别用
bool,改用结构体:type idempotentItem struct { done bool; result []byte; ts time.Time },方便后续判断是否超时 - 必须启动 goroutine 定期扫描:
time.AfterFunc(10*time.Second, cleanupExpired)或用time.Ticker,否则内存只增不减 - 写入前先
Load,命中且done == true就直接返回缓存结果;未命中则LoadOrStore占位,执行完再Store更新状态
数据库唯一索引是最后一道不可绕过的防线
无论 Kafka 幂等、Redis 校验还是 sync.Map,都可能因网络中断、服务崩溃、配置错误而失效。真正能 100% 拦住重复数据的,只有数据库的唯一约束。
立即学习“go语言免费学习笔记(深入)”;
- 联合唯一索引字段必须带业务上下文,例如:
UNIQUE INDEX idx_user_msg_type_content (user_id, msg_type, content_hash),而不是只对id加唯一 - Go 中捕获冲突要具体:
if pgx.ErrCodeUniqueViolation.Match(err) || strings.Contains(err.Error(), "Duplicate entry"),别用if err != nil一锅端 - 插入失败 ≠ 业务失败;如果是幂等场景,捕获到唯一冲突后应返回原始成功响应(比如查出已存在的记录并返回),而不是抛错给上游
- 别在事务里先 INSERT 再删 Redis key——DB 成功但 Redis 写失败,下次请求会因 key 存在被误拒;应该先校验 Redis,再进事务,最后才更新缓存(或干脆不删,靠过期自动清理)
最麻烦的不是写代码,而是搞清「哪一层该拦什么」:Kafka 拦传输重复,Redis/sync.Map 拦请求重复,DB 拦数据重复。漏掉任何一层,上线后都会在凌晨三点收到告警。

















