SNS中好友动态功能的设计思路

现在大部分SNS网站都有一个功能,就是显示好友的活动状态,比如你的好友上传了一张照片、分享了一篇文章等等动作,都可以显示在你的页面里,这样大大增强了社区的互动性,也成为现在SNS网站的主要特征,对于这样一个功能,从设计角度,还是值得思考的,并不简单,特别是用户越来越多,信息海量增长的时候,我未必能提出十全十美的方案,但我们可以由简如繁梳理一下思路。

首先我们要定义用户的活动消息,也可以理解为一个事件,就是我们举的例子:用户上传照片、与别人结为好友、修改了个人资料等等,动作各有不同,但需要在结构上通用,我们先设计一个

ID //消息ID
UserID //用户ID
MsgType //消息类型,比如加好友、上传照片等不同的类型
EventMsg //消息的内容,这里我们可以用Json的数据格式来描述出不同的活动内容
CreateTime //消息创建时间

这个结构也是个数据库的结构,当用户做个一个动作之后,就会创建这样一个消息,并保存在数据库中,当显示好友的活动信息时,就从这张表里查询自己好友id的数据,并按时间显示,这个做法是一个最简单的实现,但会出现一些问题,当你与一个用户成为好友之后,该好友之前发生的动作会显示出来,而不是在成为好友时点之后的动作,同样,切断好友关系之后也有类似的问题,如果从业务角度和用户体验上可以接受的话,也没什么,但由于信息是按时间排序,有时候会给用户错乱的感觉,还有,这个信息不能删除,如果删除了所有好友就看不到这条信息了,但在Facebook里是则是可以删除好友的动作信息的,这个方法还有一个问题是,所有信息都放在一张大表里,在信息爆炸增长,个人好友也很多的情况下,查询效率会非常低,产生严重的性能障碍,如果对这张表做水平切分,则在实现上复杂了许多,性能也未必好很多,接下来我们再思考是否有更好的解决办法。

在SNS的理论里,个人好友的合理数量在150个左右(最近有文章说Facebook的人均好友数是120人),SNS网站应该是有好友数量的限制的,我们就按人均150个好友来设想,是否可以在用户发生某些动作之后,针对他的所有好友都写入一条信息,所能解决的是,信息是在用户成为好友时点之后写入,用户可以删除好友的活动信息,不影响其他用户的显示,在显示时查询效率要高很多,但是负面效应也十分明显,一个用户的动作有平均150个写入,对于数据库来说开销也非常大,我们想想在这样的设计下,是否可以提高性能。

首先看数据库设计,我们要把信息表和信息与用户的对应表分开,我们上面定义的数据结构保留,我们定义它的表名为Event,我们再新建一张表EventUser,结构如下

ID  //主键
EventID //Event表的ID
FriendUserID //好友的ID
CreateTime //消息创建时间

对FriendUserID做索引,当用户发生动作时,首先将动作信息写入Event表,之后查找用户的所有好友,将EventID、好友ID逐条写入EventUser表,当要显示自己的好友活动信息是,查询EventUser中FriendUserID等于自己ID的信息,并和Event表做一个Join查询就可以了。

进一步提升性能的方法,将Event里的信息缓存,比如用Memcached,EventID做为Key,内容做为Value,这样,查询EventUser是就不用Join Event表,而是从缓存里读取,由于要记录给每个好友的信息,所以EventUser会变得非常大,平均要比我们最初设计的数据容量大150倍,我们对EventUser表做水平切分,根据用户ID做Hash算法,基本上能均匀的分配到所有的表中,至于EventUser水平切分的算法还有多种,根据实际情况来定,总的来说就是把数据分摊掉,同时EventUser里的数据可以不永久保存,做定期删除,可以保持数据容量在一个合理的范围内。

对于用户动作之后的数据写入,可以采用异步的方法,在发生动作时,抛出一个消息,由另外一个线程在做写入操作,如果对每个动作平均150次的写入仍存顾虑,我们可以针对每个用户开出一块内存空间,或是缓存,里面保存最后50条最新的好友动作,并在每条记录里做一个持久化标志,当有50条信息都标志是“未持久化”时,做一次数据库的写入,然后把信息置为“已持久化”,这种非实时写入的方式,可以提高一定的数据库效率,显示时,先从内存中取出,再查数据库。

还有一些问题,对不不同消息类型的处理方式不同,比如用户修改个人资料,并不是每次发生这样的动作都要做一次给朋友做一次“群发”的操作,如果遇到这个人在短时间内做了多次修改个人资料的操作,就给朋友发出多个消息,会产生垃圾信息,让人觉得很怪异,所以对于这样的操作会有一定的时效性,比如在一天之内的修改个人资料,就是一个消息,这时候的处理是更新,而不是插入。

以上是我对SNS中好友动态功能的设计思路,可能还有一些未想到的问题,还需要认真思考。

从邮件服务商取得用户通讯录的方法

我们在使用facebook开心网这些SNS网站的时候,里面有一项功能,就是通过邮件邀请好友,它可以导出你在Hotmail、Gmail、Yahoo、网易、新浪等Web Mail里的通讯录,然后给这些好友发送邀请信,这种病毒式营销推广方式起到了非常好的效果,facebook、开心网这些网站得以迅速壮大,这项功能也成了SNS网站的标配,从技术角度,我们就来探讨一下实现这项功能的原理和方法。

先说Gmail、Hotmail,Google和MS还是非常强大的,Google提供了Account Authentication API,它允许第三方的网站通过它的认证服务后,通过Google的Service取得用户的数据,包括通讯录等,MS也有Windows Live ID服务,不过我对微软的东西研究的不深,MSDN上找到的Windows Live ID SDK的资料,http://msdn.microsoft.com/en-us/library/bb404787.aspx

接下来说说其他邮件系统的取得方式,主要的原理就是通过模拟用户行为,登陆Web Mail系统,取得用户通讯录的信息,很多Web Mail都提供通讯录CSV格式的下载服务,我们取得这个CSV文件就可以了,网易的邮件系统似乎已经不提供CSV文件的下载,那只能通过通讯录页面的HTML分析,来取得信息,所以相对复杂一些,而Sohu的通讯录虽然前端采用了Ajax,但后端取得通讯返回的格式是Json,应该是最方便。

接下来说模拟用户行为,其实就是通过在自己服务器端向Web Mail发送相对的HTTP请求,而模拟出用户登陆的行为,在Linux下,我们用cURL这个组件,在编译PHP的时候把cURL编译进去,使PHP支持cURL,cURL非常好用,也非常强大,在libcurl里有大多数开发语言的支持,如果使用JAVA的话,还可以使用Apache的HttpClient

我们要模拟出用户登陆Web Mail的过程,我们就要对通常情况下,登陆Web Mail的HTTP请求过程有所了解,HttpWatch是一个非常好的工具,在做为插件安装倒IE和FireFox上,在IE或是FireFox里就可以看到所有的HTTP请求的信息了。

httpwatch

我们看看网易163的邮件的登陆过程

00:00:00.000    跳转提示
+ 0.000        0.231    1543    2352    POST    200    text/html; charset=UTF-8    http://reg.163.com/login.jsp?type=1&url=http://entry.mail.163.com/coremail/fcg/ntesdoor2?lightweight%3D1%26verifycookie%3D1%26language%3D-1%26style%3D35
0.231    1543    2352    1 request
00:00:00.462        0.177    0    0    GET    (History Cache)    image/x-icon    http://reg.163.com/favicon.ico
00:00:00.469    网易电子邮箱 – 简约3.0Beta
+ 0.000        0.118    1703    604    GET    302    Redirect to http://g4a30.mail.163.com/jy3/main.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz    http://entry.mail.163.com/coremail/fcg/ntesdoor2?lightweight=1&verifycookie=1&language=-1&style=35&username=xxx
+ 0.245        0.366    1664    6788    GET    200    text/html;charset=GBK    http://g4a30.mail.163.com/jy3/main.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz
+ 0.422        0.158    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/jscss/globle.css
+ 0.659        0.087    0    0    GET    (Cache)    application/x-javascript    http://mimg.126.net/p/jy3style/js/0903090940/tools.js
+ 0.816        0.067    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/cmcss/skin_blue.css
+ 0.998        0.578    1586    5521    GET    200    text/html;charset=GBK    http://g4a30.mail.163.com/jy3/top.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz
+ 1.015        0.624    1589    24844    GET    200    text/html;charset=GBK    http://g4a30.mail.163.com/jy3/folder.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz
+ 1.025        0.316    1590    10490    GET    200    text/html;charset=GBK    http://g4a30.mail.163.com/jy3/welcome.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz
+ 1.034        3.174    1592    2043    GET    200    text/html;charset=GBK    http://g4a30.mail.163.com/jy3/justfresh.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz
+ 1.280        0.439    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/jscss/globle.css
+ 1.632        0.407    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/jscss/globle.css
+ 1.647        0.432    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/jscss/welcome2.css
+ 1.876        0.235    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/jscss/globle.css
+ 2.179        0.117    0    0    GET    (Cache)    application/x-javascript    http://mimg.126.net/p/jy3style/js/0903090940/tools.js
+ 2.263        0.167    0    0    GET    (Cache)    application/x-javascript    http://mimg.126.net/p/jy3style/js/0903090940/tools.js
+ 2.354        0.207    0    0    GET    (Cache)    application/x-javascript    http://mimg.126.net/p/jy3style/js/0903090940/tools.js
+ 2.488        0.209    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/cmcss/skin_blue.css
+ 2.614        0.152    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/cmcss/skin_blue.css
+ 2.619        0.192    0    0    GET    (Cache)    application/x-javascript    http://mimg.126.net/p/jy3style/js/0903090940/welcome.js
+ 2.740        0.325    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/cmcss/skin_blue.css
4.208    9724    50290    20 requests
00:00:00.878        0.124    0    0    GET    (History Cache)    image/x-icon    http://www.163.com/favicon.ico
00:00:03.572    网易电子邮箱 – 简约3.0Beta
+ 0.000        0.541    0    0    GET    (Cache)    text/html; charset=GB2312    http://mimg.163.com/tianqi/city_simple/58367_090311.html
+ 0.531        0.715    1424    250    GET    304    text/css    http://mimg.163.com/jy3style/lib/jscss/ifr_weather2.css
+ 1.614        0.245    0    0    GET    (Cache)    image/gif    http://mimg.163.com/jy3style/lib/images/weather2.gif
1.859    1424    250    3 requests

首先,将用户名和密码POST到http://reg.163.com/login.jsp?type=1&url=http://entry.mail.163.com/coremail/fcg/ntesdoor2?lightweight%3D1%26verifycookie%3D1%26language%3D-1%26style%3D35,reg.163.com是网易通行证,后面的url是用户登陆网易通行证后要转向的地址,在这里我们看到就是http://entry.mail.163.com/coremail/fcg/ntesdoor2,这就是网易邮件的入口,在我们模拟登陆的时候,可以不传这个url过去,至于用户名和密码表单的name,看mail.163.com的HTML代码就知道了。相关错误处理,在POST到http://reg.163.com/login.jsp这个地址之后,返回的信息要取得的,因为如果用户输错了用户名或是密码,还是应该要提示用户的,而且在错误的情况下,也不用往下进行了,reg.163.com返回的错误页面里有“您的密码不正确”或是“用户名不正确”这样的信息,以此就可以判断用户是否成功登陆了。

之后我们要模拟用户访问http://entry.mail.163.com/coremail/fcg/ntesdoor2?lightweight=1&verifycookie=1&language=-1&style=35&username=xxx,就是登陆邮件的过程,注意,这里是个GET方法,用POST方法是不行的,访问entry.mail.163.com这个URL之后,会返回一个302码,就是要Redirect转向,到http://g4a30.mail.163.com/jy3/main.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz这个地址,这也就是大家看到的邮件登陆进去的首页了,注意这个sid要抓出来,应该是登陆的一个token,还有http://g4a30.mail.163.com这个地址,网易邮件是个集群,有非常多的服务器,你们每次上去的服务器是随随机的,用你的sid访问其他服务器也是没问题的,比如http://g1a98.mail.163.com/jy3/main.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz,所以这个服务器地址你可以写死。

解析来我们要看看网易邮件通讯录的URL,在UI看到是http://g4a30.mail.163.com/jy3/address/addrlist.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz&gid=all,我们模拟用户访问这个URL,然后抓取这个页面的内容HTML,分析一下,就可以了,在PHP里有个很好的HTML Parser工具PHP Simple HTML DOM Parser,可以像jQuery一样取得页面元素,这样就很方便的取得页面信息了。

其他一些Web Mail的登陆过程分析也是类似的,如果是取得CSV文件就更简单了,通过取到的通讯录内容,就可以在SNS网站上邀请了。

在这里我向国内这些大的门户网站或是Mail服务商提个建议,现在的互联网是一个开放化、平台化的趋势,网站应该开放自己的资源,可以让第三方的的应用更方便的访问,固步自封是不能发展的,试想如果QQ开放他的好友平台,引入第三方应用,那它将轻松超过Facebook,成为世界第一大SNS社区。