原文: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 功能。
订阅模式在互联网上已经出现好久了,可是具体实现的文章很少,但愿本篇给查找此类信息的人一点帮助。