Redis 提供了一套原生的发布订阅机制,它通过频道(Channel)把消息从发布者传递到订阅者。与消息队列常见的先存储再消费模式不同,Pub/Sub 在消息发布时立即遍历当前所有订阅该频道的客户端连接,把消息直接推送给它们。整个过程中 Redis 不会持久化消息,也不会记录哪些客户端曾经订阅过。这一点决定了它适合做实时通知类的消息分发,而用做高可靠消息队列时需要做很多额外设计。下面先从核心命令和订阅行为开始分析。

一、频道订阅与模式订阅的工作方式
Redis Pub/Sub 由几个基础命令组成:SUBSCRIBE 用于订阅一个或多个频道,PUBLISH 用于向指定频道发送消息,UNSUBSCRIBE 用于退订。订阅一旦建立,客户端会进入阻塞状态,持续接收来自服务端的消息推送。
除了精确匹配频道名,Redis 还提供模式订阅命令 PSUBSCRIBE,支持通配符匹配,比如订阅 news.* 可以接收所有以 news. 开头的频道消息。模式订阅在某些广播场景非常有用,但也要注意它会在服务端做模式匹配,订阅模式过多时可能增加 CPU 消耗。
# 客户端1:订阅频道 SUBSCRIBE order_notify # 客户端2:向频道发布消息 PUBLISH order_notify "hello redis"
当执行 PUBLISH 时,Redis 会返回一个整数,表示有多少个订阅者收到了这条消息。如果返回 0,说明当前没有在线订阅者,消息随即被丢弃,不会保留在 Redis 中。这是理解 Pub/Sub 消息队列定位的关键:它只负责投递给当下正在监听的消费者。
二、基于Python和Node.js实现消息队列
在轻量级应用中,可以用一个生产者线程定时发布任务消息,一个或多个消费者进程订阅频道并处理。下面是一段 Python 示例,生产者生成 JSON 格式的订单通知,消费者订阅后解析消息。
import redis
import json
import time
def producer():
client = redis.Redis(host='127.0.0.1', port=6379, decode_responses=True)
for i in range(1, 6):
msg = json.dumps({'order_id': i, 'action': 'created'})
receivers = client.publish('order_notify', msg)
print(f'已发送第{i}条消息,接收者数量:{receivers}')
time.sleep(1)
client.close()
if __name__ == '__main__':
producer()
消费者需要保持长连接,并进入消息循环。Python 的 redis 库提供了 pubsub() 方法来获取消息迭代器,可以逐条读取订阅到的消息,再交给业务函数处理。
import redis
import json
def handle_message(data):
print(f'处理消息:{data}')
def consumer():
client = redis.Redis(host='127.0.0.1', port=6379, decode_responses=True)
pubsub = client.pubsub()
pubsub.subscribe('order_notify')
for item in pubsub.listen():
if item['type'] == 'message':
data = json.loads(item['data'])
handle_message(data)
if __name__ == '__main__':
consumer()
从上面代码可以看出,消费者通过 listen() 阻塞获取消息,收到 message 类型后解析 JSON 并处理。这种实现结构简单,适合内部事件通知。但必须注意,如果消费者在消息发布时还没有启动,那么这条消息就彻底丢了,订阅者无法像读队列一样补读历史数据。
Node.js 方案也类似,使用 ioredis 客户端创建订阅连接,监听 message 事件即可。由于 Node.js 的事件驱动模型天然适合长连接推送,前端通知类服务经常采用这种组合。
const Redis = require('ioredis');
const publisher = new Redis();
const subscriber = new Redis();
subscriber.subscribe('order_notify', (err, count) => {
if (err) console.error(err);
console.log(`已订阅 ${count} 个频道`);
});
subscriber.on('message', (channel, message) => {
console.log(`收到来自 ${channel} 的消息:${message}`);
// 执行具体的业务处理
});
// 模拟生产者发送消息
setInterval(() => {
publisher.publish('order_notify', JSON.stringify({ time: Date.now() }));
}, 2000);
这段代码中的箭头函数使用了 =>,实际运行时需要确保 Node.js 版本支持 ES6。发布端和订阅端建议使用两个独立的 Redis 连接,因为订阅连接进入订阅模式后通常不能再执行其他命令。虽然 Redis 客户端允许复用连接,但混用发布和订阅命令容易造成协议混乱,所以分开连接是更稳妥的做法。
三、Pub/Sub做消息队列的四个关键缺陷
第一是消息无法持久化。Redis Pub/Sub 不把消息写入磁盘或内存队列,一旦发布时没有订阅者在线,消息直接消失。即使有订阅者,消息推送到客户端后也不会保留副本,消费者若处理失败无法重新读取。
第二是没有确认与重试机制。传统消息队列在消费者处理成功后发送 ACK,未确认的消息会重新投递;Pub/Sub 推出去就结束了,Redis 不关心业务是否处理成功。第三是消费者离线期间的消息不可恢复,它不像 Kafka 那样保留一段时间内的消息供消费者重新消费。第四是顺序性只对单连接成立,多个订阅者之间无法保证全局有序。
举一个真实场景:订单服务发布订单创建事件后,通知服务恰好正在进行滚动重启。重启窗口只有几秒钟,但这段时间内发布的事件没有任何订阅者接收,业务就会出现漏发短信或漏更新看板的问题。即使消费者启动后重新订阅,Redis 也不会把之前的消息补给它。这种丢失在高峰期可能被忽略,但一旦涉及资金或库存就会造成明显数据不一致。
这四点决定了 Redis Pub/Sub 不适用于金融交易、订单状态流转、任务调度等对可靠性要求高的场景。更适合实时通知、在线状态广播、聊天消息推送、配置实时下发等允许少量丢失的轻量级事件分发。
四、用Stream或其他方案增强可靠性
如果业务既想用 Redis 又要可靠队列,优先考虑 Redis Stream。Stream 支持消费者组、ACK 确认、消息持久化和阻塞读取,基本具备一个消息队列所需的能力。下面是一个使用 Python 操作 Stream 的简化示例。
import redis
client = redis.Redis(host='127.0.0.1', port=6379, decode_responses=True)
# 生产者写入消息
client.xadd('order_stream', {'order_id': '1001', 'action': 'created'})
# 消费者读取消息
messages = client.xread({'order_stream': '0'}, count=10, block=1000)
for stream, entries in messages:
for entry_id, fields in entries:
print(entry_id, fields)
# 确认处理完成
client.xack('order_stream', 'my_group', entry_id)
Stream 的 xread 只是简单读取,真正可靠消费需要创建消费者组并使用 xreadgroup,这样未确认的消息会保留在 Pending 列表中,消费者崩溃后可以被其他成员接管。这是 Pub/Sub 完全不具备的能力。
除了 Stream,Redis List 配合 BLPOP 或 BRPOP 也能实现简单的阻塞队列,消息会保存在列表中,客户端可以按顺序取出。但 List 不支持消费者组和广播推送,只适合单消费者或手动分发模式。如果消息量较大且需要广播,Pub/Sub 仍有优势。
五、选型建议与总结
综合来看,Redis Pub/Sub 实现消息队列的最大价值是简单和低延迟。它不落盘,不保存历史,推送路径短,适合对实时性要求高、允许丢失的广播类场景。比如聊天室实时消息、在线用户状态广播、配置热更新、日志实时推送等。
而一旦业务要求消息不丢、可重试、可追踪,就要切换到 Redis Stream、RabbitMQ 或 Kafka。Redis Stream 适合性能要求高且希望继续用 Redis 作为数据通道的团队,RabbitMQ 的交换机模型适合复杂路由,Kafka 适合大规模日志和事件流处理。选择时需权衡持久化需求、吞吐量、运维成本和团队熟悉度。
理解 Pub/Sub 的投递模型和边界,比记住几个命令更重要。它本身不是为消息队列设计,但作为轻量级实时消息通道,在正确的场景下依然非常实用。