前面我们分析了 “点对点消息” 的实现逻辑(IM专题:分层架构IM系统(12)—消息收发逻辑实现),今天分析 IM 群消息逻辑。
在之前的文章分析过,“私信消息”(即点对点消息)属于【基础功能需求】,“群消息” 属于【扩展功能需求】, “群消息” 功能是基于 “私信消息” 进行实现的,当 “私信消息” 功能完成后,实现 “群消息” 则轻而易举!
我们先分析 “群消息” 的领域模型,再分析其逻辑流程。
一、群消息领域模型
领域模型是对现实世界中对象的可视化表示;群消息是 IM 系统的一个问题领域,其模型可以通过 “关系表” 非常直观的进行表达。
群消息的领域模型包括 “群消息” 和 “群联系人”,其设计理念在之前的一篇文章(IM专题:分层架构IM系统(10)—Das领域模型设计)中曾深入分析过。
1、群消息
群消息关系表在设计时,需要重点考虑两个因素:一是消息定制化,二是分表存储。
先说群消息的定制化:假设张三和李四都在群里,当群里有人发出一条消息时,如果张三将这条消息删除了,并不表示其他群成员就看不到这条消息了;如果李四将这条消息点赞了,也并不表示其他群成员都点赞了该条消息。这就是群消息的定制化,虽然站在群的角度看,这是一条消息,但站在每个群成员的角度看,消息就不是一条了,它是跟每个群成员绑定的。
再说分表存储:每一个用户可能有很多的群,每一个群可能有非常多的群消息;当群消息总的数量非常大的时候,我们往往会对其进行分表处理。这个时候就需要考虑一个关键问题了:每一个用户的所有群的所有的消息都应该落在一张表中,避免在查询时对所有的表进行遍历。
基于上述两个因素的考虑,对群消息关系表设计如下:
单单分析表结构比较干涩,我们直接举例,如下:
假设一个群有三个成员,分别是 uid=101、uid=102 和 uid=103,用户 uid=101 在群里发送消息 “大家好!”,那么需要在群消息关系表中保存记录如下:
是的,该群有三个成员,一条群消息需要站在每个群成员的维度分别保存一条!
msg_id 是群消息表的全局主键,该字段值由服务端生成。
to_uid 是群消息的接收方,是记录的核心字段,其他字段属性都是围绕 to_uid 进行描述的。
from_uid 是群消息的发送方,group_id 是当前群的 id;站在群的角度分析,from_uid 和 to_uid 是可以相同的。
content 是群消息的内容,client_msg_id 是由群消息发送方客户端生成的,是在发送方客户端范围内唯一的消息id,该 id 用来实现消息的幂等性。
timestamp 是由服务端生成的群消息的发送时间。
del_flag 是群消息逻辑删除标记,这个字段体现出了群消息的定制化能力。
群消息关系结构的设计理念是针对每一条群消息,站在每一个群成员(to_uid)的维度分别存储一条记录,那么在分表时,可以非常自然的选择 to_uid 作为索引字段,这样同一用户的所有群消息会落在同一张表中。
另外,我们会发现:content、client_msg_id、timestamp 等字段内容是一样的,为了节省存储减少冗余,可以单独在 “消息内容表” 中进行存储。
分析到这里,喜欢思考的同学肯定会提出疑问:如果一个群有100个成员,难道每发一条消息需要保存100条记录吗? 这就是 IM 的 “写扩散模型”,在一定写扩散能力范围内和产品需求下,是完全可以这样实现的;想一下,为什么微信的群成员会限制在 500 人呢? 当然,当 “写扩散” 带来的耗时和负载无法承受时,可以通过 “读扩散模型” 来实现(在后续文章中进行分析)。
2、群联系人
在 IM 系统中,“联系人” 除了是自然人之外,群也可以是当前登录用户的 “联系人”;另外,“系统” 本身也是一类 “联系人”,我们在后面文章中会分析到!
群联系人领域模型在设计的时候,和群消息领域模型类似,也需要考虑相同的两个因素:定制化需求和分表存储需求。
张三和李四在同一个群里,这个群就是张三和李四的联系人,当张三删除这个 “群联系人” 时,李四的 “群联系人” 仍然是存在的,这就是群联系人模型的定制化需求。
当所有的群联系人数量很大时,需要分表存储;同样,需要同一个用户的所有的群联系人需要落在同一张表中,避免轮询所有的群联系人表,这就是群联系人模型的分表存储需求。
基于这两个因素的考虑,对群联系人关系表设计如下:
举例说明,假设一个群有三个成员,分别是 uid=101、uid=102 和 uid=103,用户 uid=101 在群里发送消息 “大家好!”,那么需要在群联系人关系表中保存记录如下:
和群消息表设计理念一致,群是该群中所有成员的联系人,需要根据群成员的数量保存相应的记录;在这里,群成员有三个人,所以需要保存三条群联系人的记录。
群联系人表主要是更新操作,在该表中要始终存储群的最后一条消息。
to_uid 是群最后一条消息的接收方,该字段也是记录的核心字段,其他字段属性都是围绕着 to_uid 进行描述的。
from_uid 是群最后一条消息的发送方,group_id 是当前群的 id ;站在群的角度分析,from_uid 和 to_uid 是可以相同的。
content 是该群最后一条消息的内容,timestamp 是群最后一条消息的发送时间,该值由服务端生成。
del_flag 是群联系人逻辑删除的标记,这个字段体现出了群联系人的定制化能力。
unread 是当前用户(to_uid)对该群未读的消息数。
群联系人表针对每一位群成员(to_uid)保存记录,当群联系人记录数量很高时,将 to_uid 作为分表索引字段,这样同一用户所有的群联系人记录会落在同一张表中。
二、群消息收发逻辑流程
开发经验丰富的同学,理解上述 “群消息模型” 和 “群联系人模型” 之后,可以非常轻松地推导出分层架构下群消息的收发逻辑流程,见下图。
群消息收发的流程设计图与点对点消息流程设计图非常相似,不同的是:点对点消息的接收方是一个客户端,群消息的接收方是多个客户端,Logic 需要对多个接收方进行遍历推送。
同样的,为了实现 IM 消息的 “及时性” 和 “可靠性”,群消息的收发流程也包括三个阶段和三重保障;在前面文章(IM专题:分层架构IM系统(12)—消息收发逻辑实现)中有详细分析,这里直接上流程,不再累述分析。
以用户 uid=101 向群(有 101、102 和 103 三位群成员)发送消息为例。
1、生产消息阶段
- 客户端 101 成功登录后,基于与入口层 Entry 之间的长连接,发送群消息请求到 Entry;
- Entry 基于与业务逻辑层 Logic 之间的 RPC 连接,将消息请求转发到 Logic;
- Logic 首先对群消息内容进行 “反作弊” 过滤,通过调用 Spam 服务识别出违反平台规则的消息;若消息违规,则整个流程止步于此;
- 对于正常的消息,Logic 做的第二件事情是通过调用数据访问层 Das 将群消息落库,具体分别写入 “群消息库” 三条记录和 “群联系人库” 三条记录;
- 消息成功落库后,Logic 构造回复包,通过 Entry 主动建立的 RPC 连接,将回复包发送给 Entry;
- Entry 将回复包发送给客户端 101。
2、推送消息阶段
在推送消息阶段,Logic 首先通过访问群数据库或缓存获取群成员列表(对于活跃群,通过访问缓存来提升性能),排除消息发送方之后,拿到群消息的接收方 [102, 103],执行如下动作:
- Logic 访问路由层 Router,获取消息接收方[102,103] 是否在线和连接的 Entry节点;对于不在线的客户端 uid=102 不在线,Logic 会封装相关数据,发送到 MQ,由 Extlogic 进行离线用户的召回逻辑处理;
- 对于在线的客户端,Logic 基于与 Entry 之间的 RPC 连接将消息发送给 Entry;对于假在线情况,Entry 会直接回调 Logic 的 unreachabled 接口,在该接口中,Logic 一方面会写 Router进行假在线修复,一方面会封装相关数据发送到 MQ,由 Extlogic 进行离线用户的召回逻辑处理;
- 若 Entry 可以在内存中找到客户端的连接句柄,则基于客户端与 Entry 之间的长连接,将消息推送给客户端。
3、确认消息阶段
- 客户端接收到消息后,构造确认包,基于与 Entry 之间的长连接,发送给 Entry;
- Entry 基于与 Logic 之间的 RPC 连接,调用 Logic 的 msg_ack 接口,向 Logic 确认群消息已经被客户端成功接收;如果在一定时间内(比如15秒),Logic 没有接收到客户端发送的该条消息的确认请求时,根据消息的级别和产品规则,要么对该消息进行重复推送,要么认为客户端已经离线,此时 Logic 一方面写 Router,一方面封装相关数据发送给 MQ,由 Extlogic 进行离线用户的召回逻辑处理。
可以发现,在群消息的逻辑流程中,“生产消息阶段” 和 “确认消息阶段” 与点对点消息的逻辑流程完全一致,只是在 “推送消息阶段”,增加了一个循环而已;所以,在写代码的时候,如果抽象和复用做得足够好,分分钟就可以实现群消息的逻辑流程。
最后,总结文中关键:
- 群消息表存储所有群消息记录,每发送一条群消息,需要站在每一个群成员维度分别存储一条记录,实现群消息的定制化需求和分表存储需求;
- 群联系人表存储用户的群联系人数据,群是所有群成员的联系人,需要为每一位群成员存储一条记录,实现群联系人的定制化需求和分表存储需求;
- 分层架构 IM 系统的群消息收发流程(和点对点消息流程类似),包括三个阶段:
- 生产消息阶段,完成客户端成功发送消息到服务端的过程;
- 推送消息阶段,完成服务端推送消息到群成员客户端的过程;
- 确认消息阶段,完成客户端接收到消息后向服务端回复确认的过程。
作者:棕生 | 来源:公众号——架构之魂
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。