Pub/Sub、阻塞命令与连接池
这些能力共同涉及一个关键概念:有些操作会长期占用连接,因此不能随意与普通命令共用。
Pub/Sub 是什么
发布者向频道发送消息,当前订阅者实时收到消息:
publisher -> channel -> subscriber A
-> subscriber B
Redis Pub/Sub 不保存历史消息。订阅者离线时发送的消息不会在重新上线后补发。
使用独立订阅连接
RESP2 模式下,进入订阅状态的连接不能再执行普通命令。因此官方建议复制客户端:
const subscriber = client.duplicate();
subscriber.on("error", (error: Error) => {
console.error("Redis subscriber error", error);
});
await subscriber.connect();
const listener = (message: string, channel: string): void => {
console.log(`频道 ${channel} 收到:${message}`);
};
await subscriber.subscribe("nloop:notifications", listener);
发布使用普通客户端:
await client.publish(
"nloop:notifications",
JSON.stringify({ type: "USER_CREATED", userId: 42 }),
);
取消订阅并关闭:
await subscriber.unsubscribe("nloop:notifications", listener);
await subscriber.close();
重复订阅同一频道会注册多个监听器,每个监听器都会收到消息。
什么时候使用 Pub/Sub
适合:
- 在线服务之间的即时通知。
- WebSocket 节点之间广播事件。
- 对偶尔丢失消息不敏感的缓存失效通知。
不适合:
- 必须保证处理一次的订单任务。
- 消费者离线后仍需补发。
- 需要确认、重试、死信队列和消费进度。
这些场景应考虑 Redis Streams 或专业消息队列。
阻塞命令
BLPOP 会等待列表出现元素:
const blockingClient = client.duplicate();
blockingClient.on("error", console.error);
await blockingClient.connect();
const item = await blockingClient.blPop("nloop:jobs", 10);
等待期间这条连接不能正常承担其他请求。不要在应用共享的主客户端上执行长时间阻塞命令。
RedisClientPool
Node-Redis 新版将隔离连接池提取为独立的 RedisClientPool,适合需要多条独占连接的场景:
import { createClientPool } from "redis";
const pool = await createClientPool()
.on("error", (error: Error) => {
console.error("Redis pool error", error);
})
.connect();
await pool.ping();
普通 GET/SET 通常只需要一个共享客户端,不要照搬数据库连接池的经验给 Redis 建几十条连接。
消息格式最佳实践
定义清晰的消息结构:
interface UserCreatedEvent {
version: 1;
type: "USER_CREATED";
occurredAt: string;
data: {
userId: number;
};
}
- 带版本号,便于将来升级结构。
- 监听器中捕获 JSON 解析错误。
- 不发送敏感字段。
- 消费处理要快速;耗时任务应转入可靠队列。