本篇介绍笔者接触的第一个后台系统,从自身见闻出发,因此涉及的内容相对比较基础,后台大牛请自觉略过。

什么是好友系统?

简单的说,好友系统是维护用户好友关系的系统。我们最熟悉的好友系统案例当属QQ,实际上QQ是一款即时通讯工具,凭着好友系统沉淀了海量的好友关系链,从而铸就了一个坚不可摧的商业帝国。好友系统的重要性可见一斑。

熟悉互联网产品的人都知道,当产品有了一定的用户量,往往会开发一个好友系统。其主要目的是增加用户粘性(有了好友就会常来)或者增加社区活跃度(有了好友就会多交流)。

而我的后台开发生涯就是从这样一个系统开始的。

那时候,好友系统对于我们团队大部分人来说,都是一个全新的事物,因为我们大部分人都是应届生。整个系统的架构自然不是我们一群黄毛小孩所能创造。当年的架构图已经找不到了,但是凭着一点记忆和多年来的经验积累,还是可以把当年的架构勾勒出来。

如图,好友系统的架构是常见的3层结构,包括接入层、逻辑层和数据层。

我们先从数据层讲起。

因为我们对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解决。类似的问题还有很多,不再赘述。

总结一下,本篇主要介绍了好友系统的整体架构和部分模块的实现方式。分布式系统中各个模块的实现其实并不难,难点主要在于应对复杂网络环境带来的问题(如丢包、时延等)和服务器异常带来的问题(如为了应对服务器宕机会增加服务器冗余度,进而又会引发其它问题)。

好友系统虽然简单,但麻雀虽小五脏俱全,架构设计的各种技术基本都有涉及。例如分层结构、负载均衡、平行扩展、容灾、服务发现、服务器开发框架等方面,后面我会在各个不同的项目中介绍这些技术,敬请期待。

QQ是怎样创造出来的?——解密好友系统的设计的更多相关文章

  1. QQ协议的TEA加解密算法

    QQ通讯协议里的加解密算法. #include <stdio.h> #include <stdlib.h> #include <memory.h> #include ...

  2. 全面解密QQ红包技术方案:架构、技术实现、移动端优化、创新玩法等

    本文来自腾讯QQ技术团队工程师许灵锋.周海发的技术分享. 一.引言 自 2015 年春节以来,QQ 春节红包经历了企业红包(2015 年).刷一刷红包(2016 年)和 AR 红包(2017 年)几个 ...

  3. 纪勇破解QQ号问题

    试题描述 来到了新学校,才相处不到一个月,jy就对oyjy一见钟情.于是向oyjy问QQ号.当然身为创(实)新(验)一班的同学,oyjy还是要考考jy.她给了纪勇一个加密后的QQ号,让纪勇解密,解密规 ...

  4. java.security.InvalidKeyException: Illegal key size aes解密失败

    使用微信时定期提示:java.security.InvalidKeyException: Illegal key size和 com.qq.weixin.mp.aes.AesException: ae ...

  5. 新开了一个ABP交流的QQ群(579765441 ),欢迎加入

    因为ABP架构设计交流群人数一直爆满,很多想交流ABP的朋友无法加进群里, 刚新建了一个QQ群,群号579765441 (ABP架构设计交流群2),欢迎对ABP感兴趣的朋友加入. 欢迎加QQ群: AB ...

  6. 关于玩QQ消息导入导出功能的感想!

    今天玩了一下QQ的导入导出聊天记录的功能,感觉自己有些白痴,因为作为一个软件开发人员对自己平时使用的软件的功能掌握的不够,别说其他的任何东西了就连功能性的操作有些也不知道更别说熟练或精通了,这不是一个 ...

  7. [C# 网络编程系列]专题九:实现类似QQ的即时通信程序

    转自:http://www.cnblogs.com/zhili/archive/2012/09/23/2666987.html 引言: 前面专题中介绍了UDP.TCP和P2P编程,并且通过一些小的示例 ...

  8. 2017-8-2新开了一个ABP交流的QQ群(291304962 ),欢迎加入

    因为ABP架构设计交流群人数一直爆满,很多想交流ABP的朋友无法加进群里, 刚新建了一个QQ群,群号291304962(ABP架构设计交流群3),欢迎对ABP感兴趣的朋友加入. 欢迎加QQ群: ABP ...

  9. 详解C# 网络编程系列:实现类似QQ的即时通信程序

    https://www.jb51.net/article/101289.htm 引言: 前面专题中介绍了UDP.TCP和P2P编程,并且通过一些小的示例来让大家更好的理解它们的工作原理以及怎样.Net ...

随机推荐

  1. [NOIp2014] luogu P2296 寻找道路

    不知道是因为我菜还是别的,最近老是看错题. 题目描述 在有向图 GGG 中,每条边的长度均为 1,现给定起点和终点,请你在图中找一条从起点到终点的路径,该路径满足以下条件: 路径上的所有点的出边所指向 ...

  2. win10系统plsql卡顿、菜单闪烁解决办法

    右键快捷方式--属性--兼容性: 设置为以win7模式运行,以管理员模式运行.如图:

  3. 【Spring Cloud】全家桶介绍(一)

    一.微服务架构 1.微服务架构简介 1.1.分布式:不同的功能模块部署在不同的服务器上,减轻网站高并发带来的压力. 1.2.集群:多台服务器上部署相同应用构成一个集群,通过负载均衡共同向外提供服务. ...

  4. openssl之aes对称加密

    AES:密码学中的高级加密标准(Advanced Encryption Standard,AES),又称 Rijndael加密法. 对称加密:用同一个密码  加密/解密  文件. 使用openssl中 ...

  5. Solr导入MongoDB数据

    数据导入方式: 全量导入和增量导入: query 是全量导入时,把你的数据中查到的数据全部导入,deltaImportQuery 和 deltaQuery 是增量导入数据所需要的两个查询语句.delt ...

  6. 查看线上日志利器less

    less实用命令 搜索 很多关于命令的解释有点令人困惑,因为前字,forward是向前,before也是前面. 上表示backward 下表示forward 向下搜索 / - 使用一个模式进行搜索,并 ...

  7. JVM(2) Java内存溢出异常

    在Java虚拟机运行时数据区中,除了程序计数器之外,虚拟机栈.本地方法栈.方法区和Java堆都有发生OutOfMemoryError(简称OOM)异常的可能. 一.Java堆溢出 Java堆用于存储对 ...

  8. Python中Linux开发的技巧

    Python的Linux基础目录 操作系统  Windows和Linux的区别  常用基本命令1.操作系统 1 操作系统的作用:向上支持应用软件的运行,向下控制硬件,软件和硬件的过渡层Linux的版本 ...

  9. 一个基于Net Core3.0的WPF框架Hello World实例

    目录 一个基于Net Core3.0的WPF框架Hello World实例 1.创建WPF解决方案 1.1 创建Net Core版本的WPF工程 1.2 指定项目名称,路径,解决方案名称 2. 依赖库 ...

  10. .NET项目中实现多工程文件共用的方法

    一处开发,多处同步编辑使用,并且发布时各个项目均可独立 一.直接编辑项目工程文件 .csproj 具体实现为:编辑 .csproj 文件,在<ItemGroup>中添加新的 <Con ...