即时通讯系统引入保证消息有序的机制,务必会影响性能,增加开发复杂度。
引入 IM 大佬的一句话:
1. 理想状态
服务端接收到消息的顺序与用户点击发送消息的按钮顺序完全一致,用户接收到的消息的顺序也和发送时的顺序完全一致。
这是几乎不可能实现的!为什么?
1. 网络延迟
不可能按下发送消息的按钮后,消息就会马上到达服务器。每个用户的网速可能不一样。
OK,那我按发送时的时间戳来排序不就行了?
2. 时钟不一致
其实每个人本地的时钟不一定都是同一个,任何物理时钟都有漂移,任何时钟同步协议都受制于网络延迟。这导致我们对时间的测量永远存在一个不确定的误差区间,区间内的事件顺序无法判定。
既然如此,那我们退一步,一个即时通讯系统,以微信举例,是不是真的需要保证消息的全局有序?
答案是不用。我们只需要保证每个会话内的消息有序就行,也就是用户1和用户2的这个聊天会话中,我们看到的消息是一样的,保证两个人发送的消息在这个会话内有序!
再退一步,那我只保证每个人发送的消息有序行不行?
答案是不行!比如用户A发送了两条消息MSG1和MSG2,用户B发送了MSG3,如果在页面上显示了MSG3 -> MSG1 -> MSG2,这样也算是乱序。
2. 如何实现会话内有序?
路径1:客户端 -> 服务端
1. 建立 TCP 长连接
我们都知道,TCP 长连接保证消息可靠性、有序性。
但是,实际并不能只依靠 TCP 连接来保证消息的发送顺序!在弱网环境,比如火车、电梯,可能会快速断网、切网,原来的 TCP 连接断开,连上一条新的TCP 连接,我发送消息MSG1在旧连接上没发出去,消息MSG2 在新的TCP 连接上发送成功了,这就导致了消息乱序。
2. 客户端生成序列号
学习 TCP 保证消息有序的方案,发送消息前,客户端生成本地递增序列号localSeq,这个序列号在本地存储的,服务端也会记录上一条接收成功了localSeq,然后在服务端判断当前消息是不是有序的。
如果乱序,可以通知客户端重发消息。
也就是:客户端发消息 ->服务端接收并校验localSeq -> 服务端发送ack或要求重发
如果一来一回严重影响性能,可以参考使用批量ack等。
路径2:服务端处理入库
因为我们的目标是保证会话有序,所以消息到达服务端时的第一件事应该是分发消息,按照会话的 ID 进行分发,在我的项目中,私聊会话格式:private_{user_id1}_{user_id2}
,user_id1 小于 user_id2。
分发最简单的就是把相同会话的消息放到一个队列,按顺序处理。
这里可以引入消息队列这个中间件,像 Kafka 的 Partition 分区机制 和 RocketMQ Queue 机制。
这里消息队列的选型我更倾向于 RMQ,至于为什么下面会说。
OK,现在正式开始处理消息了,比如入库、加上全局 ID这些。消息服务从队列里一条一条取出消息来处理,这样就一定保证消息被有序处理了吗?
不一定,因为多线程的存在,消息的处理速度可能不一样,先来的消息可能由于种种原因,导致晚于后面的消息处理。
为了保证消息被有序处理,相同会话的消息需要由单线程处理【当然会影响性能】,而 RMQ 天然支持相同QUEUE的消息被串行处理,只需要实现 MessageListenerOrderly
。所以这也是为什么要选择 RMQ 的一个重要的原因吧。
路径3:服务端 -> 客户端
消息已经被加上了序号,其实消息的有序已经实现了,接下来要保证的就是消息可靠抵达客户端和按照消息的序列显示给用户就行。
保证可靠性方面,参考【推拉结合 + ACK 机制 + 消息重发】。