最近忙著用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操作,所以我們需要模擬實現,代碼如下:
<?php
class 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);
}
?>
推拉
以前微博架構中推拉選擇的問題已經被大家討論過很多次了。實際上消息通知系統和微博差不多,也存在推拉選擇的問題,同樣答案也是類似的,那就是應該推拉結合。具體點說:在登陸用戶獲取消息的時候,就是一個拉消息的過程;在把消息發送給登陸用戶的時候,就是一個推消息的過程。
速度
假設要推送一百萬條消息的話,那麼最直白的實現就是不斷的插入,代碼如下:
<?php
for ($msgid = 1; $msgid <= 1000000; $msgid++) {
$redis->sAdd('usr:<USRID>:msg', $msgid);
}
?>
Redis的速度是很快的,但是借助PIPELINE,會更快,代碼如下:
<?php
for ($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 content1
redis> HMSET msg:2 title title2 content content2
redis> HMSET msg:3 title title3 content content3
再把這三條消息發送給某個用戶,其<USRID>是123:
redis> SADD usr:123:msg 1
redis> SADD usr:123:msg 2
redis> SADD usr:123:msg 3
此時如果簡單查詢用戶有哪些消息的話,無疑只能查到一些<MSGID>:
redis> SMEMBERS usr:123:msg
1) "1"
2) "2"
3) "3"
如果還需要用程序根據<MSGID>再來一次查詢無疑有點低效,好在Redis內置的SORT命令可以達到事半功倍的效果,實際上它類似於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的缺點是它只能GET出字符串類型的數據,如果你想要多個數據,就要多次GET:
redis> SORT usr:123:msg GET msg:*->title GET msg:*->content
1) "title1"
2) "content1"
3) "title2"
4) "content2"
5) "title3"
6) "content3"
很多情況下這顯得不夠靈活,好在我們可以采用其他一些方法平衡一下利弊,比如說新加一個字段,冗余保存完整消息的序列化,接著只GET這個字段就OK了。
實際暴露查詢接口的時候,不會使用PHP等程序來封裝,因為那會成倍降低RPS,推薦使用Webdis,它是一個Redis的Web代理,效率沒得說。
…
最近Tumblr發表了一篇類似的文章:Staircar: Redis-powered notifications,介紹了他們使用Redis實現消息通知系統的一些情況,有興趣的不妨一起看看。
==========================================
Web應用中的輕量級消息隊列
原文地址:http://hi.baidu.com/thinkinginlamp/blog/item/27a18202578f3d054bfb511f.html
Web應用中為什麼會需要消息隊列?主要原因是由於在高並發環境下,由於來不及同步處理,請求往往會發生堵塞,比如說,大量的insert,update之類的請求同時到達mysql,直接導致無數的行鎖表鎖,甚至最後請求會堆積過多,從而觸發too many connections錯誤。通過使用消息隊列,我們可以異步處理請求,從而緩解系統的壓力。在Web2.0的時代,高並發的情況越來越常見,從而使消息隊列有成為居家必備的趨勢,相應的也湧現出了很多實現方案,像Twitter以前就使用RabbitMQ實現消息隊列服務,現在又轉而使用Kestrel來實現消息隊列服務,此外還有很多其他的選擇,比如說:ActiveMQ,ZeroMQ等。
上述消息隊列的軟件中,大多為了實現AMQP,STOMP,XMPP之類的協議,變得極其重量級,但在很多Web應用中的實際情況是:我們只是想找到一個緩解高並發請求的解決方案,不需要雜七雜八的功能,一個輕量級的消息隊列實現方式才是我們真正需要的。
第一感覺是能不能使用memcached來實現消息隊列?稍加考慮後就會發現它不合適,因為memcached僅僅支持鍵值方式的操作,沒有排序之類的功能,所以如果要用它來實現消息隊列,則必須自己通過某個鍵來保存數組形式的隊列,不過這樣的話,在操作隊列的時候很容易丟失數據,比如說我們要添加一個消息,則需先取出現有隊列,然後把消息保存到隊列尾部,最後保存隊列,單純使用memcached的話,由於我們無法保證整個過程的原子性,所以當處理若干個並發請求時,各個請求間可能會互相覆蓋,丟失數據就在所難免(新的memcached擴展一定程度上能緩解這個問題)。另外,memcached只是內存鍵值緩存而已,一旦宕機,數據就消失了。
memcacheq的出現解決了上面的問題,它在memcached的基礎上實現了消息隊列,以php客戶端為例:
消息從尾部入棧:memcache_set
消息從頭部出棧:memcache_get
memcacheq依附於memcached之上,所以你可以通過現有的memcached工具來操作它,這無疑是它的一大優勢,但它也有一個很大的缺點,那就是memcacheq本身的開發維護似乎並不活躍,如果遇到問題的話,你很可能需要自己動手解決。
目前看來,我更推薦下面這種解決方案,那就是redis,如果不了解,可以參考我以前的文章,表面上看,redis和memcached差不多,也是鍵值操作,但是redis本身實現了list,相關操作也可以保證是原子的,所以可以很自然的通過list來實現消息隊列:
消息從尾部進隊列:RPUSH
消息從頭部出隊列:LPOP
redis本身雖然是一個新項目,但很有朝氣,開發維護也很活躍,如果你的下一個Web應用裡需要使用輕量級的消息隊列,不妨使用它,順便說一句,redis裡還有set結構,可以用來實現一個高效能的tag系統。
此外,還有不少其他的選擇可供嘗試,比如說MySQL第三方的Q4M引擎,通過擴展SQL語法來操作消息隊列,也是一個不錯的選擇。