原文:http://codecampo.com/topics/196git
首先是這個主題的前篇連接 http://codecampo.com/topics/4d7f18bf9f328ba60e000006github
前篇文章構思了一個用戶廣播的實現,而且給出了僞代碼。如今 codecampo 已經實現了一個基於 Mongodb + redis 的狀態廣播,因此能夠補充一下前篇沒有描述清楚的地方。web
上篇說到因爲廣播規則的複雜性,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
先介紹一下 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 操做的需求以下:
campo 實現的 timeline 操做封裝在 app/model/stream.rb 文件中,完整代碼能夠在這裏看到。
下面分析一下實現
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。
def status_ids(start = 0, stop = -1) $redis.lrange store_key, start, stop end
redis 的 lrange 操做能夠分段讀取 list 數據。實際讀取 Timeline 時,先獲取 ids,而後再到 mongodb 獲取文檔數據。具體實現看 Stream#fetch_statuses。
有兩種狀況須要重建 Timeline:一、服務崩潰致使隊列丟失 二、用戶新增訂閱。
這時候能夠用前面提到的 Mongodb 查詢重建 Timeline。重建能夠做爲後臺任務進行,這樣不管規則多麼複雜都不會阻塞用戶的新增訂閱的操做。
詳細能夠看 Stream#rebuild_later 和 Stream#rebuild 的實現。
接觸 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 查詢我以爲不太優雅,因此實際中並無調用這個方法。
如今已經實現了上篇主題中提到的第二階段 Timeline,而第三階段的「忽略不活躍用戶」,目前 campo 尚未達到這個用戶量,就不過分設計了。
對於如今的信息過載的互聯網,訂閱和廣播模式是很好的信息過濾模式。用戶應該容許只關注本身感興趣的內容,而且屏蔽不感興趣的內容。campo 接下來還會實現用戶 block 和主題 mute 功能。
訂閱模式在互聯網上已經出現好久了,可是具體實現的文章很少,但願本篇給查找此類信息的人一點幫助。