50、Redis发布订阅:PUBLISH、SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE、模式匹配
发布订阅模式,说白了就是「广播」机制。一个消息发出去,所有订阅了该频道的客户端都能收到。我在做实时消息推送系统时,这个功能帮了大忙。
Redis的发布订阅(Pub/Sub)是一种消息通信模式。发送者(Publisher)发送消息,订阅者(Subscriber)接收消息。它们之间不需要直接建立连接,通过频道(Channel)作为中介。
核心概念:发布者只管发,订阅者只管收,双方解耦。你想想看,这在微服务架构里多好用?
5.1 基本命令:PUBLISH 和 SUBSCRIBE
先看最基础的两个命令。PUBLISH用于发布消息,SUBSCRIBE用于订阅频道。
// 订阅者终端1
127.0.0.1:6379> SUBSCRIBE news:sports
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "news:sports"
3) (integer) 1
// 发布者终端2
127.0.0.1:6379> PUBLISH news:sports "湖人队夺冠了!"
(integer) 1
// 订阅者终端1 收到消息
1) "message"
2) "news:sports"
3) "湖人队夺冠了!"
嗯,这里要注意:SUBSCRIBE一旦执行,当前连接就进入「订阅模式」。什么意思?就是你不能在这个连接上再执行其他Redis命令了,只能等消息。我刚开始用的时候,老想着订阅完再查个数据,结果一直报错。
5.2 模式匹配:PSUBSCRIBE
PSUBSCRIBE支持通配符订阅。比如你想订阅所有新闻频道,不用一个个列出来。
// 订阅所有news开头的频道
127.0.0.1:6379> PSUBSCRIBE news:*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "news:*"
3) (integer) 1
// 发布消息到 news:sports
127.0.0.1:6379> PUBLISH news:sports "NBA总决赛开始了"
(integer) 1
// 订阅者收到
1) "pmessage" // 注意这里是pmessage,不是message
2) "news:*" // 匹配的模式
3) "news:sports" // 实际频道
4) "NBA总决赛开始了"
模式匹配支持的通配符:
*:匹配任意多个字符?:匹配一个字符[ab]:匹配a或b
个人经验:我在项目中用PSUBSCRIBE订阅了 order:* 模式,所有订单状态变更都通过不同频道发布。这样新增订单类型时,不需要修改订阅代码。
5.3 取消订阅:UNSUBSCRIBE 和 PUNSUBSCRIBE
有订阅就得有退订。UNSUBSCRIBE退订具体频道,PUNSUBSCRIBE退订模式。
// 退订单个频道
UNSUBSCRIBE news:sports
// 退订所有频道(不指定参数)
UNSUBSCRIBE
// 退订模式
PUNSUBSCRIBE news:*
退订操作在C语言客户端里特别重要。为什么?因为如果你不主动退订,连接一直保持,资源就浪费了。我曾经有个服务,连接数一直涨,查了半天才发现是订阅后忘了退订。
5.4 C语言实战:完整示例
下面我用hiredis库写一个完整的发布订阅示例。hiredis是Redis官方推荐的C语言客户端。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <hiredis/hiredis.h>
#include <hiredis/async.h>
#include <hiredis/adapters/libevent.h>
// 订阅回调函数
void subscribe_callback(redisAsyncContext *c, void *reply, void *privdata) {
redisReply *r = reply;
if (r == NULL) return;
// 判断消息类型
if (r->type == REDIS_REPLY_ARRAY) {
// 订阅确认消息
if (strcmp(r->element[0]->str, "subscribe") == 0) {
printf("成功订阅频道: %s\n", r->element[1]->str);
}
// 普通消息
else if (strcmp(r->element[0]->str, "message") == 0) {
printf("[频道: %s] 消息: %s\n",
r->element[1]->str, r->element[2]->str);
}
// 模式匹配消息
else if (strcmp(r->element[0]->str, "pmessage") == 0) {
printf("[模式: %s] [频道: %s] 消息: %s\n",
r->element[1]->str, r->element[2]->str, r->element[3]->str);
}
}
}
// 发布消息函数
void publish_message(redisContext *c, const char *channel, const char *msg) {
redisReply *reply = redisCommand(c, "PUBLISH %s %s", channel, msg);
if (reply == NULL) {
printf("发布失败: %s\n", c->errstr);
return;
}
printf("消息已发布,%d个订阅者收到\n", reply->integer);
freeReplyObject(reply);
}
int main() {
// 创建同步连接用于发布
redisContext *pub_ctx = redisConnect("127.0.0.1", 6379);
if (pub_ctx == NULL || pub_ctx->err) {
printf("连接Redis失败\n");
return 1;
}
// 创建事件库和异步连接用于订阅
struct event_base *base = event_base_new();
redisAsyncContext *sub_ctx = redisAsyncConnect("127.0.0.1", 6379);
if (sub_ctx->err) {
printf("异步连接失败: %s\n", sub_ctx->errstr);
return 1;
}
// 关联事件库
redisLibeventAttach(sub_ctx, base);
// 设置回调
redisAsyncSetConnectCallback(sub_ctx, NULL);
redisAsyncSetDisconnectCallback(sub_ctx, NULL);
// 订阅频道和模式
redisAsyncCommand(sub_ctx, subscribe_callback, NULL,
"SUBSCRIBE news:sports news:tech");
redisAsyncCommand(sub_ctx, subscribe_callback, NULL,
"PSUBSCRIBE alert:*");
printf("订阅成功,等待消息...\n");
// 模拟发布消息
sleep(1);
publish_message(pub_ctx, "news:sports", "NBA总决赛湖人胜");
publish_message(pub_ctx, "news:tech", "Redis 7.0发布");
publish_message(pub_ctx, "alert:system", "CPU使用率超过90%");
// 运行事件循环
event_base_dispatch(base);
// 清理
redisFree(pub_ctx);
redisAsyncDisconnect(sub_ctx);
return 0;
}
避坑指南:我曾经在生产环境遇到一个问题——订阅者断线重连后,丢失了断线期间的消息。Redis的Pub/Sub不持久化消息,这是设计如此。如果你需要消息持久化,考虑用Redis Stream或者消息队列。
5.5 发布订阅模式流程图
5.6 实际应用场景
我在几个项目中用过Redis Pub/Sub,分享几个典型场景:
| 场景 | 频道设计 | 说明 |
|---|---|---|
| 实时通知 | notify:user:{id} | 用户登录、订单状态变更等实时推送 |
| 日志收集 | log:{level} | 不同级别日志分发到不同消费者 |
| 配置更新 | config:reload | 配置变更时通知所有服务重新加载 |
| 监控告警 | alert:{type} | 系统指标异常时触发告警 |
我的建议:频道命名要有层次结构,用冒号分隔。比如 order:create、order:pay,这样用PSUBSCRIBE order:* 就能订阅所有订单相关消息。
5.7 注意事项与最佳实践
- 消息不持久化:订阅者断开期间的消息会丢失。需要持久化?用Redis Stream。
- 订阅者数量:每个订阅者独占一个连接。大量订阅者会消耗连接资源。
- 消息积压:如果订阅者处理慢,消息会在Redis缓冲区积压,可能导致内存暴涨。
- 网络分区:Redis Pub/Sub没有确认机制,网络问题可能导致消息丢失。
我曾经踩过的坑:有个项目用Pub/Sub做订单状态同步,结果网络抖动导致丢消息,订单状态不一致。后来改成Redis Stream + 消费者组,才解决了问题。Pub/Sub适合实时性要求高、允许少量丢消息的场景。
Redis发布订阅模式,说白了就是「发广播」。简单、高效,但也要清楚它的局限性。选对场景,它就是一把利器。
公众号:蓝海资料掘金营,微信deep3321