ioredis Pub/Sub内存泄漏的根本原因是连接生命周期与事件监听器绑定未对齐:重连后旧监听器未清除、未监听ready事件导致订阅失效、message回调中持有大对象或阻塞处理,三者叠加致堆内存持续上涨。

为什么ioredis Pub/Sub容易引发内存泄漏
根本原因不是代码写错了,而是连接生命周期和事件监听器绑定方式没对齐。ioredis的subClient一旦调用subscribe,就会进入订阅模式并持续监听message、pmessage等事件;如果没在连接断开或销毁前手动移除监听器,这些回调函数会一直挂在EventEmitter上,闭包持有的上下文(比如大对象、数据库连接、日志实例)就无法被GC回收。
必须显式移除message/pmessage监听器
很多人以为“连接断了监听自然失效”,但ioredis重连后会复用原有实例,on('message', handler)注册的监听器不会自动清理,重连后还会继续触发——旧handler可能引用已废弃的上下文,新旧handler共存导致堆内存缓慢上涨。
- 每次
subClient.on('message', handler)前,先调用subClient.removeAllListeners('message') - 若使用
psubscribe,也需对应调用subClient.removeAllListeners('pmessage') - 在
subClient准备销毁(如服务关闭)时,务必执行subClient.quit()+subClient.removeAllListeners() - 避免在闭包中捕获大对象:比如不要把
req、res或整个Express app实例传进message回调
重连后必须重新subscribe,且不能漏掉ready事件
ioredis自动重连成功后,subscribe状态不会恢复——这是协议层限制,不是bug。如果你在connect后立刻subscribe,但连接尚未真正就绪(比如AUTH未完成),订阅会静默失败,后续消息全丢,而监听器还挂着,形成“假活跃真空转”状态,长期积累就是内存泄漏温床。
- 必须监听
subClient.on('ready', () => { subClient.subscribe('channel'); }),而不是connect或connect后的setTimeout - 重连后
ready事件会再次触发,所以subscribe逻辑要能重复执行(幂等) - 在
subscribe回调里检查err,失败时记录日志并重试,避免“无声失效”
避免在message回调里做耗时或阻塞操作
Node.js是单线程,message回调如果同步执行大量计算、JSON.parse超大字符串、或await一个没加timeout的数据库查询,会导致事件循环卡住,后续消息堆积在内存队列里来不及处理,subClient缓冲区持续增长,RSS内存稳步上升。
- 对
message内容做长度校验:if (message.length > 1024 * 1024) return;,直接丢弃超长消息 - 异步操作必须加超时:
Promise.race([db.query(...), new Promise(r => setTimeout(r, 5000))]) - 高频消息场景下,考虑用
setImmediate或queueMicrotask把处理逻辑推到下一周期,防止单次处理阻塞太久
最常被忽略的一点:Pub/Sub本身不保存消息,断连即丢失,所以别指望靠它做可靠消息传递;内存泄漏往往不是某一行代码的问题,而是“监听器没清理 + 重连没重订 + 回调里堆数据”三者叠加的结果。上线前务必用process.memoryUsage()监控heapUsed和rss变化趋势,尤其在模拟网络抖动后观察是否回落。


















