<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Redis(十):pub/sub 發(fā)布訂閱源碼解析

          共 12155字,需瀏覽 25分鐘

           ·

          2021-03-09 15:14

          走過路過不要錯過

          點擊藍字關注我們


          談到發(fā)布訂閱模式,相信不會陌生,典型的觀察者模式的實現(xiàn)。然而從表面來看,本地實現(xiàn)一個wait/notify通知、register/update調用, 實現(xiàn)一個遠程mq服務, 還有本文說的 pub/sub, 其實道理都差不多。只是,同樣的需求,針對不同的環(huán)境,實現(xiàn)上往往是有天壤之別的。

          所以,我們就來看看 redis 的 pub/sub 是如何實現(xiàn)的吧!

          零、redis發(fā)布訂閱相關概念介紹

          Redis 發(fā)布訂閱(pub/sub)是一種消息通信模式:發(fā)送者(pub)發(fā)送消息,訂閱者(sub)接收消息。Redis 客戶端可以訂閱任意數(shù)量的頻道。

          下圖展示了頻道 channel1,以及訂閱這個頻道的三個客戶端 —— client2 、 client5 和 client1 之間的關系:

           

          當有新消息通過 PUBLISH 命令發(fā)送給頻道 channel1 時, 這個消息就會被發(fā)送給訂閱它的三個客戶端:


          Redis的pub/sub實現(xiàn)中,發(fā)布消息的方式只有一種,但是訂閱消息卻有很多種方式。

          使用場景如: 可以用做簡單消息通信中間件,監(jiān)聽某些事件的變化;

          從官方手冊上查到相關使用方法。

          1> PUBLISH channel message
          功能: 將信息發(fā)送到指定的頻道。
          返回值: 接收到該消息的個數(shù);

          2> SUBSCRIBE channel [channel ...]
          功能: 訂閱給定的一個或多個頻道的信息。
          返回值: 等待消息狀態(tài),客戶端不能再處理其他命令了。除了 SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE, PUNSUBSCRIBE, PING and QUIT commands.

          3> PSUBSCRIBE pattern [pattern ...]
          功能: 訂閱一個或多個符合給定模式的頻道。
          返回值: 等待消息狀態(tài),客戶端不能再處理其他命令了。除了 SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE, PUNSUBSCRIBE, PING and QUIT commands.

          4> PUBSUB subcommand [argument [argument ...]]
          功能: 查看訂閱與發(fā)布系統(tǒng)狀態(tài)。subcomand 有 CHANNELS,NUMSUB,NUMPAT .
          返回值: 
          PUBSUB CHANNELS [pattern] 列舉出所有至少有一個訂閱者的符合表達式的channel(精確訂閱的客戶端,即使用 SUBSCRIBE 進行訂閱的客戶端);
          PUBSUB NUMSUB [channel-1 ... channel-N] 每個要查詢的channel的訂閱數(shù)kv(精確訂閱的客戶端,即使用 SUBSCRIBE 進行訂閱的客戶端);
          PUBSUB NUMPAT 返回使用了 PSUBSCRIBE 訂閱的客戶端總數(shù);
          5> UNSUBSCRIBE [channel [channel ...]]
          功能: 指退訂給定的頻道。
          返回值: 退訂頻道的影響訂閱數(shù),自身未訂閱時,影響數(shù)為0;

          6> PUNSUBSCRIBE [pattern [pattern ...]]
          功能: 退訂所有給定模式的頻道。
          返回值: 退訂頻道的影響訂閱數(shù),自身未訂閱時,影響數(shù)為0;

          以上命令的操作,當使用 redis-cli 時,將受限。使用 SUBSCRIBE/PSUBSCRIBE 訂閱channel后,只能強行退出,不能再接受其他命令。即只有配合各語言實現(xiàn)的sdk,才能連貫完成上面完整的操作。

          一、pub/sub 相關數(shù)據(jù)結構

          pub/sub 相關接口定義如下:

              {"subscribe",subscribeCommand,-2,"rpslt",0,NULL,0,0,0,0,0},    {"unsubscribe",unsubscribeCommand,-1,"rpslt",0,NULL,0,0,0,0,0},    {"psubscribe",psubscribeCommand,-2,"rpslt",0,NULL,0,0,0,0,0},    {"punsubscribe",punsubscribeCommand,-1,"rpslt",0,NULL,0,0,0,0,0},    {"publish",publishCommand,3,"pltrF",0,NULL,0,0,0,0,0},    {"pubsub",pubsubCommand,-2,"pltrR",0,NULL,0,0,0,0,0},

          整個pub/sub使用的數(shù)據(jù)結構,都是之前介紹過的。主要有  dict, list 兩種,針對模式匹配訂閱稍微多了個屬性:

          // 使用 PSUBSCRIBE 訂閱方式,做一層數(shù)據(jù)格式封裝typedef struct pubsubPattern {    client *client;    robj *pattern;} pubsubPattern;

          二、subscribe/psubscribe 訂閱channel實現(xiàn)

          只有先有訂閱者之后,發(fā)布者發(fā)送的消息才會有意義。所以我們先看看訂閱的實現(xiàn):

          // 用法: SUBSCRIBE channel [channel ...]// pubsub.cvoid subscribeCommand(client *c) {    int j;    // n 個channel 的訂閱,循環(huán)調用即可    for (j = 1; j < c->argc; j++)        pubsubSubscribeChannel(c,c->argv[j]);    // 添加pubsub訂閱標識,方便其他地方判斷    c->flags |= CLIENT_PUBSUB;}// 具體的單個 channel 訂閱實現(xiàn)/* 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 */ // step1. 將要訂閱的 channel 添加到各自客戶端的 pubsub_channels 容器中 if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) { retval = 1; incrRefCount(channel); /* Add the client to the channel -> list of clients hash table */ // step2. 將要訂閱的channel 添加到 server.pubsub_channels 中, 方便在publish時判定是否觸發(fā)通知 de = dictFind(server.pubsub_channels,channel); if (de == NULL) { clients = listCreate(); dictAdd(server.pubsub_channels,channel,clients); incrRefCount(channel); } else { clients = dictGetVal(de); } // step3. 將客戶端自身添加到相應的 server.pubsub_channels 對應的隊列中去, 在通知時只需遍歷該隊列即可 listAddNodeTail(clients,c); } /* Notify the client */ // 響應客戶端: // *3 \r\n // $9\r\nsubscribe\r\n // channel // 111(該客戶端總共訂閱的channel數(shù)) addReply(c,shared.mbulkhdr[3]); addReply(c,shared.subscribebulk); addReplyBulk(c,channel); addReplyLongLong(c,clientSubscriptionsCount(c)); return retval;}// 客戶端訂閱的總channel數(shù), 兩種訂閱方式相加/* Return the number of channels + patterns a client is subscribed to. */int clientSubscriptionsCount(client *c) { return dictSize(c->pubsub_channels)+ listLength(c->pubsub_patterns);}

          如上就是單個channel的訂閱方式了,總結如下:

          1. 客戶端自行管理需要訂閱的channel, 放到 c->pubsub_channels 中;
          2. redis使用的一個統(tǒng)一的 server->pubsub_channels dict容器進行管理所有的channel;
          3. 對于多個客戶端訂閱一個channel, redis 使用list進行管理追加;

          整個訂閱過程,其實就是一個注冊的過程,自然復雜不到哪里去。接下來,我們同步來看一下 使用模式訂閱的方式的注冊如何?

          // 用法: PSUBSCRIBE pattern [pattern ...]// pubsub.cvoid psubscribeCommand(client *c) {    int j;    // 同樣是n個channel依次注冊    for (j = 1; j < c->argc; j++)        pubsubSubscribePattern(c,c->argv[j]);    c->flags |= CLIENT_PUBSUB;}// 注冊單個模式匹配的 channel 訂閱/* Subscribe a client to a pattern. Returns 1 if the operation succeeded, or 0 if the client was already subscribed to that pattern. */int pubsubSubscribePattern(client *c, robj *pattern) {    int retval = 0;    // 直接查找對應的 pattern, 沒有則添加    if (listSearchKey(c->pubsub_patterns,pattern) == NULL) {        retval = 1;        pubsubPattern *pat;        listAddNodeTail(c->pubsub_patterns,pattern);        incrRefCount(pattern);        pat = zmalloc(sizeof(*pat));        pat->pattern = getDecodedObject(pattern);        pat->client = c;        listAddNodeTail(server.pubsub_patterns,pat);    }    /* Notify the client */    addReply(c,shared.mbulkhdr[3]);    addReply(c,shared.psubscribebulk);    addReplyBulk(c,pattern);    addReplyLongLong(c,clientSubscriptionsCount(c));    return retval;}

          PSUBSCRIBE 的管理方式與 SUBSCRIBE 的管理方式不一樣,它是直接使用list保存訂閱的模式到 server.pubsub_patterns 中,針對不一樣的模式,使用一個新的pubsubPattern來保存。

          注意:所有客戶端的訂閱管理,server.pubsub_patterns 使用平坦式管理,即相同的模式訂閱,有多少個客戶端,就會有多個元素被添加到 pubsub_patterns 中。(為什么不使用子鏈表的方式進行管理呢???)

          三、publish 發(fā)布消息的實現(xiàn)

          publish 是觸發(fā)subscribe的方式,沒有publish動作,subscribe就會一直在等待中。想來應該不難,消息發(fā)布之后,只要將注冊上來的客戶一個進行消息推送,就實現(xiàn)了相應功能。所以,pub/sub 操作,必然是基于長連接的實現(xiàn)方式,沒毛病。

          redis 的發(fā)布命令如下:

          // 用法: PUBLISH channel message// pubsub.cvoid publishCommand(client *c) {    // 使用 channel+message 進行發(fā)布消息    int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);    // 命令傳播    if (server.cluster_enabled)        clusterPropagatePublish(c->argv[1],c->argv[2]);    else        forceCommandPropagation(c,PROPAGATE_REPL);    addReplyLongLong(c,receivers);}// 發(fā)布一條消息/* 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 */ // 使用 SUBSCRIBE 訂閱的客戶端,直接遍歷相應的 channel集合即可 de = dictFind(server.pubsub_channels,channel); if (de) { list *list = dictGetVal(de); listNode *ln; listIter li; // 依次進行數(shù)據(jù)響應,將消息傳播到訂閱端 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++; } } /* Send to clients listening to matching channels */ // 處理使用 PSUBSCRIBE 訂閱消息的客戶端 // 前面說過, PSUBSCRIBE 在redis使用平坦式管理,所以需要做的模式匹配將會更多 // 也就是說 PSUBSCRIBE 的響應性能也會更差 if (listLength(server.pubsub_patterns)) { listRewind(server.pubsub_patterns,&li); channel = getDecodedObject(channel); while ((ln = listNext(&li)) != NULL) { pubsubPattern *pat = ln->value;
          if (stringmatchlen((char*)pat->pattern->ptr, sdslen(pat->pattern->ptr), (char*)channel->ptr, sdslen(channel->ptr),0)) { addReply(pat->client,shared.mbulkhdr[4]); addReply(pat->client,shared.pmessagebulk); addReplyBulk(pat->client,pat->pattern); addReplyBulk(pat->client,channel); addReplyBulk(pat->client,message); receivers++; } } decrRefCount(channel); } return receivers;}

          可以看到,redis消息的發(fā)布可能比想象中的還要簡單。不過有一點需要注意的是,整個publish的消息并沒有在redis進行存儲操作,也就是說發(fā)布完一個消息之后,就再也找不到蹤跡了。這也是很多消息中間件的實現(xiàn)方式,因為數(shù)據(jù)的保留可能會顯得沒有意義。

          整個發(fā)布消息的過程,其實就是向各個subscriber進行數(shù)據(jù)推送的過程,而這些scriber則是基于長連接客戶端實例,以至于其看起來和本地實現(xiàn)的register/update 的觀察者模塊沒啥兩樣。

          所以,基本上 redis 的發(fā)布訂閱功能實現(xiàn)得,還是實現(xiàn)的粒度還是比較粗的。系統(tǒng)上的應用如哨兵模式下的master/slave的切換。而如果自己應用的話,就需要找準自己的應用場景,不要亂用了。

          四、unsubscribe 解除訂閱關系

          當關注的事件處理完成后,可能就不需要再訂閱相關消息了,就需要進行解決訂閱。解決訂閱關系,即不再接受相應的發(fā)布消息,將自身從注冊表中刪除即可?;旧暇褪呛陀嗛嗊M行一個反解操作!

          // 用法: UNSUBSCRIBE [channel [channel ...]]// pubsub.cvoid unsubscribeCommand(client *c) {    // unsubscribe, 直接解決所有的訂閱    if (c->argc == 1) {        pubsubUnsubscribeAllChannels(c,1);    } else {        int j;        // 根據(jù)指定的 channel 依次解除訂閱關系        for (j = 1; j < c->argc; j++)            pubsubUnsubscribeChannel(c,c->argv[j],1);    }    // 當一個訂閱也沒有,則自身不再處理 pubsub 相關的事務    if (clientSubscriptionsCount(c) == 0) c->flags &= ~CLIENT_PUBSUB;}/* Unsubscribe a client from a channel. Returns 1 if the operation succeeded, or * 0 if the client was not subscribed to the specified channel. */int pubsubUnsubscribeChannel(client *c, robj *channel, int notify) {    dictEntry *de;    list *clients;    listNode *ln;    int retval = 0;
          /* Remove the channel from the client -> channels hash table */ incrRefCount(channel); /* channel may be just a pointer to the same object we have in the hash tables. Protect it... */ // 先刪除自身的訂閱標識,再刪除 server.pubsub_channels 標識 if (dictDelete(c->pubsub_channels,channel) == DICT_OK) { retval = 1; /* Remove the client from the channel -> clients list hash table */ de = dictFind(server.pubsub_channels,channel); serverAssertWithInfo(c,NULL,de != NULL); clients = dictGetVal(de); ln = listSearchKey(clients,c); serverAssertWithInfo(c,NULL,ln != NULL); listDelNode(clients,ln); if (listLength(clients) == 0) { /* Free the list and associated hash entry at all if this was * the latest client, so that it will be possible to abuse * Redis PUBSUB creating millions of channels. */ dictDelete(server.pubsub_channels,channel); } } /* Notify the client */ // 調用 unsubscribe 進行解決訂閱的,此處都需要進行客戶端響應通知 if (notify) { addReply(c,shared.mbulkhdr[3]); addReply(c,shared.unsubscribebulk); addReplyBulk(c,channel); addReplyLongLong(c,dictSize(c->pubsub_channels)+ listLength(c->pubsub_patterns));
          } decrRefCount(channel); /* it is finally safe to release it */ return retval;}// 解決所有的當前客戶端的訂閱關系 (SUBSCRIBE 建立的訂閱)/* Unsubscribe from all the channels. Return the number of channels the * client was subscribed to. */int pubsubUnsubscribeAllChannels(client *c, int notify) { dictIterator *di = dictGetSafeIterator(c->pubsub_channels); dictEntry *de; int count = 0; // 迭代 c->pubsub_channels 的訂閱,依次刪除即可 while((de = dictNext(di)) != NULL) { robj *channel = dictGetKey(de);
          count += pubsubUnsubscribeChannel(c,channel,notify); } /* We were subscribed to nothing? Still reply to the client. */ if (notify && count == 0) { addReply(c,shared.mbulkhdr[3]); addReply(c,shared.unsubscribebulk); addReply(c,shared.nullbulk); addReplyLongLong(c,dictSize(c->pubsub_channels)+ listLength(c->pubsub_patterns)); } dictReleaseIterator(di); return count;}

          不出所料,就是一個從 pubsub_channels 中的刪除一個元素的問題,別無其他。其中需要注意的是,SUBSCRIBE 對應 UNSUBSCRIBE, PSUBSCRIBE 對應 PUNSUBSCRIBE。

          五、關于redis pub/sub 之后的思考

          需要注意的是,消息中間件是遠程通信組件,必然存在各種不確定性,所以確保長連接的有效性是非常重要,比如通過PING-PONG方式進行續(xù)租,以保持連接的有效性。

          可以說,我們要實現(xiàn)一個簡單的pub/sub 功能是簡單的,但是要應對各種異常情況則是困難的。

          1. 比如當訂閱的量越來越大時,整個發(fā)布消息過程可能變量緩慢起來,如何處理?
          2. 如果消費者端處理失敗,如何處理?
          3. 訂閱者為什么只能做很少的事情,能不能多做一點?
          4. 出現(xiàn)問題時如何進行溯源?
          5. 如何處理單機瓶頸問題?
          6. 如果是多機負載,如何處理數(shù)據(jù)一致性問題?
          7. 消費者事務處理能力問題?

          redis是專業(yè)的緩存解決方案,但不是專業(yè)的消息通信解決方案,它的實現(xiàn)只能為打開我們的一點思路。我們還是要相信專業(yè)的力量,以上問題相信在很多消息中間件中很容易找到相應答案。




          往期精彩推薦



          騰訊、阿里、滴滴后臺面試題匯總總結 — (含答案)

          面試:史上最全多線程面試題 !

          最新阿里內推Java后端面試題

          JVM難學?那是因為你沒認真看完這篇文章


          END


          關注作者微信公眾號 —《JAVA爛豬皮》


          了解更多java后端架構知識以及最新面試寶典


          你點的每個好看,我都認真當成了


          看完本文記得給作者點贊+在看哦~~~大家的支持,是作者源源不斷出文的動力


          作者:等你歸去來

          出處:https://www.cnblogs.com/yougewe/p/12349899.html

          瀏覽 77
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天天摸日日摸狠狠添 | 日韩AV无码一区二区三 | 午夜视频福利 | 天堂网av2014 | 国产TS人妖系列高潮 |