Redis訊息通知系統的實現

來源:互聯網
上載者:User

最近忙著用Redis實現一個訊息通知系統,今天大概總結了一下技術細節,其中示範代碼如果沒有特殊說明,使用的都是PhpRedis擴充來實現的。

記憶體

比如要推送一條全域訊息,如果真的給所有使用者都推送一遍的話,那麼會佔用很大的記憶體,實際上不管粘性有多高的產品,活躍使用者同全部使用者比起來,都會 小很多,所以如果只處理登入使用者的話,那麼至少在記憶體消耗上是相當划算的,至於未登入使用者,可以延遲到使用者下次登入時再處理,如果使用者一直不登入,就一了 百了了。

隊列

當大量使用者同時登入的時候,如果全部都即時處理,那麼很容易就崩潰了,此時可以使用一個隊列來儲存待處理的登入使用者,如此一來頂多是反應慢點,但不會崩潰。

Redis的LIST資料類型可以很自然的建立一個隊列,代碼如下:

<?php$redis = new Redis;$redis->connect('/tmp/redis.sock');$redis->lPush('usr', <USRID>);while ($usr = $redis->rPop('usr')) {    var_dump($usr);}?>

出於類似的原因,我們還需要一個隊列來儲存待處理的訊息。當然也可以使用LIST來實現,但LIST只能按照插入的先後順序實作類別似FIFO或LIFO形式的隊列,然而訊息實際上是有優先順序的:比如說個人訊息優先順序高,全域訊息優先順序低。此時可以使用ZSET來實現,它裡面分數的概念很自然的實現了優先順序。

不過ZSET沒有原生的POP操作,所以我們需要類比實現,代碼如下:

<?phpclass RedisClient extends Redis{    const POSITION_FIRST = 0;    const POSITION_LAST = -1;    public function zPop($zset)    {        return $this->zsetPop($zset, self::POSITION_FIRST);    }    public function zRevPop($zset)    {        return $this->zsetPop($zset, self::POSITION_LAST);    }    private function zsetPop($zset, $position)    {        $this->watch($zset);        $element = $this->zRange($zset, $position, $position);        if (!isset($element[0])) {            return false;        }        if ($this->multi()->zRem($zset, $element[0])->exec()) {            return $element[0];        }        return $this->zsetPop($zset, $position);    }}?>

類比實現了POP操作後,我們就可以使用ZSET實現隊列了,代碼如下:

<?php$redis = new RedisClient;$redis->connect('/tmp/redis.sock');$redis->zAdd('msg', <PRIORITY>, <MSGID>);while ($msg = $redis->zRevPop('msg')) {    var_dump($msg);}?>
推拉

以前微博架構中推拉選擇的問題已經被大家討論過很多次了。實際上訊息通知系統和微博差不多,也存在推拉選擇的問題,同樣答案也是類似的,那就是應該 推拉結合。具體點說:在登陸使用者擷取訊息的時候,就是一個拉訊息的過程;在把訊息發送給登陸使用者的時候,就是一個推訊息的過程。

速度

假設要推送一百萬條訊息的話,那麼最直白的實現就是不斷的插入,代碼如下:

<?phpfor ($msgid = 1; $msgid <= 1000000; $msgid++) {    $redis->sAdd('usr:<USRID>:msg', $msgid);}?>

說明:這裡我使用了SET資料類型,當然你也可以視需求換成LIST或者ZSET。

Redis的速度是很快的,但是藉助PIPELINE,會更快,代碼如下:

<?phpfor ($i = 1; $i <= 100; $i++) {    $redis->multi(Redis::PIPELINE);    for ($j = 1; $j <= 10000; $j++) {        $msgid = ($i - 1) * 10000 + $j;        $redis->sAdd('usr:<USRID>:msg', $msgid);    }    $redis->exec();}?>

說明:所謂PIPELINE,就是省略了無謂的折返跑,把命令打包給服務端統一處理。

前後兩段代碼在我的測試裡,使用PIPELINE的速度大概是不使用PIPELINE的十倍。

查詢

我們用Redis命令列來示範一下使用者是如何查詢訊息的。

先插入三條訊息,其<MSGID>分別是1,2,3:

redis> HMSET msg:1 title title1 content content1redis> HMSET msg:2 title title2 content content2redis> HMSET msg:3 title title3 content content3

再把這三條訊息發送給某個使用者,其<USRID>是123:

redis> SADD usr:123:msg 1redis> SADD usr:123:msg 2redis> SADD usr:123:msg 3

此時如果簡單查詢使用者有哪些訊息的話,無疑只能查到一些<MSGID>:

redis> SMEMBERS usr:123:msg1) "1"2) "2"3) "3"

如果還需要用程式根據<MSGID>再來一次查詢無疑有點低效,好在Redis內建的SORT命令可以達到事半功倍的效果,實際上它類似於SQL中的JOIN:

redis> SORT usr:123:msg GET msg:*->title1) "title1"2) "title2"3) "title3"redis> SORT usr:123:msg GET msg:*->content1) "content1"2) "content2"3) "content3"

SORT的缺點是它只能GET出字串類型的資料,如果你想要多個資料,就要多次GET:

redis> SORT usr:123:msg GET msg:*->title GET msg:*->content1) "title1"2) "content1"3) "title2"4) "content2"5) "title3"6) "content3"

很多情況下這顯得不夠靈活,好在我們可以採用其他一些方法平衡一下利弊,比如說新加一個欄位,冗餘儲存完整訊息的序列化,接著只GET這個欄位就OK了。

實際暴露查詢介面的時候,不會使用PHP等程式來封裝,因為那會成倍降低RPS,推薦使用Webdis,它是一個Redis的Web代理,效率沒得說。

轉至:http://huoding.com/2012/02/29/146

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.