Redis消息通知系统的实现
Redis消息通知系統(tǒng)的實(shí)現(xiàn)
Posted on by 老王 http://huoding.com/2012/02/29/146最近忙著用Redis實(shí)現(xiàn)一個(gè)消息通知系統(tǒng),今天大概總結(jié)了一下技術(shù)細(xì)節(jié),其中演示代碼如果沒有特殊說明,使用的都是PhpRedis擴(kuò)展來實(shí)現(xiàn)的。
內(nèi)存
比如要推送一條全局消息,如果真的給所有用戶都推送一遍的話,那么會(huì)占用很大的內(nèi)存,實(shí)際上不管粘性有多高的產(chǎn)品,活躍用戶同全部用戶比起來,都會(huì)小很多,所以如果只處理登錄用戶的話,那么至少在內(nèi)存消耗上是相當(dāng)劃算的,至于未登錄用戶,可以推遲到用戶下次登錄時(shí)再處理,如果用戶一直不登錄,就一了百了了。
隊(duì)列
當(dāng)大量用戶同時(shí)登錄的時(shí)候,如果全部都即時(shí)處理,那么很容易就崩潰了,此時(shí)可以使用一個(gè)隊(duì)列來保存待處理的登錄用戶,如此一來頂多是反應(yīng)慢點(diǎn),但不會(huì)崩潰。
Redis的LIST數(shù)據(jù)類型可以很自然的創(chuàng)建一個(gè)隊(duì)列,代碼如下:
<?php$redis = new Redis;
$redis->connect('/tmp/redis.sock');$redis->lPush('usr', <USRID>);while ($usr = $redis->rPop('usr')) {var_dump($usr);
}?>出于類似的原因,我們還需要一個(gè)隊(duì)列來保存待處理的消息。當(dāng)然也可以使用LIST來實(shí)現(xiàn),但LIST只能按照插入的先后順序?qū)崿F(xiàn)類似FIFO或LIFO形式的隊(duì)列,然而消息實(shí)際上是有優(yōu)先級(jí)的:比如說個(gè)人消息優(yōu)先級(jí)高,全局消息優(yōu)先級(jí)低。此時(shí)可以使用ZSET來實(shí)現(xiàn),它里面分?jǐn)?shù)的概念很自然的實(shí)現(xiàn)了優(yōu)先級(jí)。
不過ZSET沒有原生的POP操作,所以我們需要模擬實(shí)現(xiàn),代碼如下:
<?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);}
}?>模擬實(shí)現(xiàn)了POP操作后,我們就可以使用ZSET實(shí)現(xiàn)隊(duì)列了,代碼如下:
<?php$redis = new RedisClient;
$redis->connect('/tmp/redis.sock');$redis->zAdd('msg', <PRIORITY>, <MSGID>);while ($msg = $redis->zRevPop('msg')) {var_dump($msg);
}?>推拉
以前微博架構(gòu)中推拉選擇的問題已經(jīng)被大家討論過很多次了。實(shí)際上消息通知系統(tǒng)和微博差不多,也存在推拉選擇的問題,同樣答案也是類似的,那就是應(yīng)該推拉結(jié)合。具體點(diǎn)說:在登陸用戶獲取消息的時(shí)候,就是一個(gè)拉消息的過程;在把消息發(fā)送給登陸用戶的時(shí)候,就是一個(gè)推消息的過程。
速度
假設(shè)要推送一百萬條消息的話,那么最直白的實(shí)現(xiàn)就是不斷的插入,代碼如下:
<?phpfor ($msgid = 1; $msgid <= 1000000; $msgid++) {$redis->sAdd('usr:<USRID>:msg', $msgid);
}?>Redis的速度是很快的,但是借助PIPELINE,會(huì)更快,代碼如下:
<?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,就是省略了無謂的折返跑,把命令打包給服務(wù)端統(tǒng)一處理。
前后兩段代碼在我的測(cè)試?yán)?#xff0c;使用PIPELINE的速度大概是不使用PIPELINE的十倍。
查詢
我們用Redis命令行來演示一下用戶是如何查詢消息的。
先插入三條消息,其<MSGID>分別是1,2,3:
redis> HMSET msg:1 title title1 content content1 redis> HMSET msg:2 title title2 content content2 redis> HMSET msg:3 title title3 content content3
再把這三條消息發(fā)送給某個(gè)用戶,其<USRID>是123:
redis> SADD usr:123:msg 1 redis> SADD usr:123:msg 2 redis> SADD usr:123:msg 3
此時(shí)如果簡(jiǎn)單查詢用戶有哪些消息的話,無疑只能查到一些<MSGID>:
redis> SMEMBERS usr:123:msg 1) "1" 2) "2" 3) "3"
如果還需要用程序根據(jù)<MSGID>再來一次查詢無疑有點(diǎn)低效,好在Redis內(nèi)置的SORT命令可以達(dá)到事半功倍的效果,實(shí)際上它類似于SQL中的JOIN:
redis> SORT usr:123:msg GET msg:*->title 1) "title1" 2) "title2" 3) "title3" redis> SORT usr:123:msg GET msg:*->content 1) "content1" 2) "content2" 3) "content3"
SORT的缺點(diǎn)是它只能GET出字符串類型的數(shù)據(jù),如果你想要多個(gè)數(shù)據(jù),就要多次GET:
redis> SORT usr:123:msg GET msg:*->title GET msg:*->content 1) "title1" 2) "content1" 3) "title2" 4) "content2" 5) "title3" 6) "content3"
很多情況下這顯得不夠靈活,好在我們可以采用其他一些方法平衡一下利弊,比如說新加一個(gè)字段,冗余保存完整消息的序列化,接著只GET這個(gè)字段就OK了。
實(shí)際暴露查詢接口的時(shí)候,不會(huì)使用PHP等程序來封裝,因?yàn)槟菚?huì)成倍降低RPS,推薦使用Webdis,它是一個(gè)Redis的Web代理,效率沒得說。
…
最近Tumblr發(fā)表了一篇類似的文章:Staircar: Redis-powered notifications,介紹了他們使用Redis實(shí)現(xiàn)消息通知系統(tǒng)的一些情況,有興趣的不妨一起看看。
========================================== Web應(yīng)用中的輕量級(jí)消息隊(duì)列 原文地址:http://hi.baidu.com/thinkinginlamp/blog/item/27a18202578f3d054bfb511f.html Web應(yīng)用中為什么會(huì)需要消息隊(duì)列?主要原因是由于在高并發(fā)環(huán)境下,由于來不及同步處理,請(qǐng)求往往會(huì)發(fā)生堵塞,比如說,大量的insert,update之類的請(qǐng)求同時(shí)到達(dá)mysql,直接導(dǎo)致無數(shù)的行鎖表鎖,甚至最后請(qǐng)求會(huì)堆積過多,從而觸發(fā)too many connections錯(cuò)誤。通過使用消息隊(duì)列,我們可以異步處理請(qǐng)求,從而緩解系統(tǒng)的壓力。在Web2.0的時(shí)代,高并發(fā)的情況越來越常見,從而使消息隊(duì)列有成為居家必備的趨勢(shì),相應(yīng)的也涌現(xiàn)出了很多實(shí)現(xiàn)方案,像Twitter以前就使用RabbitMQ實(shí)現(xiàn)消息隊(duì)列服務(wù),現(xiàn)在又轉(zhuǎn)而使用Kestrel來實(shí)現(xiàn)消息隊(duì)列服務(wù),此外還有很多其他的選擇,比如說:ActiveMQ,ZeroMQ等。上述消息隊(duì)列的軟件中,大多為了實(shí)現(xiàn)AMQP,STOMP,XMPP之類的協(xié)議,變得極其重量級(jí),但在很多Web應(yīng)用中的實(shí)際情況是:我們只是想找到一個(gè)緩解高并發(fā)請(qǐng)求的解決方案,不需要雜七雜八的功能,一個(gè)輕量級(jí)的消息隊(duì)列實(shí)現(xiàn)方式才是我們真正需要的。
第一感覺是能不能使用memcached來實(shí)現(xiàn)消息隊(duì)列?稍加考慮后就會(huì)發(fā)現(xiàn)它不合適,因?yàn)閙emcached僅僅支持鍵值方式的操作,沒有排序之類的功能,所以如果要用它來實(shí)現(xiàn)消息隊(duì)列,則必須自己通過某個(gè)鍵來保存數(shù)組形式的隊(duì)列,不過這樣的話,在操作隊(duì)列的時(shí)候很容易丟失數(shù)據(jù),比如說我們要添加一個(gè)消息,則需先取出現(xiàn)有隊(duì)列,然后把消息保存到隊(duì)列尾部,最后保存隊(duì)列,單純使用memcached的話,由于我們無法保證整個(gè)過程的原子性,所以當(dāng)處理若干個(gè)并發(fā)請(qǐng)求時(shí),各個(gè)請(qǐng)求間可能會(huì)互相覆蓋,丟失數(shù)據(jù)就在所難免(新的memcached擴(kuò)展一定程度上能緩解這個(gè)問題)。另外,memcached只是內(nèi)存鍵值緩存而已,一旦宕機(jī),數(shù)據(jù)就消失了。
memcacheq的出現(xiàn)解決了上面的問題,它在memcached的基礎(chǔ)上實(shí)現(xiàn)了消息隊(duì)列,以php客戶端為例:
消息從尾部入棧:memcache_set
消息從頭部出棧:memcache_get
memcacheq依附于memcached之上,所以你可以通過現(xiàn)有的memcached工具來操作它,這無疑是它的一大優(yōu)勢(shì),但它也有一個(gè)很大的缺點(diǎn),那就是memcacheq本身的開發(fā)維護(hù)似乎并不活躍,如果遇到問題的話,你很可能需要自己動(dòng)手解決。
目前看來,我更推薦下面這種解決方案,那就是redis,如果不了解,可以參考我以前的文章,表面上看,redis和memcached差不多,也是鍵值操作,但是redis本身實(shí)現(xiàn)了list,相關(guān)操作也可以保證是原子的,所以可以很自然的通過list來實(shí)現(xiàn)消息隊(duì)列:
消息從尾部進(jìn)隊(duì)列:RPUSH
消息從頭部出隊(duì)列:LPOP
redis本身雖然是一個(gè)新項(xiàng)目,但很有朝氣,開發(fā)維護(hù)也很活躍,如果你的下一個(gè)Web應(yīng)用里需要使用輕量級(jí)的消息隊(duì)列,不妨使用它,順便說一句,redis里還有set結(jié)構(gòu),可以用來實(shí)現(xiàn)一個(gè)高效能的tag系統(tǒng)。
此外,還有不少其他的選擇可供嘗試,比如說MySQL第三方的Q4M引擎,通過擴(kuò)展SQL語(yǔ)法來操作消息隊(duì)列,也是一個(gè)不錯(cuò)的選擇。
總結(jié)
以上是生活随笔為你收集整理的Redis消息通知系统的实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。