虽然大部分情况下,聊天、直播互动等即时消息业务能接受“小误差的消息乱序”,但某些特定场景下,可能需要 IM 服务能保证绝对的时序。

比如发送方的某一个行为同时触发了多条消息,而且这多条消息在业务层面需要严格按照触发的时序来投递。

一个例子:用户 A 给用户 B 发送最后一条分手消息同时勾上了“取关对方”的选项,这个时候可能会同时产生“发消息”和“取关”两条消息,如果服务端处理时,把“取关”这条信令消息先做了处理,就可能导致那条“发出的消息”由于“取关”了,发送失败的情况。

对于这种情况,我们一般可以调整实现方式,在发送方对多个请求进行业务层合并,多条消息合并成一条;也可以让发送方通过单发送线程和单 TCP 连接能保证两条消息有序到达。

但即使 IM 服务端接收时有序,由于多线程处理的原因,真正处理或者下推时还是可能出现时序错乱的问题,解决这种“需要保证多条消息绝对有序性”可以通过 IM 服务端包内整流来实现。

比如:我们在实现离线推送时,在网关机启动后会自动订阅一个本 IP 的 Topic,当用户上线时,网关机会告知业务层用户有上线操作,这时业务层会把这个用户的多条离线消息 pub 给这个用户连接的那个网关机订阅的 Topic,当网关机收到这些消息后,再通过长连接推送给用户,整个过程大概是下图这样的。

但是很多时候会出现 Redis 队列组件的 Sharding 和网关机多线程消费处理导致乱序的情况,这样,如果一些信令(比如删除所有会话)的操作被乱序推送给客户端,可能就会造成端上的逻辑错误。

然后再说一下离线推送服务端整流的过程:

首先,生产者为每个消息包生成一个 packageID,为包内的每条消息加个有序自增的 seqId。

其次,消费者根据每条消息的 packageID 和 seqID 进行整流,最终执行模块只有在一定超时时间内完整有序地收到所有消息才执行最终操作,否则将根据业务需要触发重试或者直接放弃操作。

通过服务端整流,服务端包内整流大概就是图中这个样子,我们要做的是在最终服务器取到 TCP 连接后下推的时候,根据包的 ID,对一定时间内的消息做一个整流和排序,这样即使服务端处理多条消息时出现乱序,仍然可以在最终推送给客户端时整流为有序的。

消息接收端整流

携带不同序号的消息到达接收端后,可能会出现“先产生的消息后到”“后产生的消息先到”等问题,消息接收端的整流就是解决这样的一个问题的。

消息客户端本地整流的方式可以根据具体业务的特点来实现,目前业界比较常见的实现方式比较简单,步骤如下:

下推消息时,连同消息和序号一起推送给接收方;

接收方收到消息后进行判定,如果当前消息序号大于前一条消息的序号就将当前消息追加在会话里;

否则继续往前查找倒数第二条、第三条等,一直查找到恰好小于当前推送消息的那条消息,然后插入在其后展示。

全局序号生成器

关于全局序号生成器这个,用到的地方非常多,也不限于IM,其他的还有充值的订单号,商品订单号,消费单号等等。

  • 可以通过多种方式来实现,常见的比如 Redis 的原子自增命令 incr
  • DB 自带的自增 id
  • 或者类似 Twitter 的 snowflake 算法
  • “时间相关”的分布式序号生成服务等。

小结

对于如何保持消息的时序一致性的关键点在于需要找到一个时序基准来标识每一条消息的顺序。

这个时序基准可以通过全局的序号生成器来确定,常见的实现方式包括支持单调自增序号的资源生成,或者分布式时间相关的 ID 生成服务生成,两种方式各有一些限制,不过,你都可以根据业务自身的特征来进行选择。

有了通过时序基准确定的消息序号,由于 IM 服务器差异和多线程处理的方式,不能保证先服务端的消息一定能先推到接收方,可以通过“服务端包内整流”机制来保证需要“严格有序”的批量消息的正确执行,或者接收方根据消息序号来进行消息本地整流,从而确保多接收方的最终一致性。