ioredis 中不能共用一个实例进行 publish 和 subscribe,因为调用 subscribe 后连接进入订阅模式,后续 publish 会被 Redis 协议拒绝;必须分别创建 pubClient 和 subClient 实例,并在 subClient 的 ready 事件后重新订阅以应对重连,同时需在 message 回调中妥善捕获异常以防消息丢失。

ioredis 是目前 Node.js 中更现代、更健壮的 Redis 客户端,它原生支持 Promise、自动重连、集群模式和 Pub/Sub 的完整语义。相比老版 redis(v3.x 及以前),ioredis 在发布订阅场景下无需手动 duplicate(),也不强制要求“订阅独占连接”——它内部已做连接复用与隔离。
但正因如此,新手容易误用连接实例,导致消息丢失、监听失效或内存泄漏。
为什么不能只用一个 ioredis 实例同时 publish 和 subscribe
虽然 ioredis 允许你在同一个客户端上调用 publish 和 subscribe,但一旦调用了 subscribe(或 psubscribe),该连接就进入「订阅模式」:此后所有命令(包括 publish)都会被拒绝,并抛出 ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed in this context 错误。
这不是 bug,而是 Redis 协议层的硬性限制 —— 订阅连接只能处理订阅相关指令。
- 必须为发布和订阅分别创建独立的
Redis实例 - 推荐命名区分:比如
pubClient和subClient - 两个实例可共用相同配置(host/port/password),但不能共享同一连接对象
subClient 启动后立即监听,但收不到消息?检查这三点
常见现象是 subClient.on('message', ...) 一直不触发,哪怕 pubClient.publish('test', 'ok') 返回了 1。
- 确认
subClient已调用connect()并 await 成功 ——ioredisv5+ 默认 lazy connect,不显式调用不会真正建连 - 确认订阅发生在
connect之后,且在ready事件之后执行(推荐监听ready再subscribe) - 确认频道名完全一致:大小写、空格、前缀都需严格匹配;
publish('news', ...)对应subscribe('news'),不是'NEWS'或' news '
如何安全地处理断线重连后的订阅恢复
ioredis 默认开启自动重连(retry_strategy),但重连成功后,之前用 subscribe 订阅的频道**不会自动恢复** —— 这是最常被忽略的坑。
- 必须监听
reconnecting和ready事件,在ready后重新调用subscribe或psubscribe - 不要在
connect时一次性订阅完所有频道再不管;要把订阅逻辑封装成可重入函数 - 如果使用模式订阅(如
psubscribe('user:*')),注意通配符性能开销,避免psubscribe('*')这类全量匹配
示例片段:
subClient.on('ready', () => {
subClient.subscribe('chat:general');
subClient.psubscribe('user:*');
});
subClient.on('reconnecting', () => {
console.log('subClient 正在重连...');
});
收到 message 后异步处理失败,消息就丢了?
Redis Pub/Sub 是「发即忘」(fire-and-forget)机制:没有 ACK、没有重试、不保证送达。一旦你的 on('message') 回调抛出未捕获异常,或 await 的 Promise 被 reject,这条消息就彻底消失。
- 务必在回调内加
try/catch,或用.catch()处理 Promise 异常 - 不要在
on('message')里直接 await 长耗时操作(如数据库写入)——建议转给队列或 worker 处理 - 若业务要求可靠投递,请改用 Redis Streams + consumer group,而非 Pub/Sub
错误示范:
subClient.on('message', async (channel, message) => {
await db.insert(JSON.parse(message)); // 若这里报错,消息丢弃且无日志
});
正确做法:
subClient.on('message', (channel, message) => {
processMessage(channel, message).catch(err =>
console.error(`处理 ${channel} 消息失败:`, err)
);
});
async function processMessage(channel, message) {
const data = JSON.parse(message);
await db.insert(data);
}
真实项目里最麻烦的从来不是“怎么连上”,而是“连上后怎么不死、怎么不丢、怎么不崩”。Pub/Sub 看似简单,但连接生命周期、错误传播路径、消息语义边界,三者叠加就很容易出静默故障。尤其当服务跑几天后突然收不到通知——八成是重连没恢复订阅,或者某次 JSON.parse 报错把整个监听器搞崩了。











