背景
阅读本篇文章前建议先读IM单聊架构设计
上一篇文章我们分析设计了IM单聊的架构,相信阅读过后,很轻松就能想到群聊和单聊的异同点,以及群聊应该如何设计。
废话不多说,那么本篇就在单聊架构的基础上,根据几个场景来分析群聊应该如何设计。
一. 如何保存消息
群聊与单聊的不同之处顾名思义,就是一个群会话中有很多个成员。
而群消息所有人看到的都是一样的,所以不必为成员去单独维护,即单独存储一份即可。
上篇单聊中讲到会为每一个对话产生一个「会话id」,这个设计其实也是考虑群聊和单聊的数据通用性,一份数据结构群聊单聊都能用。
那么依然使用redis的sortedSet,根据msgId来形成一个有序集合,存储方式我粘贴过来再看下
Key: prefix_session_list:{sessionId}
Value: {msgId}
Score: {msgId}
由于群消息只有一份,而各个成员进群的时间是不同的。
常规逻辑来说,成员只能看到它进群时间点之后的消息,那么怎么把之前的消息进行隐藏呢?
只要有一个数据结构维护群成员入群的信息即可实现,存储可以使用redis的String,方式如下:
Key: prefix:group:join:snapshot:{群Id}:{uid}
Value: {LastMsgId}
这样记录了成员进群前的最后一个msgId,就可以控制「群消息记录」中大于此id的消息才对该成员可见,就达到了一个消息隐藏的目的。
二. 如何更新会话列表
单聊中我们讲过,用户的会话列表是一个sortedSet,score值是每个会话的最后一个msgId。
站在群聊的场景下,消息收发密度远高于单聊。如果每一次产生新消息,都去更新每个用户的会话列表,就会产生严重的「写扩散」问题,这是无法接受的资源浪费。
那么先来分析会话列表有哪几种使用情况,再看应该如何解决:
-
用户在线
客户端根据接受的消息动态调整会话列表即可,不依赖服务端的会话列表数据。
-
用户不在线
离线任务维护即可
-
用户重新上线
客户端需要从服务端拉取会话列表。
根据情况分析可知:需要考虑的就是第三种情况。
那么既然有拉取这个动作,就可以根据拉取这个动作,被动的去将用户的所有群会话排列成有序的,具体流程如下:
- 获取用户的所有群(可做限制,避免资源浪费,例如最新的100个群聊)
- 获取群消息的lastMsgId,按照lastMsgId将群会话排序,形成一个有序的群会话列表
- 将有序的群会话列表和有序的单聊会话列表进行融合,形成最终的会话列表
通过分析,「群会话列表」和「单聊会话列表」存在一个合并的过程,所以可以拆分成两个sortedSet来维护。
key: prefix_{type}:{uid} value: {会话Id} score: {lastMsgId}
三. 如何维护离线列表
离线列表和每个群成员的会话列表存在相同的问题,就是群消息收发密度太高。如果用户不在线时,把消息存入到它自己的离线列表,同样存在一个「写扩散」的问题。
有两个思路可以解决这个问题
- 客户端记录用户已读的最后一个群消息id,主动去服务端拉取最新的记录,直到拉取的消息id和记录的消息id重合即可停止拉取
- 不维护离线消息列表,用户点开群聊后再进行最新一屏数据拉取,不点开则仅显示未读计数,不拉取数据。
从用户的体验上来说其实两种方式差不多,但是第二种方式按需请求,对服务端的压力会小很多,个人建议使用第二种。
四. 如何计算未读计数
群聊的未读计数和单聊的相似,只需要维护群消息的总数,以及每个用户的已读数量做相减即可。
这里数据结构可以复用单聊的未读计数数据结构,以及数据更新方式。
群消息总数只有一份,用户的已读数量是用户触发时才变更,所以都不存在写扩散的问题。
小结
群聊和单聊的数据结构设计和消息流转过程是非常相似的,重点就是围绕业务场景进行懒拉取/数据复用等方式,来避免群聊过程中可能产生写扩散的问题。
至此,单聊+群聊的IM系统设计都讲完了,底层还有很多实现细节也很有趣,如果感兴趣的话可以去实际实现一下。