Redis订阅者断连后不补发消息是设计使然;Go需显式调用PubSub.Close()防goroutine泄漏,Python需确保pubsub.close()执行,Java需解耦连接与监听并手动重订阅,业务层须用List+Pub/Sub混合模式兜底。
订阅者异常断开后,Redis 不会自动清理订阅关系,也不补发断连期间的消息——这是设计使然,通常不是异常。真正要解决的,是客户端资源泄漏、重复消费、以及业务消息丢失这三类问题。
不调用 Close() 会导致 goroutine 持续阻塞在 Receive() 或 ReceiveContext() 上,无法退出,内存和连接句柄持续累积。
defer ps.Close() 不能写在启动监听的 goroutine 内部——因为 Receive() 是阻塞调用,defer 根本不会执行context.WithCancel 控制生命周期,把 ctx 传给 ps.ReceiveContext(ctx);收到 ctx.Done() 后,先调 ps.Close(),再等待 goroutine 退出redis.Nil 或 context.Canceled 错误继续调 ReceiveContext(),会 panic 或死循环直接丢弃 pubsub 对象而不调用 close(),其内部线程不会终止,回调引用无法释放,可能引发重复消费或内存泄漏。
ps = r.pubsub(); ps.subscribe(...); # 忘记 ps.close()
try/finally 或 with(需自行封装上下文管理器),确保 ps.close() 执行ps.close() 和 r.connection_pool.disconnect()
订阅连接(StatefulRedisPubSubConnection 或 JedisPubSub)和命令连接分离,但 close 逻辑常被混用或遗漏。
autoReconnect=true 不等于“自动恢复订阅”,重连后必须手动 subscribe(),否则监听无效JedisPubSub 回调里不能直接 new Jedis 重连——会复用连接池中已失效的连接,触发 RedisConnectionClosedException
onException 或连接关闭事件 → 清理旧 PubSubConnection → 启动带退避的重试 → 成功后再 subscribe
Pub/Sub 本身不存消息,断连即丢。想“不丢”,就得放弃纯 Pub/Sub,改用混合模式。
r.lpush('queue:order_events', data),再 r.publish('channel:order_updated', '1')
lrange queue:order_events 0 -1 补读,处理完再 ltrim queue:order_events 0 -1 清空ltrim queue:order_events -1000 或 TTL 清理策略最常被忽略的点是:服务端 tcp-keepalive 和 timeout 没配,导致连接静默断开;而客户端又没心跳,结果既没及时发现断连,也没触发重连——最后归咎于“订阅不稳定”,其实根源在 TCP 层配置缺失。