随着即时通信(Instant Messaging)场景和客户端种类的不断丰富,多端通信已经成为普遍趋势。本文为您介绍一种使用云数据库 Tair(兼容 Redis)在IM场景中实现多端同步的方案。
消息存储模型
通常,IM系统的核心架构分为三个部分:消息管理模块、消息同步模块、通知模块。这三个模块的作用如下:
消息管理模块主要负责接收和存储消息。
消息同步模块主要负责存储和推送下行消息数据及其状态。
通知模块主要负责维护第三方通道和通知功能。
消息管理模块的核心是消息存储模型,存储模型的选型直接影响着消息同步模块的实现。消息、会话、会话与消息组织关系的实现方式在业界各主流IM系统中都不尽相同,但无外乎两种形式:写扩散读聚合、读扩散写聚合。读、写扩散是消息在群组会话中的存储形式,其详细说明如下。
在读扩散场景中,消息归属于会话,相当于数据库中存储着一张conversation_message表,其中包含该会话产生的所有消息。这种存储形式的好处是消息入库效率高,只保存会话与消息的绑定关系即可。
在写扩散场景中,会话产生的消息投递到message_inbox表中,该表类似于个人邮件的收件箱,其中保存着个人的所有会话,会话中的消息按其产生的时间顺序排列。这种存储形式的好处是能实现灵活的消息状态管理,会话中的每条消息在面向不同的接收者时可以呈现出不同的状态。
如果采用读扩散的方式,在大并发修改数据的场景下,数据一致性处理效率和数据变更效率会成为系统性能瓶颈。因此,下文介绍的案例采用写扩散的方式实现消息存储模型,以更高的存储成本支持更高的更新性能。
消息同步模块
多端同步的核心问题在于多端数据的一致性,IM系统需要记录消息的顺序和每个端的同步点,从而实现消息的最终一致性。
既然采用写扩散的方式来记录消息,系统需要:
为每个用户创建一个message_inbox,用于储存该用户的消息。
为每一条消息创建一个自增的sync_id,用于记录消息的顺序。
记录用户在每个客户端上的同步ID。
通过对用户在各客户端上的数据进行对比和同步,就可以实现多端数据同步,详细的实现方式如下。
提炼数据结构。从IM系统中的各类事件中提炼出统一的消息数据结构,这些事件包括新消息、已读消息、增删会话信息等。消息数据结构示例如下:
struct message { int type; // 业务类型 string data; // 业务数据 }
进行存储产品选型。选型依据主要有以下两点:
系统需要为message_inbox中的每条消息分配一个自增的sync_id,所以用于存储消息数据的产品需要能实现原子递增队列。
完整的消息需要在IM系统的消息管理模块中保存到持久化存储(例如PolarDB)中,而message_inbox数据则无需持久化存储,只需存储一段时间(例如一周)即可,所以对存储的容量要求并不高。
支持计数器功能和Sorted Sets结构的云数据库 Tair(兼容 Redis)正好能满足上述要求。
通过云数据库 Tair(兼容 Redis)的Hash结构来存储每个用户在客户端上的同步ID。
场景案例
下图基于一个案例展示了多端同步的详细实现方式。
图中的Bob为虚拟的用户名。
新消息入库以后,推送消息逻辑被触发,系统根据用户名获取到所有客户端设备的当前点位,然后从消息队列中获取历史点位到最新点位间的所有消息,再将其推送到客户端设备。推送完成后,更新设备的当前点位信息。关键步骤的示例代码如下。
新消息入库:
sync_id = INCR bob ZADD bob $sync_id message:{type:new_message, data:"{msgid:991,cid:123,text:"hello"}"}
获取消息范围:
ZRangeByScore bob 100103 100310
获取客户端设备的点位:
HGETALL bob
加入或更新客户端设备信息:
HSET bob dev_1001 100103 HSET bob dev_1002 100202
总结
IM通信已经成为互联网环境中最常见通信方式之一,借助云数据库 Tair(兼容 Redis)丰富的数据结构,您可以构建出高可用的IM系统。不仅是本文提到的消息同步模块,IM系统的消息存储模块也可以使用Redis进行加速,最终构建出支持大规模访问的可靠IM系统。