關於如何構建一個微博型廣播 二(轉)

原文:http://codecampo.com/topics/196git

 

首先是這個主題的前篇連接 http://codecampo.com/topics/4d7f18bf9f328ba60e000006github

前篇文章構思了一個用戶廣播的實現,而且給出了僞代碼。如今 codecampo 已經實現了一個基於 Mongodb + redis 的狀態廣播,因此能夠補充一下前篇沒有描述清楚的地方。web

0 Timeline 用查詢仍是緩存?

上篇說到因爲廣播規則的複雜性,timeline 最好使用一個隊列,新增 status 使用投遞方式而不依賴數據庫查詢。redis

具體看例子,campo 當前的 status 數據會是這樣的:mongodb

> db.status_bases.findOne({ _type : "Status::Topic" })
{
    "_id" : ObjectId("4df484bde7444a4597000002"),
    "_type" : "Status::Topic",
    "created_at" : ISODate("2011-02-19T12:14:53Z"),
    "tags" : [ ],
    "topic_id" : ObjectId("4d5fb43d9f328b666500000a"),
    "user_id" : ObjectId("4d5fb41b9f328b6665000006")
}

> db.status_bases.findOne({ _type : "Status::Reply" })
{
    "_id" : ObjectId("4df484c0e7444a45970003a7"),
    "_type" : "Status::Reply",
    "created_at" : ISODate("2011-05-21T15:31:30Z"),
    "reply_id" : ObjectId("4dd7dad29f328b74df000018"),
    "targeted" : true,
    "topic_id" : ObjectId("4d5fb94c9f328b666500001f"),
    "user_id" : ObjectId("4d5e8dfc9f328bd543000002")
}

當前有兩種類型的 status,一類跟主題建立相關,叫作 Status::Topic,一類跟回覆建立相關,叫作 Status::Reply。這兩類數據存在同一個 collection 中,數據有相同的地方,好比:userid,topicid;也有各自特性的數據,好比:reply_id,targeted(是否以@開頭的直接回復),tags(緩存主題的tags)。數據庫

Timeline 的規則是:一、不顯示本身的 status 二、不顯示 targeted 爲 true 的直接回復 三、顯示 following 用戶的 status 四、顯示出現本身喜好標籤的 status 五、顯示本身關注主題和本身建立的主題的回覆 status 六、按時間排列緩存

若是用數據庫查詢怎麼實現 Timeline?用 mongoid 查詢看起來會是這樣的:app

mark_topic_ids = Topic.where(:marker_ids => @user.id).only(:_id).map(&:_id) 
self_topic_ids = @user.topics.only(:_id).map(&:_id)
topic_ids = (mark_topic_ids + self_topic_ids).uniq
status_ids = Status::Base.where(:targeted.ne => true, :user_id.ne => @user.id).any_of({:user_id.in => @user.following_ids.to_a}, {:tags.in => @user.favorite_tags.to_a}, {:topic_id.in => topic_ids}).asc(:created_at).limit(Stream.status_limit)

生成的 Mongo Query 可能讓人嚇一跳,由於用來 $in 查詢的 followingids 和 favoritetags 還有 topic_ids 會很是長。雖然過早考慮性能不是一個好習慣,但我認爲每次都用查詢來獲取一個不變的列表很是不「天然」。性能

因此能夠考慮建造一個 Timeline 隊列緩存,當前有一個很是適合存放 Timeline 的內存型數據庫:redis。fetch

1 用 redis 儲存 Timeline

先介紹一下 redis

Redis is an open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.

redis 對 lists 數據的支持很好,優於 mongodb。例如我沒找到讓 mongodb 簡單插入一條數據到 List 頭部而且限制長度、丟棄老數據的好方法。

我對 Timeline list 操做的需求以下:

  1. 能夠插入一個 status id 到列表頭部
  2. 若是 list 長度超過設定值(好比800),就刪除尾部的數據
  3. 能夠相似翻頁式的獲取某區間的 ids

campo 實現的 timeline 操做封裝在 app/model/stream.rb 文件中,完整代碼能夠在這裏看到。

下面分析一下實現

push status

def push_status(status)
  $redis.lpush store_key, status.id
  $redis.ltrim store_key, 0, Stream.status_limit - 1
end

push_status 操做先用 lpush 操做將 id 從列表左邊 push 進去,而後用 ltrim 拋棄列表右邊超過指定數量的 id。

獲取某區間 ids

def status_ids(start = 0, stop = -1)
  $redis.lrange store_key, start, stop
end

redis 的 lrange 操做能夠分段讀取 list 數據。實際讀取 Timeline 時,先獲取 ids,而後再到 mongodb 獲取文檔數據。具體實現看 Stream#fetch_statuses。

2 重建 Timeline

有兩種狀況須要重建 Timeline:一、服務崩潰致使隊列丟失 二、用戶新增訂閱。

這時候能夠用前面提到的 Mongodb 查詢重建 Timeline。重建能夠做爲後臺任務進行,這樣不管規則多麼複雜都不會阻塞用戶的新增訂閱的操做。

詳細能夠看 Stream#rebuild_later 和 Stream#rebuild 的實現。

3 關於數據完整性?

接觸 NoSQL 應用以後,常常聽到的一個問題是數據完整性。campo 當前的實現有完整性問題麼?有的,好比刪除一個 status 的時候 Timeline 裏面會遺留無效的 id。但根據狀況的不一樣,web 應用一般能夠忽略這些完整性:讀寫需求遠大於刪除需求、用戶自己不在意數據完整性。

campo 的 Timeline 裏面遇到無效 id 的時候,會致使某頁的 status 數量不足分頁數量,但這不是什麼大問題。能夠在用戶下次觸發 Timeline 重建的時候丟棄,或者隨着時間的推移被新 status 推後直至丟棄。

固然經過 redis 緩存 + mongodb 也能夠查詢一個沒有缺憾的 Timeline

# slow than fetch_statuses, but complete than fetch_statuses
def statuses
  Status::Base.where(:_id.in => status_ids).desc(:created_at)
end

可是用一個 800 ids 的 $in 查詢我以爲不太優雅,因此實際中並無調用這個方法。

4 小結

如今已經實現了上篇主題中提到的第二階段 Timeline,而第三階段的「忽略不活躍用戶」,目前 campo 尚未達到這個用戶量,就不過分設計了。

對於如今的信息過載的互聯網,訂閱和廣播模式是很好的信息過濾模式。用戶應該容許只關注本身感興趣的內容,而且屏蔽不感興趣的內容。campo 接下來還會實現用戶 block 和主題 mute 功能。

訂閱模式在互聯網上已經出現好久了,可是具體實現的文章很少,但願本篇給查找此類信息的人一點幫助。

相關文章
相關標籤/搜索