李磊
摘要:微博系統對信息實時性和并發性的要求,傳統的關系型數據庫無法滿足性能要求。Key-Value模型的內存數據庫Redis,非常適合微博系統對數據快速存取的需要。
關鍵詞:Redis;Key-Value;微博
中圖分類號:TP311 文獻標識碼:A 文章編號:1009-3044(2016)25-0061-03
Abstract:Microblog system requirements for information-real-time performance and concurrency. Traditional relational database does not meet performance requirements. Key-Value model memory database Redis, very suitable for the microblog system with fast access to the data needed.
Key words:Redis; Key-Value; Microblog
1 概述
微博系統類似于一個群聊的龐大聊天室,每時每刻都會有大量的消息產生,而且產生的消息會反饋給需要的用戶,這樣就要求數據的讀寫非???。關系型數據庫在數據量超過一定規模時,由于自身邏輯相對復雜,在信息檢索上無法滿足用戶的體驗。
Redis數據庫本身的數據就放在內存中,而且有合適的數據結構。Twitter、新浪微博都是目前最大的Redis用戶。
2 Redis介紹
Redis是一個速度非??斓姆顷P系型數據庫。Redis可以存儲鍵(Key)與5種不同類型的值(Value)之間的映射,可以將存儲在內存中的鍵值對持久化到硬盤。Redis還可以使用復制特性來擴展讀性能,使用客戶端分片來擴展寫性能。
其重要特性如下:
① 持久化:Redis定期把數據異步flush到硬盤進行保存。服務器重啟,數據不會丟失。
② 主從復制:主要用1臺服務器進行數據備份與恢復。
③ Vitual Memory功能:物理內存畢竟是有限的,這技術主要是把很少用的Value保存到硬盤,而Key保留在內存做檢索,提高訪問性能。
④ 多種數據結構支持:Redis的Key是string類型,Value的類型有:string、set、list、zset(sorted set)、hash。針對每種數據類型,還提供相應的操作命令,比如list類型有LPOP、LPUSH、RPOP、RPUSH等操作,set類型SDIFF(差運算)、SINTER(交運算)、SUNION(并運算)等操作①。
3 PHP和Redis構建微博系統基本功能
利用PHP的Redis擴展,在PHP中實現微博系統新用戶的創建、消息發布、關注與粉絲設計、消息推送等基本功能。PHP版本為5.5.12,Redis版本為3.0.501。
3.1 用戶信息表示
Redis hash是一個string類型的field和value的映射表.一個key可對應多個field,一個field對應一個value。將一個對象存儲為hash類型,較于每個字段都存儲成string類型更能節省內存。新建一個hash對象時開始是用zipmap(又稱為small hash)來存儲的。這個zipmap其實并不是hash table,但是zipmap相比正常的hash實現可以節省不少hash本身需要的一些元數據存儲開銷。盡管zipmap的添加,刪除,查找都是O(n),但是由于一般對象的field數量都不太多。所以使用zipmap也是很快的,也就是說添加刪除平均還是O(1)。如果field或者value的大小超出一定限制后,Redis會在內部自動將zipmap替換成正常的hash實現.。
這里我們在數據庫中表示用戶信息和發布的消息都用Redis的hash結構。用戶信息如表1。
創建新用戶時,我們用到一個user:id:的計數器,實際就是Redis的一個Key,初始一個值,然后每次添加到user:uid的hash后值要自增1。用用戶信息中login和id兩個filed的值構造另一個hash表users:,用來建立login和id之間的映射。關鍵代碼如下:
if($redis->hget("users:",$login)){echo "{$login}已經存在,重新輸入";}
else{$uidarray=$redis->multi()->incr("user:id:")->exec();
$uid=$uidarray[0];
$userinfo=array(
"login"=>$login,
"id"=>$uid,
"name"=>$name,
"following"=>0,
"fans"=>0,
"posts"=>0,
"signup"=>time());
$redis->multi()->hset("users:",$login,$uid)
->hmset("user:{$uid}",$userinfo)->exec();}
3.2 發布的消息表示
用戶發布的消息也用hash表示,結構如表2。
發布消息時候,也用到一個計數器message:mid:,其值傳給消息中的mid,然后自增1,保證每個消息都有不同的mid。發布消息時,不僅要添加message:mid一個新的值,還要修改user:uid中的posts域的值。關鍵代碼如下:
$midarray=$redis->multi()->incr("message:mid:")->exec();
$mid=$midarray[0];
$messageinfo=array(
"content"=>$content,
"time"=>time(),
"mid"=>$mid,
"uid"=>$uid,
"login"=>$login);
$redis->multi()->hmset("message:{$mid}",$messageinfo)
->hincrby("user:{$uid}","posts",1)->exec();
3.3 用戶主頁時間線和個人時間線
Redis提供zset這種有序集合數據結構。通過zadd命令添加的成員,按照score的值排序,默認score的值遞增。在微博系統中利用該數據結構的特點,可以很方便的取出最新的消息。
用戶主頁時間是指,當用戶登錄后,能看到用戶以及用戶關注的人所發布的消息列表,這個列表以發布消息的時間排序。在Redis中用戶主頁時間線結構如表3。
用戶個人時間線僅僅只有用戶個人發布的消息列表,也是以發布時間排序。用戶個人時間線profile:uid結構如下表4。
3.4 關注者列表和粉絲列表
微博系統就是要讓用戶之間分享各自的構想、想法。當A用戶開始關注或取消關注B用戶的時候,我們不僅要更新A用戶的關注列表following:A和A用戶的個人信息中關注數量following的值,還要更新B用戶的粉絲列表fans:B和B用戶的個人信息中粉絲數量fans的值。最后把B用戶發布的消息,profile:B中的消息,更新到A用戶的主頁時間線home:A。兩個列表都用Redis的zset有序集合結構表示。表5為關注者列表結構,表6位粉絲列表結構。
開始關注操作關鍵代碼如下:
define("HOME_TIMELINE_SIZE",1000);
$fkey1="following:{$uid}";
$fkey2="fans:{$other_id}";
$have=$redis->zscore($fkey1,$other_id);
if($have==true){ echo "{$uid}已經關注{$other_id}";}
else{$time=time();
$values=$redis->multi()->zadd($fkey1,$other_id,$time)
->zadd($fkey2,$uid,$time)
->zrevrange("profile:{$other_id}",0,HOME_TIMELINE_SIZE-1,true)
->exec();
$redis->multi()->hincrby("user:{$uid}","following",$values[0])
->hincrby("user:{$other_id}","fans",$values[1])->exec();
if($values[2]){//獲取$other_id發布的消息不為空
foreach($values[2] as $key=>$value)
{ $redis->multi()->zadd("home:{$uid}",$value,$key)->exec();}}}
取消操作關鍵代碼如下:
define("HOME_TIMELINE_SIZE",1000);
$fkey1="following:{$uid}";
$fkey2="fans:{$other_id}";
$have=$redis->zrangebyscore($fkey1,$other_id,$other_id,array(true,1));
if($have==false){echo "{$uid}沒有關注{$other_id}";}
else{$values=$redis->multi()->zrem($fkey1,$other_id)
->zrem($fkey2,$uid)
->zrevrange("profile:{$other_id}",0,HOME_TIMELINE_SIZE-1,true)
->exec();
$redis->multi()->hincrby("user:{$uid}","following",-$values[0])
->hincrby("user:{$other_id}","fans",-$values[1])->exec();
if($values[2]){//獲取$other_id發布的消息不為空
foreach($values[2] as $key=>$value)
{$redis->multi()->zrem("home:{$uid}",$key)->exec();}}}
3.5 消息推送
在3.2節里面說明的是消息發布后,除了進行消息信息添加以外,用戶個人信息中發布消息數量posts值得自增。我們還應該接著把發布的消息告訴給個人時間線和主頁時間線,也就是在profile:uid(uid為發布消息的用戶id)中添加消息編號mid和時間戳time,并且在home:uid(uid為發布消息的用戶id)中也添加消息編號和時間戳time,這個時間戳應該是消息發布的時候服務器的時間戳。然后還要在粉絲的主頁時間線home:uid(發布消息用戶的粉絲id)中添加同樣的數據。由于微博系統中有的用戶粉絲數量非常大,如果同步更新可能會導致用戶長時間等待。所以,在更新的時候,可以先更新fans:uid(uid為發布消息的用戶id)集合中前面1000個關注者,對每個關注者的home:uid進行更新。關鍵代碼如下:
$redis->multi()->zadd("profile:{$uid}",$time,$mid)
->zadd("home:{$uid}",$time,$mid)->exec();
$fans=$redis->zrevrange("fans:{$uid}",0,1000,true);
foreach($fans as $key=>$value)
{ $redis->zadd("home:{$value}",$time,$mid);}
如果存在超過1000個用戶的情況,可以設計一個延遲功能來進行轉發,避免發布消息的用戶長時間等待。
4 總結和展望
Redis本身提供了很多的數據結構,靈活應用可以構造適合微博系統的數據庫。這里我們搭建了php+redis環境,做一個簡單的微博系統,實現基本功能。要開發像Twitter、sina微博等系統,還要考慮更復雜的數據構造實現更多的功能,以及如何擴展服務器來提高服務質量。
參考文獻:
[1] 唐誠.Redis數據庫在微博系統中的實踐[J].廈門城市學院學報,2012,14(3):55-59.
[2] Josianh L Carlson.Redis實戰[M].北京:人民郵電出版社,2015.
[3] 王艷,董麗麗.NoSql與關系數據庫相結合的設計與實踐[J].電腦知識與技術, 2014(9).