本篇介绍笔者接触的第一个后台系统,从自身见闻出发,所以涉及的内容相对比较基础,后台大牛请自觉略过。面试
什么是好友系统?redis
简单的说,好友系统是维护用户好友关系的系统。咱们最熟悉的好友系统案例当属QQ,实际上QQ是一款即时通信工具,凭着好友系统沉淀了海量的好友关系链,从而铸就了一个坚如盘石的商业帝国。好友系统的重要性可见一斑。数据库
熟悉互联网产品的人都知道,当产品有了必定的用户量,每每会开发一个好友系统。其主要目的是增长用户粘性(有了好友就会常来)或者增长社区活跃度(有了好友就会多交流)。后端
而个人后台开发生涯就是从这样一个系统开始的。缓存
那时候,好友系统对于咱们团队大部分人来讲,都是一个全新的事物,由于咱们大部分人都是应届生。整个系统的架构天然不是咱们一群黄毛小孩所能创造。当年的架构图已经找不到了,可是凭着一点记忆和多年来的经验积累,仍是能够把当年的架构勾勒出来。服务器
如图,好友系统的架构是常见的3层结构,包括接入层、逻辑层和数据层。网络
咱们先从数据层讲起。session
由于咱们对QQ太熟悉了,咱们能够很容易地列出好友系统的数据主要包括用户资料、好友关系链、消息(聊天消息和系统消息)、在线状态等。架构
互联网产品每每要面对海量的请求并发,传统的关系型数据库比较难知足读写需求。在存储中,通常是读多写少的数据才会使用MySQL等关系型数据库,并且每每还须要增长缓存来保证性能;NoSQL(Not Only SQL)应该是目前的主流。并发
对于好友系统,用户资料和好友关系链都使用了kv存储,而消息使用公司自研的tlist(能够用redis的list替代),在线状态下面再介绍。
接着是逻辑层。
在这个系统中复杂度最高的应该是消息服务(而这个服务我并无参与开发[捂脸])。
消息服务中,消息按类型分为聊天消息和系统消息(系统消息包括加好友消息、全局tips推送等),按状态分为在线消息和离线消息。在实现中,维护3种list:聊天消息、系统消息和离线消息。聊天消息是两个用户共享的,系统消息和离线消息每一个用户独占。当用户在线时,聊天消息和系统消息是直接发送的;若是用户离线,就把消息往离线消息list存入一份,等用户再次登陆时拉取。
这样看来,消息服务并不复杂?其实否则,系统设计中常规的流程设计每每是比较简单的,可是对于互联网产品,异常状况才是常态,当把各类异常状况都考虑进来时,系统就会很是复杂。
这个例子中,消息发送丢包是一种异常状况,怎么保证在丢包状况下,还能正常运行就是一个不小的问题。
常见的解决方法是收包方回复确认包,发送方若是没收到确认包就重发。可是确认包又可能丢包,那又能够给确认包增长一个确认包,这是一个永无止境的确认。
解决方法能够参考TCP的重传机制。那问题来了,咱们为何不用TCP呢?由于TCP仍是比较慢的,聊天消息的可靠性没有交易数据要求那么高,丢几条消息并不会形成严重后果,可是若是用户每次发送消息后都要等好久才能被收到,那体验是不好的。
一个比较折中的方案是,收包方回复确认包,若是发送方在必定时间内没有收到确认就重发;若是收包方收到两个相同的包(自定义seq同样),去重便可。
一个面试题引起的讨论:
面试时我经常会问候选人一个问题:在分布式系统中怎样实现一个用户同时只能有一个终端在线(用户在两个地方前后登陆帐号,后一次登陆能够把前一次登陆踢下线)?这是互联网产品中很是基础的一个功能,考察的是候选人基本的架构设计能力。
设计要先从接入服务器(下称接口机)提及。接口机是好友系统对外的窗口,主要功能是维护用户链接、登陆鉴权、加解密数据和向后端服务透传数据等。用户链接好友系统,首先是链接到接口机,鉴权成功后,接口机会在内存中维护用户session,后续的操做都是基于session进行。
如图所示,用户若是尝试登陆两次,接口机经过session就能够将第一次的登陆踢下线,从而保证只有一个终端在线。
问题解决了吗?
没有。由于实际系统确定不会只有一台接口机,在多台接口的状况下,上面的方法就不可行了。由于每一个接口机只能维护部分用户的session,因此若是用户前后链接到不一样的接口机,就会形成用户多处登陆的问题。
天然能够想到,解决的方法就是要维护一个用户状态的全局视图。在咱们的好友系统中,称为在线状态服务。
在线状态服务,顾名思义就是维护用户的在线状态(登陆时间、接口机IP等)的服务。用户登陆和退出会经过接口机触发这里的状态变动。由于登陆包和退出包均可能丢包,因此心跳包也用做在线状态维护(收到一次心跳标记为在线,收不到n次心跳标记为离线)。
一种经常使用的方法是,采用bitmap存储在线状态,具体是指在内存中分配一块空间,32位机器上的天然数一共有4294967296个,若是用一个bit来表示一个用户ID(例如QQ号),1表明在线,0表明离线,那么把所有天然数存储在内存只要4294967296 / (8 * 1024 * 1024) = 512MB(8bit = 1Byte)。固然,实现中也能够根据须要给每一个用户分配更多的bit。
因而,踢下线功能如图所示。
用户登陆的时候,接口机首先查找本机上是否有session,若是有则更新session,接着给在线状态服务发送登陆包,在线状态服务检查用户是否已经在线,若是在线则更新状态信息,并向上次登陆的接口机IP发送踢下线包;接口机在收到踢下线包时会检查包中的用户ID是否存在session,若是存在则给客户端发送踢下线包并删除session。
在实际中,踢下线功能还有不少细节问题须要注意。
又回到用户前后登陆同一台接口机的状况:
图中踢下线流程是正确的,可是若是步骤10和13调换了顺序(在UDP传输中是常见的)会发生什么?你们能够本身推演一下,后到的踢下线包会把第二次登陆的A’踢下线了。这不是咱们指望的。怎么办呢?
解决方法分几个细节,①接口机在收到13号登陆成功包时,先将session A替换成session A’,而后给客户端A发生踢下线包(避免多处存活致使互相踢下线);②踢下线包中必须包含除用户ID外的其余标识信息,session的惟一标识应该是ID+XXX的形式(我最开始采用的是ID+LoginTime),XXX是为了区分某次的登陆;③接口机在收到踢下线包的时候只要判断ID+XXX是否吻合来决定是否给客户端发踢下线包。
现实状况,问题老是千奇百怪的,好在办法总比问题多。
好比我在项目中遇到过接口机和在线状态服务时间漂移(差几秒)的状况。这样踢下线的惟一标识就不能是用户ID+LoginTime的形式了。能够为每次的登陆生成一个惟一的UUID解决。相似的问题还有不少,再也不赘述。
总结一下,本篇主要介绍了好友系统的总体架构和部分模块的实现方式。分布式系统中各个模块的实现其实并不难,难点主要在于应对复杂网络环境带来的问题(如丢包、时延等)和服务器异常带来的问题(如为了应对服务器宕机会增长服务器冗余度,进而又会引起其它问题)。
好友系统虽然简单,但麻雀虽小五脏俱全,架构设计的各类技术基本都有涉及。例如分层结构、负载均衡、平行扩展、容灾、服务发现、服务器开发框架等方面,后面我会在各个不一样的项目中介绍这些技术,敬请期待。