探讨IMS系统如何高效实现阅读状态
1 前言
IM系统中,特别是在ToB环境,消息的阅读状态或许是一个强需求。本文从服务端角度来探讨阅读状态。能力有限,权当个人笔记,欢迎交流。
对于私聊,消息的阅读状态比较容易实现,在性能和存储上也不存在问题。
但对于群聊,考虑到存储和处理性能,特别当处于一个云环境时,如何高效处理群聊的阅读状态是一个非常值得探讨的话题。
这里提到的“高效”含3个方面:存储空间、处理速度、传输字节数,本文着重探讨存储空间。
我认为第3章的阅读状态存储是本文的主要价值所在。
2 阅读状态交互流程
从IMS角度,笼统地理解客户端包含App和sdk,如没有特别说明,均不做区分。
发送者发送的消息,在接收者阅读消息后,是否要求阅读者通知已读,可能是由系统配置、组织配置、群组配置等决定,也可能由发送者根据业务需求决定。以下的讨论,均假设消息需要阅读状态。
客户端与服务端之间,关于阅读状态的命令只需3个,每个命令含请求和应答。
2.1 通知消息已读(私聊、群聊通用)
当小宝阅读了一条或若干条消息,需向服务端发送消息已读通知:众爱卿发的x+y+z消息,朕已阅
。
服务端收到小宝的已读通知时,需完成以下事项:
- 存储消息的已读状态;
- 返回应答给小宝;
- 向已读列表的消息的
原始发送者
通知消息已读- 私聊的场合,比较好理解,就是发送给私聊的对方;
- 群聊的场合,可很不一样。因为小宝发送的已读消息列表,可能是由众爱卿发送的。考虑这种假设:张三、李四、王五发出的群聊消息,被小宝一下都阅读了,那么小宝发出的已读通知包含的消息列表,需要被IMS分解成3个已读通知(3个不同的消息列表),分别通知给张三、李四、王五,通知内容是
爱卿(不含'"众")发的这些消息,朕已阅
。
2.2 查询消息的未读人数(私聊、群聊通用)
消息的发送者,加载消息列表到聊天窗口时,可能需要展示消息是否被已读;对群聊而言,显示的信息可能是n人未读
的提示,那么需要向服务端查询消息的未读人数,由于客户端可能在UI显示自己发出的多条消息,需支持一次请求查询多条消息。
以未读人数的方式来表示消息的阅读状态,统一了私聊、群聊的查询,使得客户端-服务端间的接口更简单,同时使客户端的实现逻辑更统一:
- 对于私聊,如果未读人数n>0,表示消息未读;
- 对于群聊,直接显示n人未读即可,当然,当n等于0时表示全部已读。
2.3 查询群消息的已读、未读人员清单(群聊)
当客户端希望显示某一条群聊消息的已读、未读人员列表,需向服务端发起查询。
3 探讨如何存储阅读状态
群聊的阅读状态比私聊复杂,因此这里着重讨论群聊的阅读状态。
假设群成员数是n,。各个客户端立即向IMS发送已读通知。服务端需存储每个人的阅读状态,包括那些未读的成员。由于群的成员清单可能变化,比如今天增加了一个成员,则昨天发的消息、与今天发的消息,其接收者列表不一样。
即,同一个群的不同消息,对应的接收者列表可能不一样。
换言之,每一条消息都需要记录完整的接收者列表和已读人员列表。
本章讨论,以假设群成员有640人为前提。
3.1 案1
每一条消息都维护:
- 接收人员列表
receiver_list
- 已读人员列表
read_list
IMS收到一条消息时,用全体群成员构建receiver_list
;
IMS收到群成员对这条消息的已读通知时,将此成员加入到read_list
;
当客户端需要获取此消息的未读人数时,用receiver_list
的个数减去read_list的个数;
当客户端需要获取此消息的已读、未读人员列表时,需用receiver_list
减去read_list
得到未读人员列表。
案1每条消息的存储空间是:
640个ID + 不定数量的已读人员ID
3.2 案2
每一条消息维护:
- 未读人员列表
unread_list
- 已读人员列表
read_list
IMS收到一条消息时,用全体群成员构建unread_list
;
IMS收到群成员对这条消息的已读通知时,将此成员从unread_list
移出,同时加入到read_list
;
当客户端需要获取此消息的未读人数时,直接计算unread_list
的个数;
当客户端需要获取此消息的已读、未读人员列表时,直接返回unread_list
和read_list
。
案2每条消息的存储空间是:
未读人员ID + 已读人员ID,合计640个ID
案2的实现,占用的空间是案1的0.5倍~1.0倍。即案2占用的空间少,但在每次收到客户端的已读通知时,比案1多了一个操作:从unread_list
进行减员。
3.3 案3(我的实现)
3.3.1 探讨3.1节、3.2节的不足
3.1节、3.2节这两种案,都能满足功能需求,但存在巨大的存储浪费。
该群有640人,如果群内聊天每天有1024条消息,人员ID以4字节存储,那么为该群每天的消息阅读状态需要消耗的空间是:
- 3.1方法: 1024 * (640 * 4 + 已读人数 * 4),范围是 2.5MB ~ 5MB,
- 3.2方法: 1024 * 640 * 4,等于2.5MB
这仅仅是一个群在一天之内产生的阅读状态数据,如果是在云平台运行,单此功能消耗的空间,呵呵~~
题外话:如果成员不是用4字节整型存储,而改用字符串,比如"1123356777",那就更可观了。
3.3.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,此数值与3.1节、3.2节对比,有了极大幅度地下降。
方案 | 占用空间最小值 | 占用空间最大值 | 备注 |
---|---|---|---|
案1 | 2.5MB | 5.0MB | |
案2 | 2.5MB | 2.5MB | |
案3 | 2.5KB | 22.5KB | 不到案2的 1%,残暴,太残暴 |
该表格的前提条件:
- 一个群有640人
- 该群连续1024条消息对应的群成员列表是稳定的
退一步考虑,哪怕这1024条消息对应的群成员列表不稳定,中间变化了10次,那么也仅会多出2.5KB * 10即25KB的存储空间,与案1、案2相比仍然有极大优势。
4 如何提高阅读状态的处理速度
- 小宝往公司群发了一条消息
我来给大家介绍一下新来的女同事
,大家立即、马上、瞬间、闪电般地查看消息,感觉迟1秒就会失去秒杀女神的机会一样,意味着一瞬间会有N多条已读通知发送到IMS。对这些消息的处理流程是一样的,可合并这些操作以批量形式进行存储、转发。 - 由于存储消息的阅读状态是一个设置bit的过程,所以不存在互斥的问题,即使在分布式环境也可以放心操作。
- 消息对应的成员列表信息可临时缓存在内存对象内,以减少查询IO,提高效率。
5 阅读状态的通讯交互
虽然服务端采用人员清单和阅读状态分离的设计,但与客户端(不区分App和SDK)通讯时,考虑到降低客户端设计的复杂度,在第2章提到通讯过程不引入这种分离设计。
性能测试
待补足