原文位于Redis官網(wǎng)http://redis.io/topics/twitter-clone
Redis是NoSQL數(shù)據(jù)庫(kù)中一個(gè)知名數(shù)據(jù)庫(kù),在新浪微博中亦有部署,適合固定數(shù)據(jù)量的熱數(shù)據(jù)的訪問(wèn)。
作為入門蚪黑,這是一篇很好的教材,簡(jiǎn)單描述了如何使用KV數(shù)據(jù)庫(kù)進(jìn)行數(shù)據(jù)庫(kù)的設(shè)計(jì)队橙。新的項(xiàng)目www.xiayucha.com亦采用Redis + MySQL進(jìn)行開發(fā)拦坠,考慮Redis文檔比較少连躏,故翻譯了此文。
其他參考資料:
Redis命令參考中文版(Redis Command Reference)
我會(huì)在此文中描述如何使用PHP以及僅使用Redis來(lái)設(shè)計(jì)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Twitter克隆贞滨。
很多編程社區(qū)常認(rèn)為KV儲(chǔ)存是一個(gè)特別的數(shù)據(jù)庫(kù)入热,在web應(yīng)用中不能替代關(guān)系數(shù)據(jù)庫(kù)。
本文嘗試證明這恰恰相反疲迂。
這個(gè)twitter克隆名為Retwis才顿,結(jié)構(gòu)簡(jiǎn)單,性能優(yōu)異尤蒿,能很輕易地用N個(gè)web服務(wù)器和Redis服務(wù)器以分布式架構(gòu)郑气。
在此獲取源碼http://code.google.com/p/redis/downloads/list。
我們使用PHP作為例子是因?yàn)樗鼙幻總€(gè)人讀懂腰池,也能使用Ruby尾组、Python、Erlang或其他語(yǔ)言獲取同樣(或者更佳)的效果示弓。
注意:Retwis-RB是一個(gè)由Daniel Lucraft用Ruby與Sinatra寫的Retwis分支讳侨!
此文全部代碼在本頁(yè)尾部的Git repository鏈接里。
此文以PHP為例奏属,但是Ruby程序員也能檢出其他源碼跨跨。他們很相似。
注意Retwis-J是Retwis的一個(gè)分支囱皿,由Costin Leau以Java和Spring框架寫成勇婴。
源碼能在GitHub找到,并且在springsource.org有綜合的文檔嘱腥。
Key-value 數(shù)據(jù)庫(kù)基礎(chǔ)
KV數(shù)據(jù)的精髓耕渴,是能夠把value儲(chǔ)存在key里,此后該數(shù)據(jù)僅能夠通過(guò)確切的key來(lái)獲取齿兔,無(wú)法搜索一個(gè)值橱脸。
確切的來(lái)講础米,它更像一個(gè)大型HASH/字典,但它是持久化的添诉,比如屁桑,當(dāng)你的程序終止運(yùn)行,數(shù)據(jù)不會(huì)消失栏赴。
比如我們能用SET命令以key foo 來(lái)儲(chǔ)存值 bar
SET foo bar
Redis會(huì)永久儲(chǔ)存我們的數(shù)據(jù)掏颊,所以之后我們可以問(wèn)Redis:“儲(chǔ)存在key foo里的數(shù)據(jù)是什么?”艾帐,Redis會(huì)返回一個(gè)值:bar
GET foo => bar
KV數(shù)據(jù)庫(kù)提供的其他常見操作有:DEL乌叶,用于刪除指定的key和關(guān)聯(lián)的value;
SET-if-not-exists (在Redis上稱為SETNX )僅會(huì)在key不存在的時(shí)候設(shè)置一個(gè)值柒爸;
INCR能夠?qū)χ付ǖ膋ey里儲(chǔ)存的數(shù)字進(jìn)行自增准浴。
SET foo 10
INCR foo => 11
INCR foo => 12
INCR foo => 13
原子操作
目前為止它是相當(dāng)簡(jiǎn)單的,但是INCR有些不同捎稚。設(shè)想一下乐横,為什么要提供這個(gè)操作?畢竟我們自己能用以下簡(jiǎn)單的命令實(shí)現(xiàn)這個(gè)功能:
x = GET foo
x = x + 1
SET foo x
問(wèn)題在于要使上面的操作正常進(jìn)行今野,同時(shí)只能有一個(gè)客戶端操作x的值葡公。看看如果兩臺(tái)電腦同時(shí)操作這個(gè)值會(huì)發(fā)生什么:
x = GET foo (返回10)
y = GET foo (返回10)
x = x + 1 (x現(xiàn)在是11)
y = y + 1 (y現(xiàn)在是11)
SET foo x (foo現(xiàn)在是11)
SET foo y (foo現(xiàn)在是11)
問(wèn)題發(fā)生了条霜!我們?cè)黾恿酥祪纱未呤玻緫?yīng)該從10變成12,現(xiàn)在卻停留在了11宰睡。這是因?yàn)橛肎ET和SET來(lái)實(shí)現(xiàn)INCR不是一個(gè)原子操作(atomic operation)蒲凶。
所以Redis\memcached之類提供了一個(gè)原子的INCR命令,服務(wù)器會(huì)保護(hù)get-increment-set操作拆内,以防止同時(shí)的操作旋圆。
讓Redis與眾不同的是它提供了更多類似INCR的方案,用于解決模型復(fù)雜的問(wèn)題麸恍。
因此你可以不使用任何SQL數(shù)據(jù)庫(kù)灵巧、僅用Redis寫一個(gè)完整的web應(yīng)用,而不至于抓狂抹沪。
超越Ke-Value數(shù)據(jù)庫(kù)
本節(jié)我們會(huì)看到構(gòu)建一個(gè)Twitter克隆所需Redis的功能刻肄。首先需要知道的是,Redis的值不僅僅可以是字符串(String)采够。
Redis的值可以是列表(Lists)也可以是集合(Sets)肄方,在操作更多類型的值時(shí)也是原子的冰垄,所以多方操作同一個(gè)KEY的值也是安全的蹬癌。
讓我們從一個(gè)Lists開始:
LPUSH mylist a (現(xiàn)在mylist含有一個(gè)元素:'a'的list)
LPUSH mylist b (現(xiàn)在mylist含有元素'b,a')
LPUSH mylist c (現(xiàn)在mylist含有'c,b,a')
LPUSH的意思是Left Push权她, 就是把一個(gè)元素加在列表(list)的左邊(或者說(shuō)頭上)。
在PUSH操作之前逝薪,如果mylist這個(gè)鍵(key)不存在隅要,Redis會(huì)自動(dòng)創(chuàng)建一個(gè)空的list。
就像你能想到的一樣董济,同樣有個(gè)RPUSH操作可以把元素加在列表(list)的右邊(尾部)步清。
這對(duì)我們復(fù)制一個(gè)twitter非常有用,例如我們可以把用戶的更新儲(chǔ)存在username:updates里虏肾。
當(dāng)然廓啊,我們也有相應(yīng)的操作來(lái)獲取數(shù)據(jù)或者信息。比如LRANGE返回列表(list)的一個(gè)范圍內(nèi)的元素封豪,或者所有元素
LRANGE mylist 0 1 => c,b
LRANGE使用從零開始的索引(zero-based indexes)谴轮,第一個(gè)元素的索引是0,第二個(gè)是1吹埠,以此類推第步。該命令的參數(shù)是:LRANGE key first-index last-index
參數(shù)last index可以是負(fù)數(shù),具有特殊的意義:-1是列表(list)的最后一個(gè)元素缘琅,-2是倒數(shù)第二個(gè)粘都,以此類推。
所以刷袍,如果要獲取整個(gè)list翩隧,我們能使用以下命令:
LRANGE mylist 0 -1 => c,b,a
其他重要的操作有LLEN,返回列表(list)的長(zhǎng)度呻纹,LTRIM類似于LRANGE鸽心,但不僅僅會(huì)返回指定范圍內(nèi)的元素,而且還會(huì)原子地把列表(list)的值設(shè)置這個(gè)新的值居暖。
我們將會(huì)使用這些list操作顽频,但是注意閱讀Redis文檔來(lái)瀏覽所有redis支持的list操作。
數(shù)據(jù)類型:集合(set)
除了列表(list)太闺,Redis還提供了集合(sets)的支持糯景,是不排序(unsorted)的元素集合。
它能夠添加省骂、刪除蟀淮、檢查元素是否存在,并且獲取兩個(gè)結(jié)合之間的交集钞澳。當(dāng)然它也能請(qǐng)求獲取集合(set)里一個(gè)或者多個(gè)元素怠惶。
幾個(gè)例子可以使概念更為清晰。記自凇:SADD是往集合(set)里添元素策治;SREM是從集合(set)里刪除元素脓魏;SISMEMBER是檢測(cè)一個(gè)元素是否包含在集合里;SINTER用于顯示兩個(gè)集合的交集通惫。
其他操作有茂翔,SCARD用于獲取集合的基數(shù)(集合中元素的數(shù)量);SMEMBERS返回集合中所有的元素
SADD myset a
SADD myset b
SADD myset foo
SADD myset bar
SCARD myset => 4
SMEMBERS myset => bar,a,foo,b
注意SMEMBERS不會(huì)以我們添加的順序返回元素履腋,因?yàn)榧?Sets)是一個(gè)未排序的元素集合珊燎。如果你要儲(chǔ)存順序,最好使用列表(Lists)取而代之遵湖。以下是基于集合的一些操作:
SADD mynewset b
SADD mynewset foo
SADD mynewset hello
SINTER myset mynewset => foo,b
SINTER能夠返回集合之間的交集悔政,但并不僅限于兩個(gè)集合(Sets),你能獲取4個(gè)延旧、5個(gè)甚至1000個(gè)集合(sets)的交集卓箫。
最后,讓我們看下SISMEMBER是如何工作的:
SISMEMBER myset foo => 1
SISMEMBER myset notamember => 0
Okay垄潮,我覺(jué)得我們可以開始coding啦烹卒!
先決條件
如果你還沒(méi)下載,請(qǐng)前往<http: //code.google.com/p/redis/downloads/list>下載Retwis的源碼弯洗。它包含幾個(gè)PHP文件旅急,是個(gè)簡(jiǎn)單的 tar.gz文件。
實(shí)現(xiàn)的非常簡(jiǎn)單牡整,你會(huì)在里面找到PHP客戶端(redis.php)藐吮,用于redis與PHP的交互。該庫(kù)由Ludovico Magnocavallo(http://qix.it/)編寫逃贝,你可以在自己的項(xiàng)目中免費(fèi)使用谣辞。
但如果要更新庫(kù)的版本請(qǐng)下載Redis的發(fā)行版。(注意:現(xiàn)在有更好的PHP庫(kù)了沐扳,請(qǐng)檢查我們的客戶端頁(yè)面<http://redis.io/clients>)
你需要的另一個(gè)東西是正常運(yùn)行的Redis服務(wù)器泥从。僅需要獲取源碼、用make編譯沪摄、用./redis-server就完工了躯嫉,點(diǎn)兒也不須配置就可以在你的電腦上運(yùn)行Retwis。
數(shù)據(jù)結(jié)構(gòu)規(guī)劃
當(dāng)使用關(guān)系數(shù)據(jù)庫(kù)的時(shí)候杨拐,這一步往往是在設(shè)計(jì)數(shù)據(jù)表祈餐、索引的表單里處理。我們沒(méi)有表哄陶,那我們?cè)O(shè)計(jì)什么呢帆阳? 我們需要確認(rèn)物體使用的key以及key采用的類型。
讓我們從用戶這塊開始設(shè)計(jì)屋吨。當(dāng)然了蜒谤,首先需要展示用戶的username, userid, password, followers山宾,自己follow的用戶等。第一個(gè)問(wèn)題是:如何在我們的系統(tǒng)中標(biāo)識(shí)一個(gè)用戶芭逝?
username是個(gè)好主意,因?yàn)樗俏ㄒ坏脑ㄐ亍2贿^(guò)它太大了旬盯,我們想要降低內(nèi)存的使用。如果我們的數(shù)據(jù)庫(kù)是關(guān)系數(shù)據(jù)庫(kù)翎猛,我們能關(guān)聯(lián)唯一ID到每一個(gè)用戶胖翰。每一個(gè)對(duì)用戶的引用都通過(guò)ID來(lái)關(guān)聯(lián)。
做起來(lái)很簡(jiǎn)單切厘,因?yàn)槲覀冇形覀兊脑拥腎NCR命令萨咳!當(dāng)我們創(chuàng)建一個(gè)新用戶,我們假設(shè)這個(gè)用戶叫"antirez":
INCR global:nextUserId => 1000
SET uid:1000:username antirez
SET uid:1000:password p1pp0
我們使用global:nextUserId為鍵(Key)是為了給每個(gè)新用戶分配一個(gè)唯一ID疫稿,然后用這個(gè)唯一ID來(lái)加入其他key培他,以識(shí)別保存用戶的其他數(shù)據(jù)。這就是kv數(shù)據(jù)庫(kù)的設(shè)計(jì)模式!請(qǐng)牢記于心遗座,
除了已經(jīng)定義的KEY舀凛,我們還需要更多的來(lái)完整定義一個(gè)用戶,比如有時(shí)需要通過(guò)用戶名來(lái)獲取用戶ID途蒋,所以我們也需要設(shè)置這么一個(gè)鍵(Key)
SET username:antirez:uid 1000
一開始看上去這樣很奇怪猛遍,但請(qǐng)記住我們只能通過(guò)key來(lái)獲取數(shù)據(jù)!這不可能告訴Redis返回包含某值的Key,這也是我們的強(qiáng)處号坡。
用關(guān)系數(shù)據(jù)庫(kù)方式來(lái)講懊烤,這個(gè)新實(shí)例強(qiáng)迫我們組織數(shù)據(jù),以便于僅使用primary key訪問(wèn)任何數(shù)據(jù)宽堆。
關(guān)注\被關(guān)注與更新
這也是在我們系統(tǒng)中另一個(gè)重要需求.每個(gè)用戶都有follower腌紧,也有follow的用戶.對(duì)此我們有最佳的數(shù)據(jù)結(jié)構(gòu)!那就是.....集合(Sets).那就讓我們?cè)诮Y(jié)構(gòu)中加入兩個(gè)新字段:
uid:1000:followers => Set of uids of all the followers users
uid:1000:following => Set of uids of all the following users
另一個(gè)重要的事情是我們需要有個(gè)地方來(lái)放用戶主頁(yè)上的更新。這個(gè)要以時(shí)間順序排序畜隶,最新的排在舊的前面寄啼。所以,最佳的類型是列表(List)代箭。
基本上每個(gè)更新都會(huì)被LPUSH到該用戶的updates key.多虧了LRANGE墩划,我們能夠?qū)崿F(xiàn)分頁(yè)等功能。請(qǐng)注意更新(updates)和帖子(posts)講的是同一個(gè)東西嗡综,實(shí)際上更新(updates)是有點(diǎn)小的帖子(posts)乙帮。
uid:1000:posts => a List of post ids, every new post is LPUSHed here.
驗(yàn)證
OK,除了驗(yàn)證极景,或多或少我們已經(jīng)有了關(guān)于該用戶的一切東西察净。我們處理驗(yàn)證用一個(gè)簡(jiǎn)單而健壯(魯棒)的辦法:我們不使用PHP的session或者其他類似方式驾茴。
我們的系統(tǒng)必須是能夠在不同不同服務(wù)器上分布式部署的,所以一切狀態(tài)都必須保存在Redis里氢卡。所以我們所需要的一個(gè)保存在已驗(yàn)證用戶cookie里的隨機(jī)字符串锈至。
包含同樣隨機(jī)字符串的一個(gè)key告訴我們用戶的ID。我們需要使用兩個(gè)key來(lái)保證這個(gè)驗(yàn)證機(jī)制的健壯性:
SET uid:1000:auth fea5e81ac8ca77622bed1c2132a021f9
SET auth:fea5e81ac8ca77622bed1c2132a021f9 1000
為了驗(yàn)證一個(gè)用戶译秦,我們需要做一些簡(jiǎn)單的工作(login.php):
* 從登錄表單獲取用戶的用戶名和密碼
* 檢查是否存在一個(gè)鍵 username::uid
* 如果這個(gè)user id存在(假設(shè)1000)
* 檢查 uid:1000:password 是否匹配峡捡,如果不匹配,顯示錯(cuò)誤信息
* 匹配則設(shè)置cookie為字符串"fea5e81ac8ca77622bed1c2132a021f9"(uid:1000:auth的值)
實(shí)例代碼:
PHP代碼
include("retwis.php");
#?Form?sanity?checks
if(!gt("username")?||?!gt("password"))
goback("You?need?to?enter?both?username?and?password?to?login.");
#?The?form?is?OK,?checkifthe?username?is?available
$username=?gt("username");
$password=?gt("password");
$r=?redisLink();
$userid=$r->get("username:$username:id");
if(!$userid)
goback("Wrong?username?or?password");
$realpassword=$r->get("uid:$userid:password");
if($realpassword!=$password)
goback("Wrong?useranme?or?password");
#?Username?/?password?OK,?set?the?cookieandredirect?to?index.php
$authsecret=$r->get("uid:$userid:auth");
setcookie("auth",$authsecret,time()+3600*24*365);
header("Location:?index.php");
每次用戶登錄都會(huì)運(yùn)行筑悴,但我們需要一個(gè)函數(shù)isLoggedIn用于檢驗(yàn)一個(gè)用戶是否已經(jīng)驗(yàn)證们拙。
這些是isLoggedIn的邏輯步驟
* 從用戶獲取cookie里auth的值。如果沒(méi)有cookie阁吝,該用戶未登錄砚婆。我們稱這個(gè)cookie為
* 檢查auth:是否存在,存在則獲取值(例子里是1000)
* 為了再次確認(rèn)突勇,檢查uid:1000:auth是否匹配
* 用戶已驗(yàn)證装盯,在全局變量$User中載入一點(diǎn)信息
也許代碼比描述更短:
PHP代碼
functionisLoggedIn()?{
global$User,$_COOKIE;
if(isset($User))returntrue;
if(isset($_COOKIE['auth']))?{
$r=?redisLink();
$authcookie=$_COOKIE['auth'];
if($userid=$r->get("auth:$authcookie"))?{
if($r->get("uid:$userid:auth")?!=$authcookie)returnfalse;
loadUserInfo($userid);
returntrue;
}
}
returnfalse;
}
functionloadUserInfo($userid)?{
global$User;
$r=?redisLink();
$User['id']?=$userid;
$User['username']?=$r->get("uid:$userid:username");
returntrue;
}
把loadUserInfo作為一個(gè)獨(dú)立函數(shù)對(duì)于我們的應(yīng)用而言有點(diǎn)殺雞用牛刀了,但是對(duì)于復(fù)雜的應(yīng)用而言這是一個(gè)不錯(cuò)的模板甲馋。
作為一個(gè)完整的驗(yàn)證验夯,還剩下logout還沒(méi)實(shí)現(xiàn)。在logout的時(shí)候我們?cè)趺醋瞿兀?/p>
很簡(jiǎn)單摔刁,僅僅改變uid:1000:auth里的隨機(jī)字符串挥转,刪除舊的auth:并增加一個(gè)新的auth:
重要:logout過(guò)程解釋了為什么我們不僅僅查找auth:而是再次檢查了uid:1000:auth。真正的驗(yàn)證字符串是后者共屈,auth:是易變的.
假設(shè)程序中有BUGs或者腳本被意外中斷绑谣,那么就有可能有多個(gè)auth:指向同一個(gè)用戶id。
logout代碼如下:(logout.php)
PHP代碼
include("retwis.php");
if(!isLoggedIn())?{
header("Location:?index.php");
exit;
}
$r=?redisLink();
$newauthsecret=?getrand();
$userid=$User['id'];
$oldauthsecret=$r->get("uid:$userid:auth");
$r->set("uid:$userid:auth",$newauthsecret);
$r->set("auth:$newauthsecret",$userid);
$r->delete("auth:$oldauthsecret");
header("Location:?index.php");
以上是我們所描述過(guò)的拗引,應(yīng)該比較易于理解借宵。
更新(Updates)
更新,或者稱為帖子(posts)的實(shí)現(xiàn)則更為簡(jiǎn)單矾削。為了在數(shù)據(jù)庫(kù)里創(chuàng)建一個(gè)新的帖子壤玫,我們做了以下工作:
INCR global:nextPostId => 10343
SET post:10343 "$owner_id|$time|I'm having fun with Retwis"
就像你看到的一樣,帖子的用戶id和時(shí)間直接儲(chǔ)存在了字符串里哼凯。
在這個(gè)例子中我們不需要根據(jù)時(shí)間或者用戶id來(lái)查找帖子欲间,所以把他們緊湊地?cái)D在一個(gè)post字符串里更佳。
在新建一個(gè)帖子之后断部,我們獲得了帖子的id猎贴。需要LPUSH這個(gè)帖子的id到每一個(gè)follow了作者的用戶里去,當(dāng)然還有作者的帖子列表。
update.php這個(gè)文件展示了這個(gè)工作是如何完成的:
PHP代碼
include("retwis.php");
if(!isLoggedIn()?||?!gt("status"))?{
header("Location:index.php");
exit;
}
$r=?redisLink();
$postid=$r->incr("global:nextPostId");
$status=str_replace("\n","?",gt("status"));
$post=$User['id']."|".time()."|".$status;
$r->set("post:$postid",$post);
$followers=$r->smembers("uid:".$User['id'].":followers");
if($followers===?false)$followers=?Array();
$followers[]?=$User['id'];
foreach($followersas$fid)?{
$r->push("uid:$fid:posts",$postid,false);
}
#?Push?the?post?on?the?timeline,andtrim?the?timeline?to?the
#?newest?1000?elements.
$r->push("global:timeline",$postid,false);
$r->ltrim("global:timeline",0,1000);
header("Location:?index.php");
函數(shù)的核心是foreach她渴。 通過(guò)SMEMBERS獲取當(dāng)前用戶的所有follower达址,然后循環(huán)會(huì)把帖子(post)LPUSH到每一個(gè)用戶的 uid::posts里
注意我們同時(shí)維護(hù)了一個(gè)所有帖子的時(shí)間線。為此我們還需要LPUSH到global:timeline里趁耗。
面對(duì)這個(gè)現(xiàn)實(shí)沉唠,你是否開始覺(jué)得:SQL里面用ORDER BY來(lái)按時(shí)間排序有一點(diǎn)兒奇怪? 我確實(shí)是這么想的。
分頁(yè)
現(xiàn)在很清楚苛败,我們能用LRANGE來(lái)獲取帖子的范圍满葛,并在屏幕上顯示。代碼很簡(jiǎn)單:
PHP代碼
functionshowPost($id)?{
$r=?redisLink();
$postdata=$r->get("post:$id");
if(!$postdata)returnfalse;
$aux=explode("|",$postdata);
$id=$aux[0];
$time=$aux[1];
$username=$r->get("uid:$id:username");
$post=?join(array_splice($aux,2,count($aux)-2),"|");
$elapsed=?strElapsed($time);
$userlink=".urlencode($username)."">".utf8entities($username)."";
echo(''.$userlink.'?'.utf8entities($post)."
");
echo('posted?'.$elapsed.'?ago?via?web
');
returntrue;
}
functionshowUserPosts($userid,$start,$count)?{
$r=?redisLink();
$key=?($userid==?-1)??"global:timeline":"uid:$userid:posts";
$posts=$r->lrange($key,$start,$start+$count);
$c=?0;
foreach($postsas$p)?{
if(showPost($p))$c++;
if($c==$count)break;
}
returncount($posts)?==$count+1;
}
當(dāng)showUserPosts獲取帖子的范圍并傳遞給showPost時(shí)著拭,showPost會(huì)簡(jiǎn)單輸出一篇帖子的HTML代碼纱扭。
Following users 關(guān)注的用戶
如果用戶id 1000 (antirez)想要follow用戶id1000的pippo牍帚,我們做到這個(gè)僅需兩步SADD:
SADD uid:1000:following 1001
SADD uid:1001:followers 1000
再次注意這個(gè)相同的模式:在關(guān)系數(shù)據(jù)庫(kù)里的理論里follow的用戶和被follow的用戶是一張包含類似following_id和follower_id的單獨(dú)數(shù)據(jù)表儡遮。
用查詢你能明確follow和被follow的每一個(gè)用戶。在key-value數(shù)據(jù)里有一點(diǎn)特別暗赶,需要我們分別設(shè)置1000follow了1001并且1001被1000follow的關(guān)系鄙币。
這是需要付出的代價(jià),但是另一方面講蹂随,獲取這些數(shù)據(jù)即簡(jiǎn)單又超快十嘿。并且這些是獨(dú)立的集合,允許我們做一些有趣的事情岳锁,比如使用SINTER獲取兩個(gè)不同用戶的集合绩衷。
這樣我們也許可以在我們的twitter復(fù)制品中加入一個(gè)功能:當(dāng)你訪問(wèn)某個(gè)人的資料頁(yè)時(shí)顯示"你和foobar有34個(gè)共同關(guān)注者"之類的東西。
你能夠在follow.php中找到增加或者刪除following/folloer關(guān)系的代碼激率。它如你所見般平常咳燕。
使它能夠水平分割
親愛(ài)的讀者,如果你看到這里乒躺,你已經(jīng)是一個(gè)英雄了招盲,謝謝你。在講到水平分割之前嘉冒,看看單臺(tái)服務(wù)器的性能是個(gè)不錯(cuò)的主意曹货。
Retwis讓人驚訝地快,沒(méi)有任何緩存讳推。在一臺(tái)非常緩慢和高負(fù)載的服務(wù)器上顶籽,以100個(gè)線程并發(fā)請(qǐng)求100000次進(jìn)行apache基準(zhǔn)測(cè)試,平均占用5ms银觅。
這意味著你可以僅僅使用一臺(tái)linux服務(wù)器接受每天百萬(wàn)用戶的訪問(wèn)蜕衡,并且慢的跟個(gè)傻猴似的,就算用更新的硬件。
雖然慨仿,就算你有一堆用戶久脯,也許也不需要超過(guò)1臺(tái)服務(wù)器來(lái)跑應(yīng)用,但讓我們假設(shè)我們是Twitter镰吆,需要處理海量的訪問(wèn)量呢?該怎么做?
Hashing the key
第一件事是把KEY進(jìn)行hash運(yùn)算并基于hash在不同服務(wù)器上處理請(qǐng)求帘撰。有大量知名的hash算法,例如ruby客戶端自帶的consistent hashing
大致意思是你能把key轉(zhuǎn)換成數(shù)字万皿,并除以你的服務(wù)器數(shù)量
server_id = crc32(key) % number_of_servers
這里還有大量因?yàn)樘砑右慌_(tái)服務(wù)器產(chǎn)生的問(wèn)題摧找,但這僅僅是大致的意思,哪怕使用一個(gè)類似consistent hashing的更好索引算法牢硅,
是不是key就可以分布式訪問(wèn)了呢?所有用戶數(shù)據(jù)都分布在不同的服務(wù)器上蹬耘,沒(méi)有inter-keys使用到(比如SINTER,否則你需要注意要在同一臺(tái)服務(wù)器上進(jìn)行)
這是Redis不像memcached一樣強(qiáng)制指定索引算法的原因减余,需要應(yīng)用來(lái)指定综苔。另外,有幾個(gè)key訪問(wèn)的比較頻繁位岔。
特殊的Keys
比如每次發(fā)布新帖如筛,我們都需要增加global:nextPostId。單臺(tái)服務(wù)器會(huì)有大量增加的請(qǐng)求抒抬。如何修復(fù)這個(gè)問(wèn)題呢?一個(gè)簡(jiǎn)單的辦法是用一臺(tái)專門的服務(wù)器來(lái)處理增加請(qǐng)求杨刨。
除非你有大量的請(qǐng)求,否則矯枉過(guò)正了擦剑。另一個(gè)小技巧是ID并不需要真正地增加妖胀,只要唯一即可。這樣你可以使用長(zhǎng)度為不太可能發(fā)生碰撞的隨機(jī)字符串(除了MD5這樣的大小惠勒,幾乎是不可能)赚抡。
完工,我們成功消除了水平分割帶來(lái)的問(wèn)題捉撮。
另一個(gè)問(wèn)題是global:timeline怕品。這里有個(gè)不是解決辦法的解決辦法,你可以分別保存在不同服務(wù)器上巾遭,并且在需要這些數(shù)據(jù)時(shí)從不同的服務(wù)器上取出來(lái)肉康,或者用一個(gè)key來(lái)進(jìn)行排序。
如果你確實(shí)每秒有這么多帖子灼舍,你能夠再次用一臺(tái)獨(dú)立服務(wù)器專門處理這些請(qǐng)求吼和。請(qǐng)記住,商用硬件的Redis能夠以100000/s的速度寫入數(shù)據(jù)骑素。我猜測(cè)對(duì)于twitter這足夠了炫乓。
請(qǐng)隨意在下面評(píng)論處提問(wèn)以及反饋。