百万消息量IM系统技术要点分享

我们仔细观察就能发现,生活中的任何类型互联网服务都有 IM 系统的存在。

比如:

    1)基础性服务类-腾讯新闻(评论消息);

    2)商务应用类-钉钉(审批工作流通知);

    3)交流娱乐类-QQ/微信(私聊群聊 &讨论组 &朋友圈);

    4)互联网自媒体-抖音快手(点赞打赏通知)。

在这些林林总总的互联网生态产品里,即时消息系统作为底层能力,在确保业务正常与用户体验优化上,始终扮演了至关重要的角色。

所以,现如今的互联网产品中,即时通讯技术已经不仅限于传统IM聊天工具本身,它早已通过有形或无形的方式嵌入到了各种形式的互联网应用当中。IM技术(或者说即时通讯技术)对于很多开发者来说,确实是必不好可少的领域知识,不可或缺。

典型的IM系统通常需要满足四点能力:高可靠性、高可用性、实时性和有序性。

以我的这个项目来说,架构设设计要点主要是:

    1)微服务:拆分为用户微服务 &消息连接服务 &消息业务服务;

    2)存储架构:兼容性能与资源开销,选择 reids&mysql;

    3)高可用:可以支撑起高并发场景,选择 Spring 提供的 websocket;

    4)支持多端消息同步:app 端、web 端、微信公众号、小程序消息;

    5)支持在线与离线消息场景。

理解读扩散和写扩散

我们举个例子说明什么是读扩散,什么是写扩散:

一个群聊“相亲相爱一家人”,成员:爸爸、妈妈、哥哥、姐姐和我(共 5 人)。

因为你最近交到女朋友了,所以发了一条消息“我脱单了”到群里面,那么自然希望爸爸妈妈哥哥姐姐四个亲人都能收到了。

正常逻辑下,群聊消息发送的流程应该是这样:

    1)遍历群聊的成员并发送消息;

    2)查询每个成员的在线状态;

    3)成员不在线的存储离线;

    4)成员在线的实时推送。

问题在于:如果第4步发生异常,群友会丢失消息,那么会导致有家人不知道“你脱单了”,造成催婚的严重后果。

所以优化的方案是:不管群员是否在线,都要先存储消息。

按照上面的思路,优化后的群消息流程如下:

    1)遍历群聊的成员并发送消息;

    2)群聊所有人都存一份;

    3)查询每个成员的在线状态;

    4)在线的实时推送。

以上优化后的方案,便是所谓的“写扩散”了。

问题在于:每个人都存一份相同的“你脱单了”的消息,对磁盘和带宽造成了很大的浪费(这就是写扩散的最大弊端)。

所以优化的方案是:群消息实体存储一份,用户只存消息 ID 索引。

于是再次优化后的发送群消息流程如下:

    1)遍历群聊的成员并发送消息;

    2)先存一份消息实体;

    3)然后群聊所有人都存一份消息实体的 ID 引用;

    4)查询每个成员的在线状态;

    5)在线的实时推送。

二次优化后的方案,便是所谓的“读扩散”了。

小结一下:

    1)读扩散:读取操作很重,写入操作很轻,资源消耗相对小一些;

    2)写扩散:读取操作很轻,写入操作很重,资源消耗相对大一些。

消息实体模型:

常见的消息业务,可以抽象为几个实体模型概念:用户/用户关系/用户设备/用户连接状态/消息/消息队列。

实体模型概念解释:

用户实体:

    1)用户->用户终端设备:每个用户能够多端登录并收发消息;

    2)用户->消息:考虑到读扩散,每个用户与消息的关系都是 1:n;

    3)用户->消息队列:考虑到读扩散,每个用户都会维护自己的一份“消息列表”(1:1),如果考虑到扩容,甚至可以开辟一份消息溢出列表接收超出“消息列表”容量的消息数据(此时是 1:n);

    4)用户->用户连接状态:考虑到用户能够多端登录,那么 app/web 都会有对应的在线状态信息(1:n);

    5)用户->联系人关系:考虑到用户最终以某种业务联系到一起,组成多份联系人关系,最终形成私聊或者群聊(1:n);

联系人关系(主要由业务决定用户与用户之间的关系),比如说:

    1)某个家庭下有多少人,这个家庭群聊就有多少人;

    2)在 ToB 场景,在钉钉企业版里,我们往往有企业群聊这个存在。

消息实体:

消息->消息队列:考虑到读扩散,消息最终归属于一个或多个消息队列里,因此群聊场景它会分布在不同的消息队列里。

消息队列实体:

消息队列:确切说是消息引用队列,它里面的索引元素最终指向具体的消息实体对象。

用户连接状态:

    1)对于 app 端:网络原因导致断线,或者用户手动 kill 掉应用进程,都属于离线;

    2)对于 web 端:网络原因导致浏览器断网,或者用户手动关闭标签页,都属于离线;

    3)对于公众号:无法分别离线在线;

    4)对于小程序:无法分别离线在线。

用户终端设备:

客户端一般是 Android&IOS,web 端一般是浏览器,还有其他灵活的 WebView(公众号/小程序)。即时通讯开发

消息的存储方案

对于消息存储方案,本质上只有三种方案:要么放在内存、要么放在磁盘、要么两者结合存储(据说大公司为了优化性能,活跃的消息数据都是放在内存里面的,毕竟有钱~)。

下面分别解析主要方案的优点与弊端:

    1)方案一:考虑性能,数据全部放到 redis 进行存储;

    2)方案二:考虑资源,数据用 redis + mysql 进行存储。

对于方案一:redis

前提:用户 & 联系人关系,由于是业务数据,因此统一默认使用关系型数据库存储。

解释如下:

    1)用户发消息;

    2)redis 创建一条实体数据 &一个实体数据计时器;

    3)redis 在 B 用户的用户队列 添加实体数据引用;

    4)B 用户拉取消息(后续 5.2 会提及拉模式)。

实现方案:

    1)用户队列,zset(score 确保有序性);

    2)消息实体列表,hash(msg_id 确保唯一性);

    3)消息实体计数器,hash(支持群聊消息的引用次数,倒计时到零时则删除实体列表的对应消息,以节省资源)。

优点是:内存操作,响应性能好

弊端是:

    1)内存消耗巨大,eg:除非大厂,小公司的服务器的宝贵内存资源是耗不起业务的,随着业务增长,不想拓展资源,就需要手动清理数据了;

    2)受 redis 容灾性策略影响较大,如果 redis 宕机,直接导致数据丢失(可以使用 redis 的集群部署/哨兵机制/主从复制等手段解决)。

方案二:redis+mysql

前提:用户 & 联系人关系,由于是业务数据,因此统一默认使用关系型数据库存储。

释如下:

    1)用户发消息;

    2)mysql 创建一条实体数据;

    3)redis 在 B 用户的用户队列 添加实体数据引用;

    4)B 用户拉取消息(下文会提及拉模式)。

实现方案:

    1)用户队列,zset(score 确保有序性);

    2)消息实体列表,转移到 mysql(表主键 id 确保唯一性);

    3)消息实体计数器,hash(删除这个概念,因为磁盘可用总资源远远高于内存总资源,哪怕一直存放 mysql 数据库,在业务量百万级别时也不会有大问题,如果是巨大体量业务就需要考虑分表分库处理检索数据的性能了)。

优点是:

    1)抽离了数据量最大的消息实体,大大节省了内存资源;

    2)磁盘资源易于拓展 ,便宜实用。

弊端是:磁盘读取操作,响应性能较差(从产品设计的角度出发,你维护的这套 IM 系统究竟是强 IM 还是弱 IM)。

猜你喜欢

转载自blog.csdn.net/wecloud1314/article/details/125428209