Redis发布订阅模型及源码

对一些数据量比较少,而又符合发布订阅模型的业务,我们可以尝试使用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

在这里插入图片描述

源码实现

订阅

redis5.0 github 源码

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的速度变慢,甚至奔溃。
  • 可靠性。各种容灾情况需要我们自己实现,譬如网络断开了、服务器奔溃了等情况下需要我们自行进行可靠处理。

猜你喜欢

转载自blog.csdn.net/legendaryhaha/article/details/112615274