IM 消息中除了点对点的私信消息和群消息外,还有由 “系统” 发给用户的 “系统消息”。
系统消息通常包括两类:一类是由系统单独发给一个用户的私信系统消息,比如用户下单或支付后,系统会发消息通知到用户;一类是由系统群发给很多用户的广播系统消息,比如通知本周所有在平台下单的用户来领取奖品之类的。
广播系统消息,因为需要短时群发大量消息,会对系统瞬时产生很高的负载,这是 IM 系统设计的关键;根据广播消息的量级不同,广播系统消息的落地方案也会有所不同。
今天我们基于 IM 系统的分层架构,分析私信系统消息和广播系统消息的处理逻辑。
一、私信系统消息
私信系统消息处理流程与点对点的私信处理流程类似,但更简单许多,见下图。
IM 系统消息对实时性和可靠性的要求不高,而且系统消息经常需要进行业务迭代,所以这一部分业务需要放在 Extlogic 服务中进行处理。
- 当业务平台需要向用户推送私信系统消息时,就向 MQ 发送一条消息,比如,当用户被关注了、用户支付了等事件被触发;“平台业务” 是整个的业务系统,比如,电商系统、外卖系统等;IM 可以被看做是整个业务系统能链接到终端用户的通道工具,MQ 对平台业务系统和 IM 系统进行了解耦;
- Extlogic 作为下游服务,对 MQ 进行消费;Extlogic 拿到一条消息后,首先进行落库处理,即通过调用数据访问层 Das 写 “云系统消息库” 和 “联系人库” ;这里需要注意,在点对点的私信消息处理中,写消息库和联系人库是分别需要写两条,这里只写一条即可,毕竟 “系统” 不是 用户,不需要站在 “系统” 的维度进行存储;
- 系统消息落库成功以后,Extlogic 访问路由层 Router ,获取用户在线数据,如果用户离线,则结束处理流程;如果用户在线,Extlogic 则将系统消息推送给相应的 Entry 节点,由 Entry 将系统消息推送到用户客户端;
- 对于非常重要的系统消息类型,需要客户端回复 ACK 包进行确认;而对于一般的系统消息,服务端只向客户端推送即可,允许推送阶段的消息丢失,毕竟消息已经落库,将来客户端一定会拉取到。
这就是普通的私信系统消息的逻辑处理流程,并不复杂,下面我们重点分析广播系统消息。
二、广播系统消息
上面说过,广播系统消息是一对多的方式,即一条消息内容广播给很多用户,具有瞬时高并发的特点。按上面 “私信系统消息” 的逻辑处理流程,来应对广播系统消息是否可以呢?
在电商 IM 系统的实践中,对于广播 “几万量级” 的系统消息,按私信系统消息的处理流程,是完全OK的,当量级增加时,就会出现系统异常和用户体验的问题。
我们分析一下上面 “私信系统消息” 的处理流程:
广播系统消息时,“平台业务” 会将大量的用户id封装成消息写入到 MQ 中,然后由 Extlogic 进行消费; Extlogic 和 Das 都是无状态服务,当需要消费大量 MQ 消息时,对 Extlogic 和 Das 进行线性水平扩容即可;核心问题在于对 “云系统消息库” 和 “联系人库” 的写操作,当对数据库瞬间产生大量写入的时候,数据库会因高负载导致写入高延时,可能失败也可能超时;“云系统消息库” 和 “联系人库” 是两个数据库,无法通过数据库本身保证其业务逻辑的原子性。
- 当写 “云系统消息库” 和 “联系人库” 都成功或都失败时,业务逻辑没有问题;
- 当写 “云系统消息库” 成功,写 “联系人库” 失败时,用户登录后没有 “未读数” 引导用户拉取系统消息,但用户的系统消息聊天窗口中是产生了新的消息;有新消息产生,但却没有通知到用户,这样的体验是很糟糕的;
- 当写 “云系统消息库” 失败,写 “联系人库” 成功时,用户登录后会看到一个 “未读数”,但是真正进入系统消息聊天窗口时,却看不到新的消息;站在用户的角度,用户会感觉被耍了,这样的体验更糟糕。
在公司业务规模迅速扩张,每天都会多次广播系统消息时,产品和运营肯定会责成研发团队尽快解决该问题;本着架构设计 “降本增效” 的原则,如何合理设计广播系统消息的改进方案呢?
- 对 MQ 消费进行限速:数据的高负载是由 Extlogic 高速消费 MQ 导致的,在消费时限制消费流量是否可行呢?技术上完全没有问题,但产品是有问题的;试想:中午 12:30 广播系统消息,是为了用户在午饭后打开手机消遣时能恰好看到该广播消息;对 MQ 消费限速后,虽然用户在 13:30 成功收到了消息,但消息对用户的触达率是很受影响的。
- 扩容数据库:数据库的高负载通过对其进行扩容,的确会有效解决问题;但稳定扩容一个现有记录数在千万甚至亿级的数据库,绝对是一个浩大的工程,既要完成扩容,又必须保证线上业务稳定运行,没有至少两周时间,是很难完成的;研发同学可以等,但产品和运营不会等。
- 引入缓存:在同等量级的情况下,缓存可以抗住数据的高并发写入,可以解决数据库高负载的问题;但需要明白,缓存的本质是提供高并发的读,而非写,同时缓存中数据的可靠性远远低于数据库,如何保证缓存和数据库的一致性,会涉及调整上层大量的业务逻辑。
- 增大数据库超时:在实践中,系统的异常主要是写数据库的超时,那么增大超时时间是否可行呢?比如由原来的 2 秒超时,调整到10 秒;这是一种典型的治标不治本的解决思路,实践中会发现,即使超时时间设定为 20 秒,问题仍然存在。
- 分布式事务:该问题的本质其实就是分布式事务的问题,那么引入分布式事务的解决方案是否可行呢?比如 2PC / 3PC、TCC、Saga 等。分布式事务解决方案,在业务开发应用中,经常会被采取能避免则尽量避免的原子,原因是其性能低和复杂度高,在没有其他更好的解决方案的情况下,可以作为兜底策略。
广播系统消息的落地方案,在实践中到底是如何设计的呢?
分析一下用户群体,大部分用户在广播系统消息时是处于离线状态的,只有非常小的一部分用户在线;所以,在广播操作时,只需要写 “云系统消息库” 一个库即可,当用户登录时,触发对 “联系人库” 的写入,完成两个数据库之间的原子性逻辑。见下图。
- 广播系统消息时,“平台业务” 封装要广播的用户id写入 MQ,下游 Extlogic 进行消费;
- 对于非常少量的在线用户,仍然走 “私信系统消息” 的处理流程;
- 对于离线用户,通过调用 Das 完成对 “云系统消息库” 的写入;
- 当用户登录时,Logic 封装 “登录” 事件写入 MQ,Extlogic 消费时,完成 “云系统消息库” 和 “联系人库” 之间的数据对齐操作。
对齐 “云系统消息库” 和 “联系人库”,是将用户未读的系统消息写入到联系人表中,同时将最后一条云系统消息内容写入到联系人表中;这一步并不复杂,首先根据联系人表中的最新消息查询 “云系统消息库” 最近记录,然后与 “联系人库” 数据做对比,若存在差异,修复即可。
该解决方案采用的是 “压力均摊” 的核心思路,因为用户登录操作是分散的,不会对 IM 系统的联系人核心库造成冲击。
同时,我们可以从该解决方案中,抽象出一种通用的分布式事务解决思路,即:写数据时,只写一个库,在读数据发生前,触发两个库的对齐操作,完成数据的最终一致性。
最后,总结文中关键:
- IM 系统消息从业务上包括:私信系统消息和广播系统消息;
- 私信系统消息处理逻辑与点对点私信类似,由 Extlogic 完成业务处理,分别写 “云系统消息库” 和 “联系人库”;
- 广播系统消息处理逻辑,在广播量级较低时,可采用私信系统消息方式;在广播量级较高时,通过 “压力均摊” 的思路设计方案,即:广播发生时,只写 “云系统消息库” ,当用户登录时,触发对 “联系人库” 的写操作,对齐两个数据库之间登录用户的数据差异。
- 抽象通用的分布式事务解决思路:写数据时,只写一个库;在读数据发生前,触发两个库的对齐操作,完成数据的最终一致性。
作者:棕生 | 来源:公众号——架构之魂
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。