对一些数据量比较少,而又符合发布订阅模型的业务,我们可以尝试使用redis进行实现,而无需一上来就使用消息队列这么重的工具。
发布订阅模型
发布订阅模型如下图所示,3个消费者(consummer)订阅(subscribe)了频道channel-1,当生产者(producer)有内容需要群发给订阅频道channel-1的用户时,只需要将内容发布(publish)到频道channel-1上,订阅了频道channel-1的消费者们就可以被通知到了。
redis实现
注意,一定要先订阅再发布
- 通过SUBSCRIBE 命令订阅一或多个频道,如图订阅了message1、message2频道。
# eg. SUBSCRIBE message1 message2
SUBSCRIBE chanel[chanel...]
- 通过PUBLISH 命令发布消息,如图分别往message1频道和message2频道发送消息。
# eg. PUBLISH message2 "nice to meet you!"
PUBLISH chanel message
源码实现
订阅
5.0版本的源码,关于保存所有频道的订阅关系的指针定义不在pubsub.c文件,而在server.h文件中。
struct redisServer {
/.../
// 一个字典, key为频道channel, value为一个list列表,列表内容为订阅频道的客户端
dict *pubsub_channels; /* Map channels to list of subscribed clients */
// list列表, 存放订阅频道的客户端
list *pubsub_patterns; /* A list of pubsub_patterns */
};
/* Subscribe a client to a channel. Returns 1 if the operation succeeded, or
* 0 if the client was already subscribed to that channel. */
int pubsubSubscribeChannel(client *c, robj *channel) {
dictEntry *de;
list *clients = NULL;
int retval = 0;
/* Add the channel to the client -> channels hash table */
// 先添加一个频道作为key,添加成功会返回DICT_OK值
if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
retval = 1;
incrRefCount(channel);
/* Add the client to the channel -> list of clients hash table */
// 通过key找出列表, 为null则先创建再添加订阅的客户端
de = dictFind(server.pubsub_channels,channel);
if (de == NULL) {
clients = listCreate();
dictAdd(server.pubsub_channels,channel,clients);
incrRefCount(channel);
} else {
clients = dictGetVal(de);
}
// 将客户端添加到末尾
listAddNodeTail(clients,c);
}
以 客户端A、B、C依次订阅message1频道为例:
- A先订阅message1,pubsub_channels 字典中没有这个key,于是先添加它,然后添加订阅者A。
- B,C依次订阅,依次添加到队列末端处。
发布
/* Publish a message */
int pubsubPublishMessage(robj *channel, robj *message) {
int receivers = 0;
dictEntry *de;
listNode *ln;
listIter li;
/* Send to clients listening for that channel */
// 根据key即频道,找出订阅者列表
de = dictFind(server.pubsub_channels,channel);
if (de) {
list *list = dictGetVal(de);
listNode *ln;
listIter li;
listRewind(list,&li);
// 遍历链表发送消息
while ((ln = listNext(&li)) != NULL) {
client *c = ln->value;
addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.messagebulk);
addReplyBulk(c,channel);
addReplyBulk(c,message);
receivers++;
}
}
/.../
// 另一种根据指定模式(类似正则表达式)发送消息的方式, 原理和上面类似
}
其他
PSUBSCRIBE pattern [pattern ...]
订阅一个或多个符合给定模式的频道。PUBSUB subcommand [argument [argument ...]]
查看订阅与发布系统状态。PUNSUBSCRIBE [pattern [pattern ...]]
退订所有给定模式的频道。UNSUBSCRIBE [channel [channel ...]]
指退订给定的频道。
应用场景
- 异步消息/任务通知。多个项目A、B、C、D,B、C、D项目的运营说要实时从A中拿一些数据做分析…
- 商家或用户工单的消息通知
- 参数变更通知
- 低级的聊天室
缺点
- 稳定性。当redis读取消息的速度不够快时,不断的积压的消息就会使得redis输出缓冲区的体积越来越大,这可能会导致redis的速度变慢,甚至奔溃。
- 可靠性。各种容灾情况需要我们自己实现,譬如网络断开了、服务器奔溃了等情况下需要我们自行进行可靠处理。