Redis Pub/Sub不保证消息不重复,因是纯广播机制且无ACK;需发布方生成唯一msg_id并透传,消费者用SETNX+EX做幂等校验,注意Redis实例故障导致去重失效风险。

Redis Pub/Sub 本身不保证消息不重复
这是最常被误判的一点:Redis 的 PUBLISH/SUBSCRIBE 是纯广播机制,没有 ACK、没有消费确认、也没有去重逻辑。网络抖动、客户端重连、多实例订阅同一频道,都可能让同一条消息被多次收到——这不是 bug,是设计如此。
- 发布端调用一次
publish('channel', 'msg'),所有当前在线的订阅者都会收到一份副本 - 如果一个服务启了 3 个进程监听
order_events频道,每条消息天然就被投递 3 次 - Redis 不记录谁收到了、谁没收到、谁处理失败了,它只管“发出去”
用 SET 或 Redis String + EX 做轻量级幂等校验
在消费者端做“是否已处理过”的判断,是最直接、成本最低的方案。关键不是存什么,而是怎么存得快、查得准、不拖慢主流程。
- 推荐用
SET结构:利用SETNX(或SET ... NX EX)原子写入 + 过期,避免手动删 key 和锁竞争 - 消息 ID 必须由发布方生成并随 payload 透传,比如用
uuid4()或 Snowflake ID,不能靠 Redis 自增或时间戳拼接(易冲突) - 校验必须在真正执行业务逻辑前完成,且失败时直接丢弃(
return),不要ACK或重试——Pub/Sub 本就不支持 ACK
if not redis_client.set(f"seen:{msg_id}", "1", nx=True, ex=3600):
return # 已处理过,跳过
# 后续执行下单、发通知等真实逻辑
别把 PSUBSCRIBE 当去重手段
有人以为用模式订阅(如 PSUBSCRIBE order.*)能绕开重复,其实完全无关。模式匹配只影响“哪些频道的消息能被收到”,不影响“收到几次”。多个实例仍会各自收到全量匹配消息。
-
PSUBSCRIBE和SUBSCRIBE在重复问题上行为一致,只是订阅粒度不同 - 如果你用
PSUBSCRIBE order.*,而发布端发了PUBLISH order.created ...和PUBLISH order.updated ...,两个消息都会被每个订阅者收到 - 真正要防重复,还是得回到消息 ID + 存储校验这一层,和订阅方式无关
注意 Redis 实例生命周期与去重数据一致性
用 Redis 做去重缓存,最大的隐性风险不是性能,而是数据“看不见的丢失”:比如 Redis 重启、主从切换、集群 slot 迁移,都可能导致 seen:{msg_id} 记录消失,从而引发误重放。
- 如果业务对“绝对不重复”要求极高(如支付扣款),仅靠 Redis
SET不够,需结合 DB 去重表或带持久化的分布式锁服务 - 若用 Redis Cluster,确保
seen:{msg_id}的 key 落在同一个 slot(加{...}标签),否则跨节点查询会失败 - 过期时间(
EX)别设太长——消息重放窗口一般按业务容忍周期定,1 小时足够覆盖绝大多数重连场景;设成 7 天反而增加误判概率
实际跑起来之后,最容易被忽略的是:消息 ID 的生成责任归属和传递完整性。发布方漏传、序列化时截断、JSON 字段名写错(比如传了 message_id 但代码里读 msg_id),都会让整套幂等逻辑形同虚设。











