redis pub/sub 会重复投递消息是因为其设计本质是无状态广播,不保证消息去重;必须由业务层用发布方生成的唯一msg_id配合setnx+ex原子操作实现幂等校验,且需考虑redis故障导致去重失效的风险。

Redis Pub/Sub 为什么会重复投递消息
不是 Redis 出了问题,而是它根本没打算防重——PUBLISH 是纯广播,所有当前在线的 SUBSCRIBE 客户端(包括同一服务的多个进程、不同机器上的实例)都会收到完整副本。没有 offset、不存消费状态、不等 ACK。网络抖动导致重连后,也会重新收到已发消息。
常见误判场景:
- 一个订单服务部署 3 个实例,监听
order.pay→ 每条支付消息被投递 3 次 - 用
PSUBSCRIBE order.*认为能“去重” → 实际只是扩大匹配范围,每个匹配到的客户端仍各收一份 - 以为靠时间戳或自增 ID 当 msg_id → 易冲突,无法保证全局唯一
必须在业务层用 SETNX+EX 做幂等校验
Redis Pub/Sub 本身不提供去重能力,任何试图调整订阅逻辑、换命令、改配置的做法都无效。唯一可靠路径:发布方生成唯一 msg_id,消费者用 SET key value NX EX seconds 原子判断是否已处理。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
msg_id必须由发布方生成并透传,推荐uuid4()或 Snowflake ID,不能拼接时间戳或用 Redis 自增 - key 命名建议统一前缀,例如
seen:order:{msg_id},避免跨业务冲突 - 必须用单条原子命令:
redis_client.set(f"seen:{msg_id}", "1", nx=True, ex=3600),分开SETNX+EXPIRE会并发漏判 - 校验失败必须直接
return,不要抛异常、不重试、不模拟 ACK —— Pub/Sub 本就不支持这些语义
Redis 故障会导致幂等失效,这点最容易被忽略
用 Redis 存 seen:{msg_id} 是轻量方案,但最大隐患不是性能,而是数据“意外丢失”:单点重启、主从切换、集群 slot 迁移,都可能让去重标记消失,从而引发误重放。
- 若业务对“绝对不重复”要求极高(如支付扣款),仅靠
SET不够,需叠加 DB 去重表或带持久化的分布式锁服务 - 用 Redis Cluster 时,必须确保
seen:{msg_id}的 key 落在同一 slot,否则跨节点查询失败 → 推荐写成seen:order:{msg_id},用{}强制哈希路由 - 过期时间设为 1 小时到 24 小时较稳妥:太短(如 5 分钟)可能因处理延迟导致误重放;太长(如 7 天)则内存占用不可控
别把分布式锁当幂等替代方案
有人用 setIfAbsent(taskId, "1", 300, TimeUnit.SECONDS) 加锁再执行,看似可行,实则埋坑:
- 锁过期后第二个实例仍可能进业务逻辑 → 本质还是没解决“是否已执行过”的判断
- 锁释放时机难把握:提前删锁 → 并发风险;不删锁 → 内存泄漏或阻塞后续消息
- 真正要防的是“同一条消息被多次执行”,不是“多个消息并发执行”,所以重点不在互斥,而在“判重”
真正难的不是写出那几行 SET,而是想清楚:你的业务能容忍多少次 Redis 故障引发的误重放,以及是否值得为它上更重的方案。










