一、与消息相关的主要场景
1、存储和离线消息。
现在的IM系统,消息都要落地存储。这样如果接收消息的用户不在线,等他下次上线时,能获取到消息数据。
2、消息漫游
消息漫游包括主要两种场景,
(1)用户新安装IM软件,要能看到以前的聊天记录
(2)聊天软件有PC版和App版,在App上聊的天,打开PC版要能够看到
二、不同场景读取 消息关键点
1、拉取离线消息
每个用户打开App就需要拉取离线,网络中断重连后要拉取离线,收到消息序列号不连续也要拉取离线,拉取离线消息是一个高频操作 。离线消息包括单聊、群聊、控制类等消息,消息类型类型众多。因此离线消息需要以用户ID(多端情况下需要以端)为检索维度。说的直白一点,就是每个人(端)都需要一个收件箱,拉离线消息就是把个人(端)收件箱里的消息取到客户端。
2、消息漫游
消息漫游的典型使用场景是,打开某个会话(单聊、群聊、公众号),下拉界面,客户端向服务端请求这个会话的聊天数据。消息漫游需要以会话为检索维度。消息漫游拉取数据的频率相对较低。我们把这类获取消息的方式成为拉取历史消息。
三、存储消息关键点
1、离线消息
离线消息读取频繁(写也有一定压力),但是检索逻辑简单(参看《一个海量在线用户即时通讯系统(IM)的完整设计》拉取离线消息章节)。我们采用内存数据库(Redis)存储,主要结构使用SortedSet(可以有更高效的存储结构,但Redis不支持)。对于群消息,采用扩散写方式(一条群消息给每个群成员都写一份)。按照消息接受者ID水平分库。
2、历史消息
历史消息的访问频率低,但是每条消息都需要存储,我们采用关系型数据库(MySQL)存储,重点考虑写入效率。对于群消息,采用扩散读方式(每条群消息只写一条记录)。按照消息发送者ID(单聊),或群ID(群聊)进行水平分库。
四、消息存取方案
1、离线消息
存储离线消息。按照消息接收者ID(toID),取模Hash分库(也可以用一致性Hash)。每个用户创建一个SortedSet结构的Key,用于存储离线消息。离线消息按照时间先后顺序排列即可。SortedSet添加一个元素时间复杂度是O(log(N)),N 是有序集的基数,由于离线消息的msgid是有序的,所以实际插入时间复杂度很可能退化为O(1)。
读取离线消息。离线消息读取策略参看《一个海量在线用户即时通讯系统(IM)的完整设计》拉取离线消息章节。理论上读取离线消息的时间复杂度为O(log(N)+M), N 为离线消息的条数, M 为一次读取消息的条数。实际上,由于离线消息从有序集的头部开始读取,实际时间复杂度比这个值低。
2、历史消息
历史消息分为两大类,单聊消息、群聊消息。
单聊消息按照发送者ID(fromId)水平(取模Hash)分库,存到一张数据表(例如叫msg_user_send)中。核心字段包括msgid(消息ID),fromId(发送者Id),toId(接收者Id),content(消息内容)。
拉取单聊历史消息时(假设拉取userId1跟userId2的聊天),分别读取两人给对方发送的消息(因为分库原因,两人发送的消息可能分布在不同数据库中),然后进行Merge。
群聊消息按照群ID(groupId)水平(取模Hash)分库,存到一张表中(例如叫:msg_group)。核心字段包括msgid(消息ID),fromId(发送者Id),groupId(群Id),content(消息内容)。
另外一张msg_group_user表记录用户加入群的时间,如下图。某个人(如张三)加入群的时间,相当于一个游标,群消息表中,这个游标之后的聊天消息,是这个人(张三)能够查看的数据(当然,也可以做查看加入群之前若干条消息)。
拉取群历史消息,直接倒序读取这个群消息表数据即可。
由于MySQL和Redis都采用了水平分库,存储能力几乎可以线性扩展!是不是这样就足够了呢?答案是否定的,优化永远没有尽头。如果我在非洲某个国家登录系统,从北京的机房读取消息数据显然不太合适!如何让数据靠近用户,是一个更加有挑战的问题。
本文分享自微信公众号 - 普通程序员(farmerbrag)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。