使用Tair实现即时通信场景中的多端同步

随着即时通信(Instant Messaging)场景和客户端种类的不断丰富,多端通信已经成为普遍趋势。本文为您介绍一种使用云数据库 Tair(兼容 Redis)在IM场景中实现多端同步的方案。

消息存储模型

通常,IM系统的核心架构分为三个部分:消息管理模块、消息同步模块、通知模块。这三个模块的作用如下:

  • 消息管理模块主要负责接收和存储消息。

  • 消息同步模块主要负责存储和推送下行消息数据及其状态。

  • 通知模块主要负责维护第三方通道和通知功能。

消息管理模块的核心是消息存储模型,存储模型的选型直接影响着消息同步模块的实现。消息、会话、会话与消息组织关系的实现方式在业界各主流IM系统中都不尽相同,但无外乎两种形式:写扩散读聚合、读扩散写聚合。读、写扩散是消息在群组会话中的存储形式,其详细说明如下。

读扩散与写扩散

  • 在读扩散场景中,消息归属于会话,相当于数据库中存储着一张conversation_message表,其中包含该会话产生的所有消息。这种存储形式的好处是消息入库效率高,只保存会话与消息的绑定关系即可。

  • 在写扩散场景中,会话产生的消息投递到message_inbox表中,该表类似于个人邮件的收件箱,其中保存着个人的所有会话,会话中的消息按其产生的时间顺序排列。这种存储形式的好处是能实现灵活的消息状态管理,会话中的每条消息在面向不同的接收者时可以呈现出不同的状态。

如果采用读扩散的方式,在大并发修改数据的场景下,数据一致性处理效率和数据变更效率会成为系统性能瓶颈。因此,下文介绍的案例采用写扩散的方式实现消息存储模型,以更高的存储成本支持更高的更新性能。

消息同步模块

多端同步的核心问题在于多端数据的一致性,IM系统需要记录消息的顺序和每个端的同步点,从而实现消息的最终一致性。

既然采用写扩散的方式来记录消息,系统需要:

  • 为每个用户创建一个message_inbox,用于储存该用户的消息。

  • 为每一条消息创建一个自增的sync_id,用于记录消息的顺序。

  • 记录用户在每个客户端上的同步ID。

通过对用户在各客户端上的数据进行对比和同步,就可以实现多端数据同步,详细的实现方式如下。

  1. 提炼数据结构。从IM系统中的各类事件中提炼出统一的消息数据结构,这些事件包括新消息、已读消息、增删会话信息等。消息数据结构示例如下:

    struct message {
      int type; // 业务类型
      string data; // 业务数据
    }
  2. 进行存储产品选型。选型依据主要有以下两点:

    • 系统需要为message_inbox中的每条消息分配一个自增的sync_id,所以用于存储消息数据的产品需要能实现原子递增队列。

    • 完整的消息需要在IM系统的消息管理模块中保存到持久化存储(例如PolarDB)中,而message_inbox数据则无需持久化存储,只需存储一段时间(例如一周)即可,所以对存储的容量要求并不高。

    支持计数器功能和Sorted Sets结构的云数据库 Tair(兼容 Redis)正好能满足上述要求。

  3. 通过云数据库 Tair(兼容 Redis)的Hash结构来存储每个用户在客户端上的同步ID。

场景案例

下图基于一个案例展示了多端同步的详细实现方式。

多端同步示例

说明

图中的Bob为虚拟的用户名。

新消息入库以后,推送消息逻辑被触发,系统根据用户名获取到所有客户端设备的当前点位,然后从消息队列中获取历史点位到最新点位间的所有消息,再将其推送到客户端设备。推送完成后,更新设备的当前点位信息。关键步骤的示例代码如下。

  1. 新消息入库:

    sync_id = INCR bob
    ZADD bob $sync_id message:{type:new_message, data:"{msgid:991,cid:123,text:"hello"}"}
  2. 获取消息范围:

    ZRangeByScore bob 100103 100310
  3. 获取客户端设备的点位:

    HGETALL bob
  4. 加入或更新客户端设备信息:

    HSET bob dev_1001 100103
    HSET bob dev_1002 100202

总结

IM通信已经成为互联网环境中最常见通信方式之一,借助云数据库 Tair(兼容 Redis)丰富的数据结构,您可以构建出高可用的IM系统。不仅是本文提到的消息同步模块,IM系统的消息存储模块也可以使用Redis进行加速,最终构建出支持大规模访问的可靠IM系统。