Redis 发布订阅(Pub/Sub)
Redis 发布订阅(Pub/Sub)
一、Pub/Sub 是什么
想象一下:订单服务刚创建了一笔订单,库存系统要减库存、通知服务要发短信、分析平台要记日志——如果每个服务之间都直接调 HTTP,当第四个消费者加入时所有上游都得改代码。这种网状调用会随着系统变大而崩溃。
发布订阅(Pub/Sub)翻转了这种模型:
Publisher ──(msg)──> Redis ──(fan-out)──> Subscriber A
──> Subscriber B
──> Subscriber C
核心思想:发布者只管往"频道"扔消息,不关心谁在听。订阅者只管从频道收消息,不关心谁发的。Redis 作为中间人负责路由。
二、Redis Pub/Sub 的三个核心命令
| 命令 | 作用 |
|---|---|
PUBLISH channel message |
向频道发送消息,返回收到消息的订阅者数量 |
SUBSCRIBE channel [channel ...] |
订阅一个或多个频道(精确匹配) |
PSUBSCRIBE pattern |
订阅匹配模式的所有频道(如 order:*) |
一个关键性质:消息不落地。Redis 不存储已发送的消息——如果订阅者掉线了,错过就是错过了。这是 Pub/Sub 和消息队列(RabbitMQ/Kafka)的根本区别。
三、Go 中的订阅与发布
订阅者
// 订阅者开启一个专用长连接,持续接收消息
pubsub := rdb.Subscribe(ctx, "orders:new", "payments:done")
defer pubsub.Close()
// Channel() 返回一个 Go channel,把 Redis 的网络流转换为本地 channel
ch := pubsub.Channel()
// 循环读消息(阻塞直到来消息或 ctx 取消)
for msg := range ch {
fmt.Printf("[%s] %s\n", msg.Channel, msg.Payload)
}
关键细节:Subscribe() 后 Redis 客户端会分配一条专用 TCP 连接,这条连接被锁定在 Pub/Sub 模式,不能再执行普通命令(GET/SET 之类的)。这也是为什么生产环境通常给 Pub/Sub 单独配一个连接池。
发布者
// 发布一个 JSON 消息
type OrderEvent struct {
OrderID string `json:"order_id"`
Amount float64 `json:"amount"`
Status string `json:"status"`
}
event, _ := json.Marshal(OrderEvent{OrderID: "O-1024", Amount: 99.9, Status: "paid"})
n, _ := rdb.Publish(ctx, "orders:new", event).Result()
fmt.Printf("消息触达 %d 个订阅者\n", n)
PUBLISH 的返回值是收到消息的订阅者数量——如果返回 0,说明没人监听,消息丢了。这是 Pub/Sub 的"fire-and-forget"天性。
模式订阅:用通配符监听一批频道
// 订阅所有 order 相关事件:orders:created, orders:cancelled, orders:paid...
pubsub := rdb.PSubscribe(ctx, "orders:*")
ch := pubsub.Channel()
for msg := range ch {
fmt.Printf("[%s → %s] %s\n", msg.Pattern, msg.Channel, msg.Payload)
}
* 匹配一个层级,如 order:* 匹配 order:new 但不匹配 order:item:stock。如果需要多级匹配可以用 order:**(取决于 Redis 版本)。
四、Pub/Sub 的消息可靠性问题
Pub/Sub 设计上是一条"广播水管"——水流过就没了。什么时候会丢消息?
| 场景 | 后果 |
|---|---|
| 订阅者未上线时发布消息 | 消息丢失 |
| 订阅者的 Channel 缓冲区满(处理慢) | 消息积压,Redis 客户端可能主动断连 |
| 网络闪断期间的消息 | 丢失 |
| Redis 宕机重启 | 所有订阅关系 + 未发消息全部消失 |
解决方案:Pub/Sub + Stream 双通道
Pub/Sub 负责实时推送,Stream 负责消息持久化:
Publisher ──PUBLISH──> Redis Pub/Sub ──实时推送──> Subscriber
──XADD────> Redis Stream ──补偿读取──> Subscriber(掉线重连后)
- 在线时:吃 Pub/Sub 的实时推送(低延迟)
- 掉线后:从 Stream 里按消费者组的 LastDeliveredID 往回拉取遗漏的消息
这不是 Redis 内建功能,而是应用层组合拳。
五、Pub/Sub vs 其他消息模式
| 维度 | Pub/Sub | Redis Streams | RabbitMQ | Kafka |
|---|---|---|---|---|
| 消息持久化 | ❌ 不持久化 | ✅ | ✅ | ✅ |
| 消费确认 | ❌ 无 | ✅ XACK | ✅ ACK | ✅ offset commit |
| 回溯重复消费 | ❌ | ✅ | ✅ | ✅ |
| 延迟 | < 1ms | < 1ms | ~ ms 级 | ~ ms 级 |
| 运维复杂度 | 极低 | 低 | 中 | 高 |
| 适用场景 | 实时广播、缓存失效通知、WebSocket 跨节点同步 | 事件日志、消息队列、消费者组 | 企业消息中间件 | 大数据流处理 |
一句话选型:要实时 + 丢几条没事 → Pub/Sub;要可靠 + 不能丢 → Streams。
六、常见应用场景
1. 跨节点缓存失效
多个 Web 节点各自有本地缓存,数据更新后通过 Pub/Sub 广播一个 cache:invalidate:user:42,所有节点同时清掉本地缓存。这是 Redis Pub/Sub 最经典的生产场景。
2. WebSocket 跨节点消息推送
用户连在节点 A 的 WebSocket 上,但触发消息的服务跑在节点 B 上。B 发 Pub/Sub 到 Redis,A 订阅并推给 WebSocket 客户端。Socket.IO 的 Redis Adapter 就是这个原理。
3. 配置热更新
配置中心更新配置后,PUBLISH 一条通知。所有服务订阅了 config:reload,收到后立刻拉取最新配置。
七、本章要点
| 要点 | 一句话 |
|---|---|
| Pub/Sub 是广播,不是队列 | 消息发完就消失,没有重放、没有回执 |
| 一条长连接一个 PubSub | Subscribe 后连接被专用,不要混用 |
PSubscribe 用 * 通配 |
可以监听整批频道,省去逐个 SUBSCRIBE |
| 可靠性靠组合拳 | Pub/Sub 做实时通道,Stream 做补偿兜底 |
| 延迟极低 | 亚毫秒级,适合实时场景 |
核心认知:把 Pub/Sub 当成一个"广播喇叭"而非"消息仓库"来用,就对了。

浙公网安备 33010602011771号