鍍金池/ 教程/ 大數(shù)據(jù)/ 使用 Redis 實(shí)現(xiàn) Twitter(下)
使用 Redis 實(shí)現(xiàn) Twitter(上)
集群(下)
使用 Redis 實(shí)現(xiàn) Twitter(下)
使用 Redis 作為 LRU 緩存
高可用(上)
高可用客戶端指引
集群(中)
高可用(下)
持久化
Redis 介紹
集中插入
集群(上)
從入門到精通(上)
從入門到精通(下)
從入門到精通(中)
分片
數(shù)據(jù)類型初探
復(fù)制

使用 Redis 實(shí)現(xiàn) Twitter(下)

把 loadUserInfo 作為一個(gè)單獨(dú)的函數(shù)有點(diǎn)大題小做了,但是在復(fù)雜的程序中這是一個(gè)很好的方法。認(rèn)證中唯一被遺漏的事情就是登出了。我們?cè)趺磥碜龅浅瞿??很?jiǎn)單,我們改變 user:1000 的 auth 字段中的隨機(jī)串,從 auths 哈希中刪除舊的認(rèn)證秘鑰,然后添加一個(gè)新的。

重要:登出的步驟解釋了為什么我們不是僅僅在 auths 哈希中查看認(rèn)證秘鑰以后認(rèn)證用戶,而是雙重檢查 user:1000 的 auth 字段。真正的認(rèn)證字符串是后者,auths 哈希只不過是一個(gè)會(huì)揮發(fā)(volatile)的認(rèn)證字段,或者,如果程序中 bug 或者腳本被中斷,我們會(huì)發(fā)現(xiàn) auths 鍵中有多個(gè)對(duì)應(yīng)同一個(gè)用戶 ID 的入口。登出代碼如下(logout.php):

include("retwis.php");  

if (!isLoggedIn()) {  
    header("Location: index.php");  
    exit;  
}  

$r = redisLink();  
$newauthsecret = getrand();  
$userid = $User['id'];  
$oldauthsecret = $r->hget("user:$userid","auth");  

$r->hset("user:$userid","auth",$newauthsecret);  
$r->hset("auths",$newauthsecret,$userid);  
$r->hdel("auths",$oldauthsecret);  

header("Location: index.php");  

這就是我們所描述的,你需要去理解的。

帖子(Updates)

更新(updates),也就是我們知道的帖子(posts),更加簡(jiǎn)單。為了創(chuàng)建一個(gè)新的帖子我們這么干:

INCR next_post_id => 10343  
HMSET post:10343 user_id $owner_id time $time body "I'm having fun with Retwis"  

如你所見,每篇帖子由有 3 個(gè)字段的哈希組成。帖子擁有者的用戶 ID,帖子撞見時(shí)間,最后是帖子的正文,真正的狀態(tài)消息。

創(chuàng)建一個(gè)帖子后,我們獲取其帖子 ID,LPUSH 其 ID 到帖子作者的每個(gè)粉絲用戶的時(shí)間軸中,當(dāng)然還有作者自己的帖子列表中(每個(gè)人事實(shí)上關(guān)注了他自己)。post.php 文件展示了這一切是怎么執(zhí)行的:

include("retwis.php");  

if (!isLoggedIn() || !gt("status")) {  
    header("Location:index.php");  
    exit;  
}  

$r = redisLink();  
$postid = $r->incr("next_post_id");  
$status = str_replace("\n"," ",gt("status"));  
$r->hmset("post:$postid","user_id",$User['id'],"time",time(),"body",$status);  
$followers = $r->zrange("followers:".$User['id'],0,-1);  
$followers[] = $User['id']; /* Add the post to our own posts too */  

foreach($followers as $fid) {  
    $r->lpush("posts:$fid",$postid);  
}  
# Push the post on the timeline, and trim the timeline to the  
# newest 1000 elements.  
$r->lpush("timeline",$postid);  
$r->ltrim("timeline",0,1000);  

header("Location: index.php");  

函數(shù)的核心是這個(gè) foreach 循環(huán)。我們使用 ZRANGE 獲取當(dāng)前用戶的所有粉絲,然后通過遍歷 LPUSH 帖子到每一位粉絲的時(shí)間軸列表中。

注意,我們也為所有的帖子維護(hù)了一個(gè)全局的時(shí)間軸,這樣我們就可以在 Retwis 首頁輕易的展示每個(gè)人的帖子。這只需要執(zhí)行 LPUSH 到時(shí)間軸列表?;氐浆F(xiàn)實(shí),我們難道沒有開始覺得在 SQL 中使用 ORDER BY 來排序按照時(shí)間順序添加的東西有一點(diǎn)點(diǎn)奇怪嗎?至少我只這么認(rèn)為的。

上面的代碼有個(gè)有意思的地方值得注意:我們對(duì)全局時(shí)間軸執(zhí)行完 LPUSH 操作之后使用了一個(gè)新命令 LTRIM。這是為了裁剪列表到 1000 個(gè)元素。全局時(shí)間軸事實(shí)上只會(huì)用在首頁展示少量帖子,沒有必要獲取全部歷史帖子。

基本上 LTRIM+LPUSH 是 Redis 中創(chuàng)建上限 (capped) 集合的一種方式。

帖子分頁(Paginating)

我們?nèi)绾问褂?LRANGE 來獲取一個(gè)范圍的帖子,展現(xiàn)這些帖子在屏幕上,現(xiàn)在已經(jīng)相當(dāng)清楚了。代碼很簡(jiǎn)單:

function showPost($id) {  
    $r = redisLink();  
    $post = $r->hgetall("post:$id");  
    if (emptyempty($post)) return false;  

    $userid = $post['user_id'];  
    $username = $r->hget("user:$userid","username");  
    $elapsed = strElapsed($post['time']);  
    $userlink = "<a class=\"username\"href=\"profile.php?u=".urlencode($username)."\">".utf8entities($username)."</a>";  

    echo('<div class="post">'.$userlink.' '.utf8entities($post['body'])."<br>");  
    echo('<i>posted '.$elapsed.'ago via web</i></div>');  
    return true;  
}  

function showUserPosts($userid,$start,$count) {  
    $r = redisLink();  
    $key = ($userid == -1) ? "timeline" : "posts:$userid";  
    $posts = $r->lrange($key,$start,$start+$count);  
    $c = 0;  
    foreach($posts as $p) {  
        if (showPost($p)) $c++;  
        if ($c == $count) break;  
    }  
    return count($posts) == $count+1;  
}  

showPost 只是轉(zhuǎn)換和打印一篇 HTML 帖子,showUserPosts 獲取一個(gè)范圍的帖子然后傳遞給 showPost。

注意:如果帖子列表很大的話,LRANGE 比較低效,我們想訪問列表的中間元素,因?yàn)?Redis 列表的背后實(shí)現(xiàn)是鏈表。如果系統(tǒng)設(shè)計(jì)為為幾百萬的項(xiàng)分頁,那最好求助于有序集合。

關(guān)注用戶(Following users)

我們還沒有討論如何創(chuàng)建關(guān)注 / 粉絲關(guān)系,盡管這并不困難。如果 ID 為 1000 的用戶(antirez) 想關(guān)注用戶 ID 為 5000 的用戶(pippo),我們需要同時(shí)創(chuàng)建關(guān)注和被關(guān)注關(guān)系。我們只需要調(diào)用 ZADD:

ZADD following:1000 5000  
ZADD followers:5000 1000  

仔細(xì)關(guān)注一下同一個(gè)模式。理論上,在關(guān)系型數(shù)據(jù)庫中,關(guān)注者列表和粉絲列表會(huì)在同一張表中,使用像 following_id 和 follower_id 這樣的列。你可以使用 SQL 查詢來抽取每個(gè)用戶的關(guān)注者和粉絲。在鍵值數(shù)據(jù)庫中則有一些不同,因?yàn)槲覀冃枰O(shè)置 1000 關(guān)注 5000,同時(shí) 5000 被 1000 關(guān)注的雙重關(guān)系。這是要付出的代價(jià),但是另一方面,訪問數(shù)據(jù)很簡(jiǎn)單并相當(dāng)?shù)目?。將這些作為獨(dú)立的集合可以讓我們做一些有意思的事情。例如,使用 ZINTERSTORE 我們可以獲得兩個(gè)不同用戶的粉絲的交集,于是我們可以給我們的 Twitter 系統(tǒng)增加一個(gè)特性,當(dāng)你訪問某個(gè)人的主頁時(shí),可以很快的告訴你” 你和 Alice 有 34 個(gè)共同粉絲” 這樣類似的事情。

你可以在 follow.php 中找到設(shè)置和刪除關(guān)注/粉絲關(guān)系的代碼。

水平伸縮(horizontally scalable)

親愛的讀者,如果你意識(shí)到了這一點(diǎn)你就已經(jīng)是一個(gè)英雄了。謝謝你。在討論水平伸縮之前有必要查看一下單臺(tái)服務(wù)器的性能。Retwis 相當(dāng)?shù)目?,沒有任何的緩存。在一臺(tái)很慢的過載的服務(wù)器上,apache 的 benchmark 使用 100 個(gè)并發(fā)客戶端發(fā)出 10000 個(gè)請(qǐng)求,測(cè)量出平均 uv 為 5 毫秒。這意味著單臺(tái) Linux 服務(wù)器每天可以服務(wù)數(shù)以百萬計(jì)的用戶,這個(gè)像猴子屁股一樣的慢,想象一下如果用更新的硬件會(huì)是什么結(jié)果。

然而,你不可能永遠(yuǎn)使用單臺(tái)服務(wù)器,如何伸縮一個(gè)鍵值存儲(chǔ)?

Retwis 不執(zhí)行任何多鍵操作,所以伸縮很簡(jiǎn)單:你可以使用客戶端分片,或者類似于 Twemproxy 的分片代理,或者是即將橫空出世的 Redis 集群。

想更多的了解這個(gè)主題請(qǐng)閱讀我們的分片文檔。這里我們想強(qiáng)調(diào)的是,在鍵值存儲(chǔ)系統(tǒng)中,如果你小心設(shè)計(jì),數(shù)據(jù)集是可以拆分到相互獨(dú)立的小的鍵上去。相比較使用語義上更復(fù)雜的數(shù)據(jù)庫系統(tǒng),分布這些鍵到多個(gè)節(jié)點(diǎn)更簡(jiǎn)單直接和可預(yù)見。

上一篇:復(fù)制下一篇:高可用(下)