如何设计一个IM群聊架构

989 阅读4分钟

背景

阅读本篇文章前建议先读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。

站在群聊的场景下,消息收发密度远高于单聊。如果每一次产生新消息,都去更新每个用户的会话列表,就会产生严重的「写扩散」问题,这是无法接受的资源浪费。

那么先来分析会话列表有哪几种使用情况,再看应该如何解决:

  1. 用户在线

    客户端根据接受的消息动态调整会话列表即可,不依赖服务端的会话列表数据。

  2. 用户不在线

    离线任务维护即可

  3. 用户重新上线

    客户端需要从服务端拉取会话列表。

根据情况分析可知:需要考虑的就是第三种情况。

那么既然有拉取这个动作,就可以根据拉取这个动作,被动的去将用户的所有群会话排列成有序的,具体流程如下:

  1. 获取用户的所有群(可做限制,避免资源浪费,例如最新的100个群聊)
  2. 获取群消息的lastMsgId,按照lastMsgId将群会话排序,形成一个有序的群会话列表
  3. 将有序的群会话列表和有序的单聊会话列表进行融合,形成最终的会话列表

通过分析,「群会话列表」和「单聊会话列表」存在一个合并的过程,所以可以拆分成两个sortedSet来维护。

key: prefix_{type}:{uid} value: {会话Id} score: {lastMsgId}

三. 如何维护离线列表

离线列表和每个群成员的会话列表存在相同的问题,就是群消息收发密度太高。如果用户不在线时,把消息存入到它自己的离线列表,同样存在一个「写扩散」的问题。

有两个思路可以解决这个问题

  1. 客户端记录用户已读的最后一个群消息id,主动去服务端拉取最新的记录,直到拉取的消息id和记录的消息id重合即可停止拉取
  2. 不维护离线消息列表,用户点开群聊后再进行最新一屏数据拉取,不点开则仅显示未读计数,不拉取数据。

从用户的体验上来说其实两种方式差不多,但是第二种方式按需请求,对服务端的压力会小很多,个人建议使用第二种。

四. 如何计算未读计数

群聊的未读计数和单聊的相似,只需要维护群消息的总数,以及每个用户的已读数量做相减即可。

这里数据结构可以复用单聊的未读计数数据结构,以及数据更新方式。

群消息总数只有一份,用户的已读数量是用户触发时才变更,所以都不存在写扩散的问题。

小结

群聊和单聊的数据结构设计和消息流转过程是非常相似的,重点就是围绕业务场景进行懒拉取/数据复用等方式,来避免群聊过程中可能产生写扩散的问题。

至此,单聊+群聊的IM系统设计都讲完了,底层还有很多实现细节也很有趣,如果感兴趣的话可以去实际实现一下。