日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > php >内容正文

php

PHP + Redis 实现一个简单的twitter

發布時間:2024/1/17 php 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 PHP + Redis 实现一个简单的twitter 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

原文位于Redis官網http://redis.io/topics/twitter-clone

Redis是NoSQL數據庫中一個知名數據庫,在新浪微博中亦有部署,適合固定數據量的熱數據的訪問。

作為入門,這是一篇很好的教材,簡單描述了如何使用KV數據庫進行數據庫的設計。新的項目www.xiayucha.com亦采用Redis + MySQL進行開發,考慮Redis文檔比較少,故翻譯了此文。

其他參考資料:

  • Redis命令參考中文版(Redis Command Reference)
  • Try Redis

?

我會在此文中描述如何使用PHP以及僅使用Redis來設計實現一個簡單的Twitter克隆。
很多編程社區常認為KV儲存是一個特別的數據庫,在web應用中不能替代關系數據庫。
本文嘗試證明這恰恰相反。

這個twitter克隆名為Retwis,結構簡單,性能優異,能很輕易地用N個web服務器和Redis服務器以分布式架構。
在此獲取源碼http://code.google.com/p/redis/downloads/list。
我們使用PHP作為例子是因為它能被每個人讀懂,也能使用Ruby、Python、Erlang或其他語言獲取同樣(或者更佳)的效果。

注意:Retwis-RB是一個由Daniel Lucraft用Ruby與Sinatra寫的Retwis分支!
此文全部代碼在本頁尾部的Git repository鏈接里。
此文以PHP為例,但是Ruby程序員也能檢出其他源碼。他們很相似。

注意Retwis-J是Retwis的一個分支,由Costin Leau以Java和Spring框架寫成。
源碼能在GitHub找到,并且在springsource.org有綜合的文檔。


Key-value 數據庫基礎

KV數據的精髓,是能夠把value儲存在key里,此后該數據僅能夠通過確切的key來獲取,無法搜索一個值。
確切的來講,它更像一個大型HASH/字典,但它是持久化的,比如,當你的程序終止運行,數據不會消失。
比如我們能用SET命令以key foo 來儲存值 bar
?SET foo bar
Redis會永久儲存我們的數據,所以之后我們可以問Redis:“儲存在key foo里的數據是什么?”,Redis會返回一個值:bar
?GET foo => bar
KV數據庫提供的其他常見操作有:DEL,用于刪除指定的key和關聯的value;
SET-if-not-exists (在Redis上稱為SETNX )僅會在key不存在的時候設置一個值;
INCR能夠對指定的key里儲存的數字進行自增。
?SET foo 10
?INCR foo => 11
?INCR foo => 12
?INCR foo => 13

原子操作
目前為止它是相當簡單的,但是INCR有些不同。設想一下,為什么要提供這個操作?畢竟我們自己能用以下簡單的命令實現這個功能:
?x = GET foo
?x = x + 1
?SET foo x
問題在于要使上面的操作正常進行,同時只能有一個客戶端操作x的值。看看如果兩臺電腦同時操作這個值會發生什么:
?x = GET foo (返回10)
?y = GET foo (返回10)
?x = x + 1 (x現在是11)
?y = y + 1 (y現在是11)
?SET foo x (foo現在是11)
?SET foo y (foo現在是11)
問題發生了!我們增加了值兩次,本應該從10變成12,現在卻停留在了11。這是因為用GET和SET來實現INCR不是一個原子操作(atomic operation)。
所以Redis\memcached之類提供了一個原子的INCR命令,服務器會保護get-increment-set操作,以防止同時的操作。
讓Redis與眾不同的是它提供了更多類似INCR的方案,用于解決模型復雜的問題。
因此你可以不使用任何SQL數據庫、僅用Redis寫一個完整的web應用,而不至于抓狂。


超越Ke-Value數據庫
本節我們會看到構建一個Twitter克隆所需Redis的功能。首先需要知道的是,Redis的值不僅僅可以是字符串(String)。
Redis的值可以是列表(Lists)也可以是集合(Sets),在操作更多類型的值時也是原子的,所以多方操作同一個KEY的值也是安全的。
讓我們從一個Lists開始:
?LPUSH mylist a (現在mylist含有一個元素:'a'的list)
?LPUSH mylist b (現在mylist含有元素'b,a')
?LPUSH mylist c (現在mylist含有'c,b,a')
LPUSH的意思是Left Push, 就是把一個元素加在列表(list)的左邊(或者說頭上)。
在PUSH操作之前,如果mylist這個鍵(key)不存在,Redis會自動創建一個空的list。
就像你能想到的一樣,同樣有個RPUSH操作可以把元素加在列表(list)的右邊(尾部)。
這對我們復制一個twitter非常有用,例如我們可以把用戶的更新儲存在username:updates里。
當然,我們也有相應的操作來獲取數據或者信息。比如LRANGE返回列表(list)的一個范圍內的元素,或者所有元素
?LRANGE mylist 0 1 => c,b
LRANGE使用從零開始的索引(zero-based indexes),第一個元素的索引是0,第二個是1,以此類推。該命令的參數是:LRANGE key first-index last-index
參數last index可以是負數,具有特殊的意義:-1是列表(list)的最后一個元素,-2是倒數第二個,以此類推。
所以,如果要獲取整個list,我們能使用以下命令:
?LRANGE mylist 0 -1 => c,b,a
其他重要的操作有LLEN,返回列表(list)的長度,LTRIM類似于LRANGE,但不僅僅會返回指定范圍內的元素,而且還會原子地把列表(list)的值設置這個新的值。
我們將會使用這些list操作,但是注意閱讀Redis文檔來瀏覽所有redis支持的list操作。


數據類型:集合(set)
除了列表(list),Redis還提供了集合(sets)的支持,是不排序(unsorted)的元素集合。
它能夠添加、刪除、檢查元素是否存在,并且獲取兩個結合之間的交集。當然它也能請求獲取集合(set)里一個或者多個元素。
幾個例子可以使概念更為清晰。記住:SADD是往集合(set)里添元素;SREM是從集合(set)里刪除元素;SISMEMBER是檢測一個元素是否包含在集合里;SINTER用于顯示兩個集合的交集。
其他操作有,SCARD用于獲取集合的基數(集合中元素的數量);SMEMBERS返回集合中所有的元素
?SADD myset a
?SADD myset b
?SADD myset foo
?SADD myset bar
?SCARD myset => 4
?SMEMBERS myset => bar,a,foo,b
注意SMEMBERS不會以我們添加的順序返回元素,因為集合(Sets)是一個未排序的元素集合。如果你要儲存順序,最好使用列表(Lists)取而代之。以下是基于集合的一些操作:
?SADD mynewset b
?SADD mynewset foo
?SADD mynewset hello
?SINTER myset mynewset => foo,b
SINTER能夠返回集合之間的交集,但并不僅限于兩個集合(Sets),你能獲取4個、5個甚至1000個集合(sets)的交集。
最后,讓我們看下SISMEMBER是如何工作的:
?SISMEMBER myset foo => 1
?SISMEMBER myset notamember => 0
Okay,我覺得我們可以開始coding啦!


先決條件
如果你還沒下載,請前往<<a href="http://code.google.com/p/redis/downloads/list">http: //code.google.com/p/redis/downloads/list>下載Retwis的源碼。它包含幾個PHP文件,是個簡單的 tar.gz文件。
實現的非常簡單,你會在里面找到PHP客戶端(redis.php),用于redis與PHP的交互。該庫由Ludovico Magnocavallo(http://qix.it/ )編寫,你可以在自己的項目中免費使用。
但如果要更新庫的版本請下載Redis的發行版。(注意:現在有更好的PHP庫了,請檢查我們的客戶端頁面<<a href="http://redis.io/clients">http://redis.io/clients>)
你需要的另一個東西是正常運行的Redis服務器。僅需要獲取源碼、用make編譯、用./redis-server就完工了,點兒也不須配置就可以在你的電腦上運行Retwis。

?

數據結構規劃
當使用關系數據庫的時候,這一步往往是在設計數據表、索引的表單里處理。我們沒有表,那我們設計什么呢? 我們需要確認物體使用的key以及key采用的類型。
讓我們從用戶這塊開始設計。當然了,首先需要展示用戶的username, userid, password, followers,自己follow的用戶等。第一個問題是:如何在我們的系統中標識一個用戶?
username是個好主意,因為它是唯一的。不過它太大了,我們想要降低內存的使用。如果我們的數據庫是關系數據庫,我們能關聯唯一ID到每一個用戶。每一個對用戶的引用都通過ID來關聯。
做起來很簡單,因為我們有我們的原子的INCR命令!當我們創建一個新用戶,我們假設這個用戶叫"antirez":
?INCR global:nextUserId => 1000
?SET uid:1000:username antirez
?SET uid:1000:password p1pp0
我們使用global:nextUserId為鍵(Key)是為了給每個新用戶分配一個唯一ID,然后用這個唯一ID來加入其他key,以識別保存用戶的其他數據。這就是kv數據庫的設計模式!請牢記于心,
除了已經定義的KEY,我們還需要更多的來完整定義一個用戶,比如有時需要通過用戶名來獲取用戶ID,所以我們也需要設置這么一個鍵(Key)
?SET username:antirez:uid 1000
一開始看上去這樣很奇怪,但請記住我們只能通過key來獲取數據!這不可能告訴Redis返回包含某值的Key,這也是我們的強處。
用關系數據庫方式來講,這個新實例強迫我們組織數據,以便于僅使用primary key訪問任何數據。


關注\被關注與更新
這也是在我們系統中另一個重要需求.每個用戶都有follower,也有follow的用戶.對此我們有最佳的數據結構!那就是.....集合(Sets).那就讓我們在結構中加入兩個新字段:
?uid:1000:followers => Set of uids of all the followers users
?uid:1000:following => Set of uids of all the following users
另一個重要的事情是我們需要有個地方來放用戶主頁上的更新。這個要以時間順序排序,最新的排在舊的前面。所以,最佳的類型是列表(List)。
基本上每個更新都會被LPUSH到該用戶的updates key.多虧了LRANGE,我們能夠實現分頁等功能。請注意更新(updates)和帖子(posts)講的是同一個東西,實際上更新(updates)是有點小的帖子(posts)。
?uid:1000:posts => a List of post ids, every new post is LPUSHed here.

?

驗證
OK,除了驗證,或多或少我們已經有了關于該用戶的一切東西。我們處理驗證用一個簡單而健壯(魯棒)的辦法:我們不使用PHP的session或者其他類似方式。
我們的系統必須是能夠在不同不同服務器上分布式部署的,所以一切狀態都必須保存在Redis里。所以我們所需要的一個保存在已驗證用戶cookie里的隨機字符串。
包含同樣隨機字符串的一個key告訴我們用戶的ID。我們需要使用兩個key來保證這個驗證機制的健壯性:
?SET uid:1000:auth fea5e81ac8ca77622bed1c2132a021f9
?SET auth:fea5e81ac8ca77622bed1c2132a021f9 1000
為了驗證一個用戶,我們需要做一些簡單的工作(login.php):
* 從登錄表單獲取用戶的用戶名和密碼
* 檢查是否存在一個鍵 username::uid
* 如果這個user id存在(假設1000)
* 檢查 uid:1000:password 是否匹配,如果不匹配,顯示錯誤信息
* 匹配則設置cookie為字符串"fea5e81ac8ca77622bed1c2132a021f9"(uid:1000:auth的值)
實例代碼:

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,?check?if?the?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?cookie?and?redirect?to?index.php ??
  • $authsecret?=?$r->get("uid:$userid:auth"); ??
  • setcookie("auth",$authsecret,time()+3600*24*365); ??
  • header("Location:?index.php"); ??
  • ??

  • 每次用戶登錄都會運行,但我們需要一個函數isLoggedIn用于檢驗一個用戶是否已經驗證。
    這些是isLoggedIn的邏輯步驟
    * 從用戶獲取cookie里auth的值。如果沒有cookie,該用戶未登錄。我們稱這個cookie為
    * 檢查auth:是否存在,存在則獲取值(例子里是1000)
    * 為了再次確認,檢查uid:1000:auth是否匹配
    * 用戶已驗證,在全局變量$User中載入一點信息
    也許代碼比描述更短:

    PHP代碼
  • function?isLoggedIn()?{ ??
  • ????global?$User,?$_COOKIE; ??
  • ??
  • ????if?(isset($User))?return?true; ??
  • ??
  • ????if?(isset($_COOKIE['auth']))?{ ??
  • ????????$r?=?redisLink(); ??
  • ????????$authcookie?=?$_COOKIE['auth']; ??
  • ????????if?($userid?=?$r->get("auth:$authcookie"))?{ ??
  • ????????????if?($r->get("uid:$userid:auth")?!=?$authcookie)?return?false; ??
  • ????????????loadUserInfo($userid); ??
  • ????????????return?true; ??
  • ????????} ??
  • ????} ??
  • ????return?false; ??
  • } ??
  • ??
  • function?loadUserInfo($userid)?{ ??
  • ????global?$User; ??
  • ??
  • ????$r?=?redisLink(); ??
  • ????$User['id']?=?$userid; ??
  • ????$User['username']?=?$r->get("uid:$userid:username"); ??
  • ????return?true; ??
  • } ??
  • ??
  • 把loadUserInfo作為一個獨立函數對于我們的應用而言有點殺雞用牛刀了,但是對于復雜的應用而言這是一個不錯的模板。
    作為一個完整的驗證,還剩下logout還沒實現。在logout的時候我們怎么做呢?
    很簡單,僅僅改變uid:1000:auth里的隨機字符串,刪除舊的auth:并增加一個新的auth:
    重要:logout過程解釋了為什么我們不僅僅查找auth:而是再次檢查了uid:1000:auth。真正的驗證字符串是后者,auth:是易變的.
    假設程序中有BUGs或者腳本被意外中斷,那么就有可能有多個auth:指向同一個用戶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"); ??
  • ??
  • 以上是我們所描述過的,應該比較易于理解。

    更新(Updates)
    更新,或者稱為帖子(posts)的實現則更為簡單。為了在數據庫里創建一個新的帖子,我們做了以下工作:
    ?INCR global:nextPostId => 10343
    ?SET post:10343 "$owner_id|$time|I'm having fun with Retwis"
    就像你看到的一樣,帖子的用戶id和時間直接儲存在了字符串里。
    在這個例子中我們不需要根據時間或者用戶id來查找帖子,所以把他們緊湊地擠在一個post字符串里更佳。
    在新建一個帖子之后,我們獲得了帖子的id。需要LPUSH這個帖子的id到每一個follow了作者的用戶里去,當然還有作者的帖子列表。
    update.php這個文件展示了這個工作是如何完成的:

    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($followers?as?$fid)?{ ??
  • ????$r->push("uid:$fid:posts",$postid,false); ??
  • } ??
  • #?Push?the?post?on?the?timeline,?and?trim?the?timeline?to?the ??
  • #?newest?1000?elements. ??
  • $r->push("global:timeline",$postid,false); ??
  • $r->ltrim("global:timeline",0,1000); ??
  • ??
  • header("Location:?index.php"); ??
  • ??

  • 函數的核心是foreach。 通過SMEMBERS獲取當前用戶的所有follower,然后循環會把帖子(post)LPUSH到每一個用戶的 uid::posts里
    注意我們同時維護了一個所有帖子的時間線。為此我們還需要LPUSH到global:timeline里。
    面對這個現實,你是否開始覺得:SQL里面用ORDER BY來按時間排序有一點兒奇怪? 我確實是這么想的。

    ?

    分頁
    現在很清楚,我們能用LRANGE來獲取帖子的范圍,并在屏幕上顯示。代碼很簡單:

    PHP代碼
  • function?showPost($id)?{ ??
  • ????$r?=?redisLink(); ??
  • ????$postdata?=?$r->get("post:$id"); ??
  • ????if?(!$postdata)?return?false; ??
  • ??
  • ????$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
  • '); ??

    • ????return?true; ??
    • } ??
    • ??
    • function?showUserPosts($userid,$start,$count)?{ ??
    • ????$r?=?redisLink(); ??
    • ????$key?=?($userid?==?-1)???"global:timeline"?:?"uid:$userid:posts"; ??
    • ????$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; ??
    • } ??
    • ??


    當showUserPosts獲取帖子的范圍并傳遞給showPost時,showPost會簡單輸出一篇帖子的HTML代碼。

    ?

    Following users 關注的用戶
    如果用戶id 1000 (antirez)想要follow用戶id1000的pippo,我們做到這個僅需兩步SADD:
    SADD uid:1000:following 1001
    SADD uid:1001:followers 1000
    再次注意這個相同的模式:在關系數據庫里的理論里follow的用戶和被follow的用戶是一張包含類似following_id和follower_id的單獨數據表。
    用查詢你能明確follow和被follow的每一個用戶。在key-value數據里有一點特別,需要我們分別設置1000follow了1001并且1001被1000follow的關系。
    這是需要付出的代價,但是另一方面講,獲取這些數據即簡單又超快。并且這些是獨立的集合,允許我們做一些有趣的事情,比如使用SINTER獲取兩個不同用戶的集合。
    這樣我們也許可以在我們的twitter復制品中加入一個功能:當你訪問某個人的資料頁時顯示"你和foobar有34個共同關注者"之類的東西。
    你能夠在follow.php中找到增加或者刪除following/folloer關系的代碼。它如你所見般平常。


    使它能夠水平分割
    親愛的讀者,如果你看到這里,你已經是一個英雄了,謝謝你。在講到水平分割之前,看看單臺服務器的性能是個不錯的主意。
    Retwis讓人驚訝地快,沒有任何緩存。在一臺非常緩慢和高負載的服務器上,以100個線程并發請求100000次進行apache基準測試,平均占用5ms。
    這意味著你可以僅僅使用一臺linux服務器接受每天百萬用戶的訪問,并且慢的跟個傻猴似的,就算用更新的硬件。
    雖然,就算你有一堆用戶,也許也不需要超過1臺服務器來跑應用,但讓我們假設我們是Twitter,需要處理海量的訪問量呢?該怎么做?

    Hashing the key
    第一件事是把KEY進行hash運算并基于hash在不同服務器上處理請求。有大量知名的hash算法,例如ruby客戶端自帶的consistent hashing
    大致意思是你能把key轉換成數字,并除以你的服務器數量
    ?server_id = crc32(key) % number_of_servers
    這里還有大量因為添加一臺服務器產生的問題,但這僅僅是大致的意思,哪怕使用一個類似consistent hashing的更好索引算法,
    是不是key就可以分布式訪問了呢?所有用戶數據都分布在不同的服務器上,沒有inter-keys使用到(比如SINTER,否則你需要注意要在同一臺服務器上進行)
    這是Redis不像memcached一樣強制指定索引算法的原因,需要應用來指定。另外,有幾個key訪問的比較頻繁。

    特殊的Keys
    比如每次發布新帖,我們都需要增加global:nextPostId。單臺服務器會有大量增加的請求。如何修復這個問題呢?一個簡單的辦法是用一臺專門的服務器來處理增加請求。
    除非你有大量的請求,否則矯枉過正了。另一個小技巧是ID并不需要真正地增加,只要唯一即可。這樣你可以使用長度為不太可能發生碰撞的隨機字符串(除了MD5這樣的大小,幾乎是不可能)。
    完工,我們成功消除了水平分割帶來的問題。

    另一個問題是global:timeline。這里有個不是解決辦法的解決辦法,你可以分別保存在不同服務器上,并且在需要這些數據時從不同的服務器上取出來,或者用一個key來進行排序。
    如果你確實每秒有這么多帖子,你能夠再次用一臺獨立服務器專門處理這些請求。請記住,商用硬件的Redis能夠以100000/s的速度寫入數據。我猜測對于twitter這足夠了。
    請隨意在下面評論處提問以及反饋。

    創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎

    總結

    以上是生活随笔為你收集整理的PHP + Redis 实现一个简单的twitter的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。