Redis Pub/Sub断连即丢消息是设计使然,非bug;可靠场景应改用Streams或List+Pub/Sub混合模式,并配心跳保活与连接池优化。

Redis 订阅者掉线后不会补收断连期间的消息,重连只是重新开始监听新消息——这是 Pub/Sub 机制的固有行为,不是 bug,也改不了。 想靠 subscribe 自动兜底,注定会丢数据。真要可靠,得换方案或加兜底逻辑。
为什么心跳检测不能只靠 TCP keepalive
Redis 的 Pub/Sub 连接是应用层长连接,tcp-keepalive(默认 300 秒)只管底层链路是否通,不管 Redis 协议层是否“活着”。实际中常见现象是:TCP 连接没断,但 Redis server 已经不响应 PING 或卡在阻塞命令里,客户端却还在傻等。
- 必须在客户端主动发
PING并等待PONG,超时即判为失联 - Jedis 不自带心跳,得自己起定时任务调用
connection.ping() - Lettuce 支持配置
pingBeforeActivateConnection=true和timeout,但仅对命令连接生效,StatefulRedisPubSubConnection需额外监听onException+ 手动触发ping() - 心跳间隔建议设为 15–30 秒,太短加重服务端压力,太长无法及时发现僵死连接
自动重连不能只靠 onPMessage 异常捕获
JedisPubSub.onUnsubscribe 或 Lettuce 的 onError 回调,往往在连接已断、线程已退出后才触发,此时再 new connection + subscribe 很可能失败或漏事件。
- 重连逻辑必须和订阅逻辑解耦:单独维护一个“重连状态机”,用
AtomicBoolean connected控制状态 - 不要在
onMessage里直接重连,容易引发并发 subscribe 导致 channel 重复监听 - 推荐做法:监听连接关闭事件 → 清理旧连接 → 启动带退避的重试(如指数退避:1s, 2s, 4s… 最大 30s)→ 成功后重建
StatefulRedisPubSubConnection→ 再subscribe - 重连前务必检查
connection.isOpen()或connection.isClosed(),避免对已关闭连接调用subscribe报RedisConnectionClosedException
List + Pub/Sub 混合模式怎么落地才不丢消息
这是目前最轻量、最可控的补漏方案,核心是把“通知”和“消息体”分离:Pub/Sub 只传信号,真实数据落 LPUSH 到 List,重连后先 LRANGE 补读再 SUBSCRIBE。
- 发布端必须原子写:
r.lpush('queue:order_events', json.dumps(data))必须在r.publish('channel:order_updated', '1')之前执行;顺序反了会导致通知发了但数据没存进去 - 订阅端启动时,先
lrange queue:order_events 0 -1拉取全量,处理完再ltrim queue:order_events 0 -1清空(或用lrem按值删),否则每次重启都重复消费 - 注意 List 长度控制,避免内存爆掉;可配合
ltrim queue:order_events -1000 0保留最近 1000 条作为缓冲 - 别用
BRPOP替代SUBSCRIBE,它本质是轮询,延迟高且浪费连接资源
连接池配置不当会让重连更脆弱
很多人以为用了 JedisPool 就万事大吉,其实 minIdle=0 或 maxIdle 过小,会导致空闲连接被回收,下次订阅还得重建连接,增加断连概率。
- JedisPool 建议设
minIdle=5、maxIdle=20、maxWaitMillis=2000,避免连接频繁创建销毁 - 别共用一个 JedisPool 实例做命令操作和 Pub/Sub —— Pub/Sub 连接是独占式、长生命周期的,混用会互相干扰
- Lettuce 推荐用
RedisClient单例 + 多个StatefulRedisPubSubConnection,每个连接绑定独立线程,避免onMessage阻塞影响心跳 - 所有连接必须显式调用
close(),JVM GC 不会自动释放 Redis 连接,泄漏后很快触发Cannot allocate memory
真正难的不是写重连代码,而是接受“Pub/Sub 本就不该承载关键业务消息”这个事实。混合模式里的 List 是兜底,不是主力;心跳和重连是保活,不是恢复。消息可靠性,终究得靠 Streams 或专业 MQ 来兜底。











