pubsub.close() 必须显式调用,gc 不会自动释放连接或停止监听 goroutine/线程;漏调用会导致僵尸连接、资源泄漏及 connected_clients 持续上涨,需在监听退出或取消时立即关闭,并在重连前强制 close 旧实例。

PubSub.Close() 必须显式调用,不能依赖 GC
Go 的 redis/v9、Python 的 redis-py、Java 的 Lettuce 都要求你手动调用 Close() 或 close() —— GC 不会帮你停掉监听 goroutine 或线程,也不会释放底层连接。一旦漏掉,这个连接就永远卡在连接池里,变成“借走不还”的僵尸资源。
-
Subscribe()返回的*redis.PubSub(Go)或PubSub实例(Python/Java)会独占一个连接,并启动阻塞循环(如Receive()或后台线程) - 哪怕上下文已取消、Redis 服务重启、或 channel 已关闭,只要没调
Close(),goroutine/线程仍在等待消息,连接也不归还 -
defer pubsub.Close()写在主 goroutine 里无效:因为Receive()是阻塞的,defer永远不会执行到 - 正确写法是:在监听循环退出后、或收到取消信号时,立刻调用
pubsub.Close(),并检查返回错误
重连逻辑中旧 PubSub 必须先 Close 再新建
网络抖动或 Redis 临时不可用时,重连代码最容易引发双倍泄漏:旧连接没关,新连接又建,两个都卡住。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 错误模式:
oldPubSub.Close()放在if err != nil分支里 —— 若Close()本身失败(比如连接已断),旧对象内部状态可能混乱,goroutine 仍运行 - 稳妥做法:每次重连前,不管 oldPubSub 是否还活着,都先尝试
oldPubSub.Close();再判断是否成功,失败也继续新建,避免阻塞 - Go 示例:
if oldPubSub != nil {<br> _ = oldPubSub.Close() // 忽略 close 错误,避免阻塞<br>}<br>newPubSub := rdb.Subscribe(ctx, topic) - Python 中同理:
old_pubsub.close()后再client.pubsub(),不要省略
Spring Boot 中 RedisMessageListenerContainer 的泄漏风险
RedisMessageListenerContainer 底层封装了长期订阅连接,配置不当会悄悄建一堆连接却不释放。
- 默认重试策略(如无限重试)+ 网络异常 → 容器不断新建连接,旧连接未清理 →
connected_clients持续上涨 - 必须显式配置
setPhase(1)和setShutdownTimeout(),并在应用关闭时调用stop() - 监控关键指标:
info clients查connected_clients趋势;CLIENT LIST看idle超过 3600 秒的连接,尤其来源 IP 集中在某台应用机器 - 别混用:
@Autowired RedisTemplate是安全的,但若同时手动 newJedisPool.getResource(),漏close()就直接泄漏
连接池配置与监控不能替代代码修复
调大 max-active 或加 testWhileIdle 只能延缓问题,无法根治 PubSub 泄漏。
- 连接池参数(如 Lettuce 的
max-active: 200)只是缓冲带,不是兜底方案 -
testWhileIdle可检测空闲连接是否有效,但对“被 PubSub 占着不动”的连接无效 —— 它根本不算空闲 - 真正有效的监控是:定期抓
jstack看线程是否卡在getResource();用 Prometheus 监控lettuce.pool.active.count是否缓慢爬升 - 最易被忽略的点:所有语言的 PubSub 对象生命周期都由业务代码完全控制,没有自动回收机制 —— 这不是 bug,是设计使然










