Redis Pub/Sub 消息断连即丢,因其无持久化与消息队列;关键业务应改用 Streams 或 List+Pub/Sub 混合模式补漏,并配连接池与心跳保活。

Redis订阅连接断开后消息就丢了?Pub/Sub 本身不保证可靠性
Redis 的 PUB/SUB 是纯内存、无持久化的广播机制,一旦客户端断连(哪怕只断 1 秒),期间发布的所有消息都会永久丢失——这不是 bug,是设计使然。别指望靠重连自动补收,subscribe 命令只从“执行那一刻”开始收新消息。
常见错误现象:ConnectionError 或 TimeoutError 后,listen() 阻塞中断,再 subscribe 也收不到断连期间的任何消息;日志里反复出现 “disconnected” 却没看到对应消息处理逻辑。
- 使用场景:实时通知、事件广播类业务(如聊天室上线提醒、运维告警推送)可以接受丢失;但订单状态变更、支付结果回调等关键链路绝不能依赖原生
PUB/SUB - 根本原因:Redis 不为订阅者维护消息队列,也不记录“谁订阅过、上次收到哪条”
- 性能影响:加持久化层会引入延迟和存储开销,但这是换取可靠性的必要代价
用 List + Pub/Sub 混合模式补消息,最轻量落地方案
核心思路:把真正要传递的业务消息先存进一个 List(比如 queue:order_events),再用 PUB/SUB 发个轻量通知(比如 channel:order_updated)告诉订阅者“有新消息了,快去 lpop”。这样即使断连,重启后先扫一遍 List 就能补全。
实操建议:
- 发布端必须原子写:先
r.rpush('queue:order_events', json.dumps(data)),再r.publish('channel:order_updated', '1')—— 顺序不能反,否则可能通知发了但消息没存进去 - 订阅端启动时,先
r.llen('queue:order_events')判断是否有积压,有则循环lpop直到空,再调p.subscribe('channel:order_updated') - 注意
List长度监控:避免积压过多导致内存暴涨,可配合ltrim做 TTL 清理(例如只保留最近 1000 条) - 不要用
brpop替代lpop:阻塞式命令在断连恢复后可能跳过部分消息,非阻塞 + 循环更可控
连接池 + 心跳保活,减少断连发生频率
频繁断连是消息遗漏的前提。很多问题其实出在连接层没稳住,而不是订阅逻辑本身。Java 用 Lettuce、Python 用 redis-py、Node.js 用 ioredis,都支持连接池和心跳配置,但默认往往关着。
关键参数和易踩坑点:
-
pingConnectionInterval=1000(Lettuce)或health_check_interval=1(ioredis):必须显式开启,设为 1 秒级,防防火墙/SLB 空闲超时(通常 5–30 分钟) - 连接池
maxIdle/minIdle要合理:设太小(如minIdle=0)会导致空闲连接被回收,下次用得重建;设太大又浪费资源 - 别忽略
timeout和socketTimeout差异:前者控制命令级超时(如get卡住多久放弃),后者控制底层 socket 读写超时,两者都要设(建议 3–5 秒) - Python 的
redis-py默认不重连,需手动加retry_on_timeout=True和health_check_interval=1
真要强一致?换用 Streams 或外部消息队列
如果业务要求“一条都不能丢、必须按序、支持多消费者组”,Pub/Sub 就不该是首选。Redis 5.0+ 的 Streams 才是官方推荐的替代方案,它支持消费者组、ACK 确认、消息重传、历史回溯。
对比说明:
-
XADD写入、XREADGROUP读取,天然支持断连后从上次last_id继续消费,无需自己维护 offset - 但
Streams不支持广播(PUB/SUB的核心优势),一个消息只能被组内一个消费者处理;若需广播,得建多个消费者组 - 性能略低于
PUB/SUB(因要持久化索引),但差距在可接受范围;内存占用比 List 方案更可控 - 老版本 Redis(PUB/SUB
最容易被忽略的一点:很多人试图用 Keyspace Notifications(__keyevent@0__:expired)来兜底,但它只通知 key 变更事件,不携带业务数据,且本身也受断连影响——它不是消息通道,只是辅助信号。










