QQ是怎样创造出来的?——解密好友系统的设计
本篇介绍笔者接触的第一个后台系统,从自身见闻出发,因此涉及的内容相对比较基础,后台大牛请自觉略过。
什么是好友系统?
简单的说,好友系统是维护用户好友关系的系统。我们最熟悉的好友系统案例当属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是怎样创造出来的?——解密好友系统的设计的更多相关文章
- QQ协议的TEA加解密算法
QQ通讯协议里的加解密算法. #include <stdio.h> #include <stdlib.h> #include <memory.h> #include ...
- 全面解密QQ红包技术方案:架构、技术实现、移动端优化、创新玩法等
本文来自腾讯QQ技术团队工程师许灵锋.周海发的技术分享. 一.引言 自 2015 年春节以来,QQ 春节红包经历了企业红包(2015 年).刷一刷红包(2016 年)和 AR 红包(2017 年)几个 ...
- 纪勇破解QQ号问题
试题描述 来到了新学校,才相处不到一个月,jy就对oyjy一见钟情.于是向oyjy问QQ号.当然身为创(实)新(验)一班的同学,oyjy还是要考考jy.她给了纪勇一个加密后的QQ号,让纪勇解密,解密规 ...
- java.security.InvalidKeyException: Illegal key size aes解密失败
使用微信时定期提示:java.security.InvalidKeyException: Illegal key size和 com.qq.weixin.mp.aes.AesException: ae ...
- 新开了一个ABP交流的QQ群(579765441 ),欢迎加入
因为ABP架构设计交流群人数一直爆满,很多想交流ABP的朋友无法加进群里, 刚新建了一个QQ群,群号579765441 (ABP架构设计交流群2),欢迎对ABP感兴趣的朋友加入. 欢迎加QQ群: AB ...
- 关于玩QQ消息导入导出功能的感想!
今天玩了一下QQ的导入导出聊天记录的功能,感觉自己有些白痴,因为作为一个软件开发人员对自己平时使用的软件的功能掌握的不够,别说其他的任何东西了就连功能性的操作有些也不知道更别说熟练或精通了,这不是一个 ...
- [C# 网络编程系列]专题九:实现类似QQ的即时通信程序
转自:http://www.cnblogs.com/zhili/archive/2012/09/23/2666987.html 引言: 前面专题中介绍了UDP.TCP和P2P编程,并且通过一些小的示例 ...
- 2017-8-2新开了一个ABP交流的QQ群(291304962 ),欢迎加入
因为ABP架构设计交流群人数一直爆满,很多想交流ABP的朋友无法加进群里, 刚新建了一个QQ群,群号291304962(ABP架构设计交流群3),欢迎对ABP感兴趣的朋友加入. 欢迎加QQ群: ABP ...
- 详解C# 网络编程系列:实现类似QQ的即时通信程序
https://www.jb51.net/article/101289.htm 引言: 前面专题中介绍了UDP.TCP和P2P编程,并且通过一些小的示例来让大家更好的理解它们的工作原理以及怎样.Net ...
随机推荐
- [LUOGU3413] SAC#1 - 萌数
题目背景 本题由世界上最蒟蒻最辣鸡最撒比的SOL提供. 寂月城网站是完美信息教室的官网.地址:http://191.101.11.174/mgzd . 题目描述 辣鸡蒟蒻SOL是一个傻逼,他居然觉得数 ...
- 存储物理页属性的PFN数据库
Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html 存储物理页属性的PFN数据库 一.PFN的基础概念 页帧:即CPU ...
- vue 页面滚动到原位置
哈哈哈,昨天登QQ的时候,意外发现有人看了我写的博客,居然还加了我,这就激起了我内心的小波澜啊 公司最近在做电商,用的前端框架依然是VUE 矩MAX(微信公众号)可以搜的到哦,安卓商店或苹果AppSt ...
- VSCode 安装 code 命令
VSCode 提供 code 命令直接从命令行中打开文件目录,此时需要先安装 code 命令. 1.首先打开 VSCode 2.使用 command + shift + p (注意window 下使用 ...
- insert into select 引起的 "子查询返回的值不止一个。当子查询跟随在**之后,或子查询用作表达式时,这种情况是不允许的"
目录 1.事故现场 1.1 在使用 Insert into Table2 select * from Table1 将表1的数据插入到表2时,报错如下: 1.2 sql 语句 2.推测 3.解决方案 ...
- 百万年薪python之路 -- 生成器
1.生成器 #本质就是迭代器 1.1 生成器的构建方式 在python中有三种方式来创建生成器: 1.通过生成器函数 2.通过生成器推导式 3.python内置函数或者模块提供 1.2 生 ...
- cxf 调用 .net webservice
1. 问题背景 现在我们两套语言并行,其中必然会涉及到不同系统的相互访问. .net 的会员信息是用 webservice 提供服务的.那如何对现有 .net webservi ...
- Html.CSS.JavaScript 学习经验
HTML里面 不要使用 document.getElementsByName() 来获取 元素,会出错. 使用 document.getElementById()更好一些. substring()首字 ...
- Leetcode Tags(2)Array
一.448. Find All Numbers Disappeared in an Array 给定一个范围在 1 ≤ a[i] ≤ n ( n = 数组大小 ) 的 整型数组,数组中的元素一些出现了 ...
- 修改List<Map<String, Object>>的值
List<Map<String, Object>> aList = new ArrayList(); //加入一个Map元素Map map = new HashMap();m ...