把 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),也就是我們知道的帖子(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) 集合的一種方式。
我們?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)分頁,那最好求助于有序集合。
我們還沒有討論如何創(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)系的代碼。
親愛的讀者,如果你意識(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ù)見。