共计 4347 个字符,预计需要花费 11 分钟才能阅读完成。
1、引言
IM 系统中,特别是在企业应用场景下,消息的已读未读状态是一个强需求。
以阿里的钉钉为例,钉钉的产品定位是用于商务交流,其“强制已读回执”功能,让职场人无法再“假装不在线”、“假装没收到”。更有甚者,钉钉的群聊“强制已读回执”功能,甚至能够知道谁读了消息,谁没有读消息(老板的福音啊)。
▲ 钉钉里的群聊消息已读未读功能效果
功能看起来很酷,但用起来是一言难尽(上班族心里苦 ….)。实际上,技术实现也并不容易。
那么,对于已读未读状态:
- 1)如果是私聊:消息的阅读状态比较容易实现,在性能和存储上也不存在问题;
- 2)如果是群聊:考虑到存储和处理性能,特别当处于一个云环境时,如何高效地处理群聊的已读未读状态是一个非常值得探讨的话题。
这里提到的“高效”含 3 个方面:
- 1)存储空间;
- 2)处理速度;
- 3)传输字节数。
本文将从服务端的角度来探讨已读未读状态,在具体的技术实现上对于存储空间占用方面的思路差异。能力有限,权当个人笔记,欢迎交流。
学习交流:
– 即时通讯 / 推送技术开发交流 5 群:215477170[推荐]
– 移动端 IM 开发入门文章:《新手入门一篇就够:从零开发移动端 IM》
本文已同步发布于“即时通讯技术圈”公众号,欢迎关注:
▲ 本文在公众号上的链接是:https://mp.weixin.qq.com/s/yUkKPOBsdqLlxiFrGmwFRQ,原文链接是:http://www.52im.net/thread-3054-1-1.html
2、内容点评
在收录本文前,Jack Jiang 建议原作者对某些具体的技术点进行更深入的分享,但因作者工作较忙,本文中的某些关键技术点未来的及作进一步展开。
所以,本文可以作为 IM 聊天消息(主要是群聊)中已读未读功能的基本实现思路方面的参考,但不建议盲目迷信文中的结论或方案,避免被一些不够具体的技术指标而误导。
3、相关文章
如果你还想了解更多有关 IM 群聊中已读未读功能的实现逻辑,可以进一步阅读干货文章《IM 群聊消息的已读回执功能该怎么实现?》(强烈推荐)。
如果你对 IM 中的已读未读功能有产品方面的痛点困惑,可以参考一下微信对已读未读功能的设计定位,详见《IM 热门功能思考:为什么微信里没有消息“已读”功能?》。
更多 IM 群聊技术方面的文章详见文本附录部分。
4、已读未读状态交互流程
发送者发送的 IM 聊天消息,在接收者阅读消息后,是否要求阅读者通知已读,可能是由系统配置、组织配置、群组配置等决定,也可能由发送者根据业务需求决定。以下的讨论,均假设消息需要已读未读状态。
客户端与服务端之间,关于阅读状态的命令只需 3 个,每个命令含请求和应答。
4.1 通知消息已读(私聊、群聊通用)
当小宝阅读了一条或若干条消息,需向服务端发送消息已读通知:“众爱卿发的 x +y+ z 消息,朕已阅”。
服务端收到小宝的已读通知时,需完成以下事项:
- 1)存储消息的已读状态;
- 2)返回应答给小宝;
- 3)向已读列表的消息的原始发送者通知消息已读。
对于第“3)”步:
- 1)私聊的场合,比较好理解,就是发送给私聊的对方;
- 2)群聊的场合,可很不一样:因为小宝发送的已读消息列表,可能是由众爱卿发送的。考虑这种假设:张三、李四、王五发出的群聊消息,被小宝一下都阅读了,那么小宝发出的已读通知包含的消息列表,需要被 IMS 分解成 3 个已读通知(3 个不同的消息列表),分别通知给张三、李四、王五,通知内容是“_爱卿(不含 '” 众 ”)发的这些消息,朕已阅_”。
下面是大致的逻辑流程图:
4.2 查询消息的未读人数(私聊、群聊通用)
消息的发送者,加载消息列表到聊天窗口时,可能需要展示消息是否被已读。
对群聊而言,显示的信息可能是 n 人未读的提示,那么需要向服务端查询消息的未读人数,由于客户端可能在 UI 显示自己发出的多条消息,需支持一次请求查询多条消息。
以未读人数的方式来表示消息的阅读状态,统一了私聊、群聊的查询,使得客户端 - 服务端间的接口更简单,同时使客户端的实现逻辑更统一。
就像下面这样:
- 1)对于私聊:如果未读人数 n >0,表示消息未读;
- 2)对于群聊:直接显示 n 人未读即可,当然,当 n 等于 0 时表示全部已读。
4.3 查询群消息的已读、未读人员清单(群聊)
当客户端希望显示某一条群聊消息的已读、未读人员列表,需向服务端发起查询。
大致的逻辑流程图如下:
5、几种具体的已读未读状态存储思路探讨
5.1 基本约定
群聊的阅读状态比私聊复杂,因此这里着重讨论群聊的阅读状态。
假设群成员数是 n,各个客户端立即 IM 服务端发送已读通知。服务端需存储每个人的阅读状态,包括那些未读的成员。由于群的成员清单可能变化,比如今天增加了一个成员,则昨天发的消息、与今天发的消息,其接收者列表不一样。
即:
- 1)同一个群的不同消息,对应的接收者列表可能不一样。
- 2)换言之,每一条消息都需要记录完整的接收者列表和已读人员列表。
为了方便讨论,本章假设群成员有 640 人为前提。
5.2 存储思路 1
每一条消息都维护:
- 1)接收人员列表 receiver_list;
- 2)已读人员列表 read_list。
具体是:
- 1)IM Server 收到一条消息时,用全体群成员构建 receiver_list;
- 2)IM Server 收到群成员对这条消息的已读通知时,将此成员加入到 read_list。
客户端获取此消息的数据:
- 1)当需要获取未读人数时,用 receiver_list 的个数减去 read_list 的个数;
- 2)当需要获取已读、未读人员列表时,需用 receiver_list 减去 read_list 得到未读人员列表。
那么,思路 1 每条消息的存储空间是:
640 个 ID + 不定数量的已读人员 ID
5.3 存储思路 2
每一条消息维护:
- 1)未读人员列表 unread_list;
- 2)已读人员列表 read_list。
具体是:
- 1)IM Server 收到一条消息时,用全体群成员构建 unread_list;
- 2)IM Server 收到群成员对这条消息的已读通知时,将此成员从 unread_list 移出,同时加入到 read_list。
客户端获取此消息的数据:
- 1)当需要获取未读人数时,直接计算 unread_list 的个数;
- 2)当需要获取已读、未读人员列表时,直接返回 unread_list 和 read_list。
那么,思路 2 每条消息的存储空间是:
未读人员 ID + 已读人员 ID,合计 640 个 ID
思路 2 的实现,占用的空间是案 1 的 0.5 倍~1.0 倍。即案 2 占用的空间少,但在每次收到客户端的已读通知时,比案 1 多了一个操作:从 unread_list 进行减员。
5.4 存储思路 3(我的实现)
5.4.1)探讨 5.2 节、5.3 节的不足:
5.2 节、5.3 节这两种思路,都能满足功能需求,但存在巨大的存储浪费。
该群有 640 人,如果群内聊天每天有 1024 条消息,人员 ID 以 4 字节存储计算,那么为该群每天的消息阅读状态需要消耗的空间是:
5.2 节思路 1:1024 * (640 * 4 + 已读人数 * 4),范围是 2.5MB ~ 5MB;
5.3 节思路 2:1024 * 640 * 4,等于 2.5MB。
这仅仅是一个群在一天之内产生的阅读状态数据,如果是在云平台运行,单此功能消耗的空间,呵呵~~
题外话:如果成员不是用 4 字节整型存储,而改用字符串,比如 ”1123356777″,那就更可观了。
5.4.2)如何减少存储空间:
考虑群成员并非时时刻刻都在变化,多数情况下,群成员的列表是相对稳定的,今天的和上周(甚至更久以前)的列表甚至可能是一样的,那么有可能几百条消息,甚至几万条消息对应的群成员列表是相同的。
因此,引出本文的重点思想:
考虑让不同的消息共用群成员列表,即把消息的阅读状态与群成员列表分开存储,并记录它们之间的关联。
假定平均每 1024 条消息共用一个群成员列表,发了 1024 条消息后,群成员变化了,此后需要用新的群成员列表。
那么这一千条消息的阅读状态所占用的空间是:
群成员列表空间 + 1024 条消息的阅读状态:640 * 4 + 1024 * 每条消息的阅读状态所占空间
在具备群成员列表的前提下,如何减少每条消息的阅读状态所占空间?
很自然会想到用 bit 来表示已读人员,因为一个 32 位整型可表示 32 个人的已读状态。bit 的顺序只需与群成员列表的顺序一致即可。
当一条消息没有人已读时,阅读状态占用 0 字节;当群内每个人都阅读时,占用的空间最大,即 640 / 32 = 20 字节。
因此优化之后,这一千条消息的阅读状态所占用的空间,范围是 2.5KB ~ (2.5KB + 1024 * 20B),即 2.5KB ~ 22.5KB,此数值与 5.2 节思路 1、5.3 节思路 2 对比,有了极大幅度地下降。
如下图所示:
该表格的前提条件:
- 1)一个群有 640 人;
- 2)该群连续 1024 条消息对应的群成员列表是稳定的。
退一步考虑,哪怕这 1024 条消息对应的群成员列表不稳定,中间变化了 10 次,那么也仅会多出 2.5KB * 10 即 25KB 的存储空间,与案 1、案 2 相比仍然有极大优势。
6、如何提高已读未读状态的处理速度
小宝往公司群发了一条消息我来给大家介绍一下新来的女同事,大家立即、马上、瞬间、闪电般地查看消息,感觉迟 1 秒就会失去秒杀女神的机会一样,意味着一瞬间会有 N 多条已读通知发送到 IMS。
对这些消息的处理流程是一样的:
- 1)可合并这些操作以批量形式进行存储、转发;
- 2)由于存储消息的阅读状态是一个设置 bit 的过程,所以不存在互斥的问题,即使在分布式环境也可以放心操作;
- 3)消息对应的成员列表信息可临时缓存在内存对象内,以减少查询 IO,提高效率。
附录:更多 IM 群聊技术文章
《快速裂变:见证微信强大后台架构从 0 到 1 的演进历程(一)》
《如何保证 IM 实时消息的“时序性”与“一致性”?》
《IM 单聊和群聊中的在线状态同步应该用“推”还是“拉”?》
《IM 群聊消息如此复杂,如何保证不丢不重?》
《微信后台团队:微信后台异步消息队列的优化升级实践分享》
《移动端 IM 中大规模群消息的推送如何保证效率、实时性?》
《现代 IM 系统中聊天消息的同步和存储方案探讨》
《关于 IM 即时通讯群聊消息的乱序问题讨论》
《IM 群聊消息的已读回执功能该怎么实现?》
《IM 群聊消息究竟是存 1 份 (即扩散读) 还是存多份(即扩散写)?》
《一套高可用、易伸缩、高并发的 IM 群聊、单聊架构方案设计实践》
《[技术脑洞] 如果把 14 亿中国人拉到一个微信群里技术上能实现吗?》
《IM 群聊机制,除了循环去发消息还有什么方式?如何优化?》
《网易云信技术分享:IM 中的万人群聊技术方案实践总结》
《阿里钉钉技术分享:企业级 IM 王者——钉钉在后端架构上的过人之处》
《IM 群聊消息的已读未读功能在存储空间方面的实现思路探讨》
>> 更多同类文章 ……
(本文同步发布于:http://www.52im.net/thread-3054-1-1.html)