📞Redis 发布订阅模型 ------ Pub/Sub 原理、消息队列、聊天系统实战
🎯 引言
嘿,各位小伙伴,欢迎回到老曹的 Redis课堂!今天我们要聊的是一个非常有趣也非常实用的功能------发布订阅模型(Pub/Sub)。如果你觉得 Redis 只能用来做缓存和存储数据,那你可就太小看它的潜力了!
在这节课中,我们将深入探讨 Redis 的 Pub/Sub 模型是如何工作的,如何用它构建轻量级的消息队列,甚至动手做一个简易的聊天室系统。废话不多说,Let's go!
🎯 学习目标
- 理解 Redis Pub/Sub 的基本概念与工作机制
- 掌握
PUBLISH、SUBSCRIBE、PSUBSCRIBE等核心命令 - 实现基于 Redis 的消息队列(Message Queue)
- 动手搭建一个多用户的实时聊天系统
- 探讨 Pub/Sub 在生产环境中的优缺点及替代方案
🔍 一、Redis Pub/Sub 基础知识
1.1 什么是发布订阅模式?
发布订阅是一种消息通信范式,允许发送者(Publisher)将消息广播给多个接收者(Subscriber)。这种模式类似于微信公众号:作者发布一篇文章,所有关注该公众号的人都能收到推送。
在 Redis 中:
- 频道(Channel):相当于"公众号名称"
- 订阅者(Subscriber):即关注者
- 发布者(Publisher):就是那个发文章的人
1.2 核心命令一览
| 命令 | 作用 |
|---|---|
SUBSCRIBE channel [channel...] |
订阅指定频道 |
UNSUBSCRIBE [channel [channel...]] |
取消订阅 |
PUBLISH channel message |
向频道发送消息 |
PSUBSCRIBE pattern [pattern...] |
按模式匹配订阅多个频道 |
PUNSUBSCRIBE [pattern [pattern...]] |
取消按模式订阅 |
📌 示例演示:
bash
# 客户端 A(订阅者)
127.0.0.1:6379> SUBSCRIBE news
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "news"
3) (integer) 1
# 客户端 B(发布者)
127.0.0.1:6379> PUBLISH news "Hello World!"
(integer) 1
# 客户端 A 收到消息
1) "message"
2) "news"
3) "Hello World!"
🧠 二、工作原理解析(Mermaid 流程图)
下面是 Redis Pub/Sub 的内部流转过程:
Yes
No
Publish 发送消息
Redis Server
是否有订阅者?
转发至所有订阅者
丢弃消息
Client 1
Client 2
Client N...
🔍 关键点:
- 消息是即时传递的,不会持久化保存。
- 如果某个客户端断开连接后再重连,默认不会补发之前的消息。
- Redis 使用内部结构维护频道与订阅者的映射关系。
🧪 三、实战项目①:简易消息队列
虽然 Redis Pub/Sub 不适合做严格意义上的消息队列(缺乏确认机制和持久化),但我们依然可以用它快速实现一个 demo 版 MQ。
步骤说明:
Step 1:创建消费者(Consumer)
python
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
def consume_messages(channel):
pubsub = r.pubsub()
pubsub.subscribe(channel)
print(f"Listening on channel '{channel}'...")
for msg in pubsub.listen():
if msg['type'] == 'message':
print(f"[Received]: {msg['data'].decode()}")
consume_messages('tasks')
Step 2:创建生产者(Producer)
python
import redis
import time
r = redis.Redis(host='localhost', port=6379, db=0)
for i in range(5):
task = f"Task #{i+1}"
r.publish('tasks', task)
print(f"[Sent]: {task}")
time.sleep(1)
🚀 运行效果:
shell
[Listening on channel 'tasks'...]
[Sent]: Task #1
[Received]: Task #1
[Sent]: Task #2
[Received]: Task #2
...
🎯 小贴士:
这种方法非常适合临时性的异步通知场景,比如邮件提醒、日志收集等。
💬 四、实战项目②:多人在线聊天室
现在我们升级一下难度,尝试做一个支持多用户实时交流的聊天室系统!
架构设计
- 使用 WebSocket 协议建立长连接
- 后端采用 Python + Flask-SocketIO
- 利用 Redis Pub/Sub 实现实时广播
核心代码片段
服务端(Flask + SocketIO)
python
from flask import Flask, render_template
from flask_socketio import SocketIO, emit, join_room, leave_room
import redis
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)
r = redis.Redis()
@app.route('/')
def index():
return render_template('chat.html')
@socketio.on('join')
def handle_join(data):
username = data['username']
room = data['room']
join_room(room)
emit('status', {'msg': f'{username} has entered the room.'}, room=room)
@socketio.on('send_message')
def handle_send_message(data):
room = data['room']
msg = data['msg']
r.publish(f'room:{room}', msg) # 广播到 Redis 频道
emit('receive_message', {'msg': msg}, room=room)
if __name__ == '__main__':
socketio.run(app, debug=True)
前端页面(HTML + JS)
html
<!-- chat.html -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
<script>
const socket = io();
document.getElementById('joinBtn').onclick = () => {
const username = prompt("Enter your name:");
const room = prompt("Enter room ID:");
socket.emit('join', { username, room });
};
document.getElementById('sendBtn').onclick = () => {
const input = document.getElementById('messageInput');
socket.emit('send_message', {
room: currentRoom,
msg: input.value
});
input.value = '';
};
</script>
💡 效果预览:
当你打开多个浏览器窗口分别输入不同用户名进入同一房间时,就能看到文字实时跳动的效果啦!
⚠️ 五、Pub/Sub 的局限性和改进方向
5.1 主要缺陷
| 缺陷项 | 描述 |
|---|---|
| ❌ 无持久化 | 断线期间的消息丢失 |
| ❌ 无 ACK 机制 | 无法确认消息是否送达 |
| ❌ 内存占用高 | 大量活跃订阅会影响性能 |
5.2 生产环境推荐做法
- 对可靠性要求高的场景 → 使用 RabbitMQ/Kafka
- 需要历史消息追溯 → 自行扩展 Redis,记录每条消息到数据库
- 控制订阅数量 → 添加权限校验和限流措施
📊 六、高频面试题汇总
| 序号 | 问题 | 回答要点 |
|---|---|---|
| Q1 | Redis Pub/Sub 与普通 List 实现消息队列有何区别? | Pub/Sub 是广播式,List 是拉取式 |
| Q2 | 是否可以使用通配符订阅多个频道? | 可以,使用 PSUBSCRIBE 命令 |
| Q3 | 如何优雅地关闭一个订阅连接? | 调用 UNSUBSCRIBE 或直接断开连接 |
| Q4 | 消费速度跟不上生产速度会怎样? | 新消息覆盖旧消息,容易导致积压 |
| Q5 | Redis Pub/Sub 最大支持多少并发订阅? | 理论上受操作系统文件句柄限制 |
| Q6 | 能否在一个客户端同时既是发布者又是订阅者? | 当然可以 |
| Q7 | 消息是否会重复投递? | 不会,除非网络抖动造成重试 |
| Q8 | 如何检测某频道是否存在订阅者? | 使用 PUBSUB NUMSUB 命令 |
| Q9 | Redis 集群环境下 Pub/Sub 行为如何变化? | 所有节点共享全局订阅状态 |
| Q10 | 是否可以在 Lua 脚本中调用 PUBLISH? | 不建议这么做,可能导致阻塞 |
🎉 七、结语 & 下一步预告
今天我们完成了 Redis Pub/Sub 的全面学习,从基础理论到实战演练应有尽有。相信你现在对这个功能已经有了深刻的理解!
下一节我们将聚焦于 Pipeline 技术,教你如何大幅提升 Redis 的吞吐量。记得准时回来哦~