一、引言
前些天所在部门出去团建,于是公司行政和 HR 拉了一个微信群,发布一些跟团和集合信息。
当我正在查看途经路线和团建行程时,忽然一条带着喜意的消息扑面而来,消息上赫然带着八个大字:恭喜发财,大吉大利。
抢红包!!原来是公司领导在群里发了个红包,于是引得群员哄抢,气氛其乐融融。
毕竟,团不团建无所谓,不上班就很快乐;抢多抢少无所谓,有钱进就很开心。
打工人果然是最容易满足的生物!
我看着群里嬉戏打闹的聊天,心中陷入了沉思:微信这个集齐了陌生人聊天、文件分享和抢红包功能的群聊设计确实有点意思,如果在面试或者工作中让我们设计一个群聊系统,需要从哪些方面来考虑呢?
群聊系统设计
面试官:微信作为 10 亿用户级别的全民 App,有用过吧?
我:(内心 OS:说没用过你也不会相信啊~)当然,亲爱的面试官,我经常使用微信来接收工作消息和文件,并且经常在上面处理工作内容。
面试官:(内心 OS:这小伙子工作意识很强嘛,加分!)OK,微信的群聊功能是微信里面核心的一个能力,它可以将数百个好友或陌生人放进一个群空间,如果让你设计一个用户量为 10 亿用户的群聊系统,你会怎么设计呢?
二、系统需求
1.系统特点与功能需求
我:首先群聊功能是社交应用的核心能力之一,它允许用户创建自己的社交圈子,与家人、朋友或共同兴趣爱好者进行友好地交流。
以下是群聊系统常见的几个功能:
创建群聊:用户可以创建新的聊天群组,邀请其他好友用户加入或与陌生人面对面建群。
群组管理:群主和管理员能够管理群成员,设置规则和权限。
消息发送和接收:允许群成员发送文本、图片、音频、视频等多种类型的消息,并推送给所有群成员。
实时通信:消息应该能够快速传递,确保实时互动。
抢红包:用户在群聊中发送任意个数和金额的红包,群成员可以抢到随机金额的红包。
2.非功能需求
除了功能需要,当我们面对 10 亿微信用户每天都可能使用建群功能的情景时,还需要处理大规模的用户并发。
这就引出了系统的非功能需求,包括:
高并发:系统需要支持大量用户同时创建和使用群组,以确保无延迟的用户体验。
高性能:快速消息传递、即时响应,是数字社交的关键。
海量存储:系统必须可扩展,以容纳用户生成的海量消息文本、图片及音视频数据。
面试官:嗯,不错,那你可以简要概述一下这几个常用的功能吗?
3.核心组件
我:好的,我们首先做系统的概要设计,这里涉及到群聊系统的核心组件和基本业务的概要说明。
1)核心组件
群聊系统中,会涉及到如下核心组件和协议。
客户端:接收手机或 PC 端微信群聊的消息,并实时传输给后台服务器;
Websocket传输协议:支持客户端和后台服务端的实时交互,开销低,实时性高,常用于微信、QQ 等 IM 系统通信系统;
长连接集群:与客户端进行 Websocket 长连接的系统集群,并将消息通过中间件转发到应用服务器;
消息处理服务器集群:提供实时消息的处理能力,包括数据存储、查询、与数据库交互等;
消息推送服务器集群:这是信息的中转站,负责将消息传递给正确的群组成员;
数据库服务器集群:用于存储用户文本数据、图片的缩略图、音视频元数据等;
分布式文件存储集群:存储用户图片、音视频等文件数据。
2)业务概要说明
在业务概要说明里,我们关注用户的交互方式和数据存储......
面试官:稍等一下,群聊系统的好友建群功能比较简单,拉好友列表存数据就可以了!你用过面对面建群吧,可以简要说一下如何设计面对面建群功能吗?
我:(内心 OS,还好之前在吃饭时用过面对面建群结账,不然就G了),好的,群聊系统除了拉好友建群外,还支持面对面建群的能力。
4.面对面建群
用户发起面对面建群后,系统支持输入一个 4 位数的随机码,周围的用户输入同一个随机码便可加入同一个群聊,面对面建群功能通常涉及数据表设计和核心业务交互流程如下。
1)数据库表设计
User 表:存储用户信息,包括用户 ID、昵称、头像等。
Group 表:存储群组信息,包括群 ID、群名称、创建者 ID、群成员个数等。
GroupMember 表:关联用户和群组,包括用户 ID 和群 ID。
RandomCode 表:存储面对面建群的随机码和关联的群 ID。
2)核心业务交互流程
用户 A 在手机端应用中发起面对面建群,并输入一个随机码,校验通过后,等待周围(50 米之内)的用户加入。此时,系统将用户信息以 HashMap 的方式存入缓存中,并设置过期时间为 3min。
{随机码,用户列表[用户A(ID、名称、头像)]}
用户 B 在另一个手机端发起面对面建群,输入指定的随机码,如果该用户周围有这样的随机码,则进入同一个群聊等待页面,并可以看到其它群员的头像和昵称信息。
此时,系统除了根据随机码获取所有用户信息,也会实时更新缓存里的用户信息。
成员A进群
当第一个用户点击进入该群时,就可以加入群聊,系统将生成的随机码保存在 RandomCode 表中,并关联到新创建的群 ID,更新群成员的个数。
然后,系统将用户信息和新生成的群聊信息存储在 Group、GroupMember 表中,并实时更新群成员个数。
成员B加入
然后,B 用户带着随机码加入群聊时,手机客户端向服务器后端发送请求,验证随机码是否有效。后台服务检查随机码是否存在于缓存中,如果存在,则校验通过。
然后,根据 Group 中的成员个数,来判断当前群成员是否满员(目前普通用户创建的群聊人数最多为 500 人)。
如果验证通过,后台将用户 B 添加到群成员表 GroupMember 中,并返回成功响应。
面试官:如果有多个用户同时加入,MySQL 数据库如何保证群成员不会超过最大值呢?
我:有两种方式可以解决。一个是通过 MySQL 的事务,将获取 Group 群成员数和插入 GroupMember 表操作放在同一个事务里,但是这样可能带来锁表的问题,性能较差。
另一种方式是采用 Redis 的原子性命令incr 来记录群聊的个数,其中 key 为群聊ID,value 为当前群成员个数。
当新增群员时,首先将该群聊的人数通过 incr 命令加一,然后获取群成员个数。如果群员个数大于最大值,则减一后返回群成员已满的提示。
使用 Redis 的好处是可以快速响应,并且可以利用 Redis 的原子特性避免并发问题,在电商系统中也常常使用类似的策略来防止超卖问题。
位置算法
同时,在面对面建群的过程中相当重要的能力是标识用户的区域,比如 50 米以内。这个可以用到 Redis 的 GeoHash 算法,来获取一个范围内的所有用户信息。
由于篇幅有限,这里不展开赘述,想了解更多位置算法相关的细节,可以看我之前的文章:听说你会架构设计?来,弄一个公交&地铁乘车系统。
面试官:嗯不错,那你再讲一下群聊系统里的消息发送和接收吧!
5.消息发送与接收
我:当某个成员在微信群里发言,系统需要处理消息的分发、通知其他成员、以及确保消息的显示。
在群聊系统中保存和展示用户的图片、视频或音频数据时,通常需要将元数据和文件分开存储。
其中元数据存储在 MySQL 集群,文件数据存储在分布式对象存储集群中。
1)交互流程
消息发送和接收的时序图如下所示:
用户A在群中发送一条带有图片、视频或音频的消息。
移动客户端应用将消息内容和媒体文件上传到服务器后端。
服务器后端接收到消息和媒体文件后,将消息内容存储到 Message 表中,同时将媒体文件存储到分布式文件存储集群中。在 Message 表里,不仅记录了媒体文件的 MediaID,以便关联消息和媒体;还记录了缩略图、视频封面图等等。
服务器后端会向所有群成员广播这条消息。移动客户端应用接收到消息后,会根据消息类型(文本、图片、视频、音频)加载对应的展示方式。
当用户点击查看图片、视频或音频缩略图时,客户端应用会根据 MediaID 到对象存储集群中获取对应的媒体文件路径,并将其展示给用户。
2)消息存储和展示
除了上述建群功能中提到的用户表和群组表以外,存储元数据还需要以下表结构:
Message表: 用于存储消息,每个消息都有一个唯一的 MessageID,消息类型(文本、图片、视频、音频),消息内容(文字、图片缩略图、视频封面图等),发送者 UserID、接收群 GroupID、发送时间等字段。
Media表:存储用户上传的图片、视频、音频等媒体数据。每个媒体文件都有一个唯一的 MediaID,文件路径、上传者 UserID、上传时间等字段。
MessageState表: 用于存储用户消息状态,包括 MessageID、用户 ID、是否已读等。在消息推送时,通过这张表计算未读数,统一推送给用户,并在离线用户的手机上展示一个小数字代表消息未读数。
面试官:我们时常看到群聊有 n 个未读消息,这个是怎么设计的呢?
我:MessageState 表记录了用户的未读消息数,想要获取用户的消息未读数时,只需要客户端调用一下接口查询即可获取,这个接口将每个群的未读个数加起来,统一返回给客户端,然后借助手机的 SDK 推送功能加载到用户手机上。
面试官:就这么简单吗,可以优化一下不?
我:(内心 OS,性能确实很差,就等着你问呢)是的,我们需要优化一下,首先 MySQL 查询 select count 类型的语句时,都会触发全表扫描,所以每次加载消息未读数都很慢。
为了查询性能考虑,我们可以将用户的消息数量存入 Redis,并实时记录一个未读数值。并且,当未读数大于 99 时,就将未读数值置为 100 且不再增加。
当推送用户消息时,只要未读数为 100,就将推送消息数设置为 99+,以此来提升存储的性能和交互的效率。
面试官:嗯,目前几乎所有的消息推送功能都是这么设计的。那你再说一下 10 亿用户的群聊系统应该如何在高并发,海量数据下保证高性能和高可用吧!
我:我想到了几个点,比如采用集群部署、消息队列、多线程、缓存等。
集群部署:可扩展
在群聊系统中,我们用到了分布式可扩展的思想,无论是长连接服务、消息推送服务,还是数据库以及分布式文件存储服务,都是集群部署。
一方面防止单体故障,另一方面可以根据业务来进行弹性伸缩,提升了系统的高可用性。
消息队列:异步、削峰
在消息推送时,由于消息量和用户量很多,所以我们将消息放到消息队列(比如 Kafka)中异步进行消费和推送,来进行流量削峰,防止数据太多将服务打崩。
多线程
在消息写入和消费时,可以多线程操作,一方面节省了硬件开销,不至于部署太多机器。另一方面提升了效率,毕竟多个流水线工作肯定比单打独斗更快。
其它优化
缓存前面已经说到了,除了建群时记录 code,加群时记录群成员数,我们还可以缓存群聊里最近一段时间的消息,防止每个用户都去 DB 拉取一遍数据,这提升了消息查阅的效率。
除此之外,为了节省成本,可以记录流量的高峰时间段,根据时间段来定时扩缩节点(当然,这只是为了成本考虑,在实际业务中这点开销不算什么大问题)。
6.小结
面试官:嗯不错,实际上的架构中也没有节省这些资源,而是把重心放在了用户体验上。(看了看表)OK,那今天的面试就到这,你有什么想问的吗?
我:(内心 OS,有点慌,但是不能表现出来)由于时间有限,之前对系统高并发、高性能的设计,以及对海量数据的处理浅尝辄止,这在系统设计的面试中占比如何?
面试官:整体想得比较全,但是还不够细节。当然,也可能是时间不充分的原因,已经还不错了!
我:(内心 OS,借你吉言)再想问一下,如果我把这些写出来,会有读者给我点赞、分享、加入在看吗?
面试官:……
结语
群聊系统是社交应用的核心功能之一,每个社交产品几乎都有着群聊系统的身影:包括但不限于 QQ、微信、抖音、小红书等。
上述介绍的技术细节可能只是群聊系统的冰山一角,像常见的抢红包、群内音视频通话这些核心功能也充斥着大量的技术难点。
但正是有了这些功能,才让我们使用的 App 变得更加有趣。而这,可能也是技术和架构的魅力所在吧~