即时消息技术剖析与实战
袁武林
微博研发中心技术专家
立即订阅
6503 人已学习
课程目录
已完结 24 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 搞懂“实时交互”的IM技术,将会有什么新机遇?
免费
基础篇 (8讲)
01 | 架构与特性:一个完整的IM系统是怎样的?
02 | 消息收发架构:为你的App,加上实时通信功能
03 | 轮询与长连接:如何解决消息的实时到达问题?
04 | ACK机制:如何保证消息的可靠投递?
05 | 消息序号生成器:如何保证你的消息不会乱序?
06 | HttpDNS和TLS:你的消息聊天真的安全吗?
07 | 分布式锁和原子性:你看到的未读消息提醒是真的吗?
08 | 智能心跳机制:解决网络的不确定性
场景篇 (4讲)
09 | 分布式一致性:让你的消息支持多终端漫游
10 | 自动智能扩缩容:直播互动场景中峰值流量的应对
11 | 期中实战:动手写一个简易版的IM系统
12 | 服务高可用:保证核心链路稳定性的流控和熔断机制
进阶篇 (10讲)
13 | HTTP Tunnel:复杂网络下消息通道高可用设计的思考
14 | 分片上传:如何让你的图片、音视频消息发送得更快?
15 | CDN加速:如何让你的图片、视频、语音消息浏览播放不卡?
16 | APNs:聊一聊第三方系统级消息通道的事
17 | Cache:多级缓存架构在消息系统中的应用
18 | Docker容器化:说一说IM系统中模块水平扩展的实现
19 | 端到端Trace:消息收发链路的监控体系搭建
20 | 存储和并发:万人群聊系统设计中的几个难点
21 | 期末实战:为你的简约版IM系统,加上功能
22 | 答疑解惑:不同即时消息场景下架构实现上的异同
结束语 (1讲)
结束语 | 真正的高贵,不是优于别人,而是优于过去的自己
即时消息技术剖析与实战
登录|注册

05 | 消息序号生成器:如何保证你的消息不会乱序?

袁武林 2019-09-06
你好,我是袁武林。
前面几节课,我们较为系统地介绍了如何解决消息实时到达的问题,也对保证消息可靠投递实战中常用的方式进行了一一讲解。
那么,今天的课程我们继续一起聊一聊,IM 系统设计中另一个比较复杂,但又非常重要的话题:消息收发的一致性。需要提醒的是,我们这里的讲到的一致性,一般来说是指消息的时序一致性。

为什么消息的时序一致性很重要?

对于聊天、直播互动等业务来说,消息的时序代表的是发送方的意见表述和接收方的语义逻辑理解,如果时序一致性不能保证,可能就会造成聊天语义不连贯、容易出现曲解和误会。
你可以想象一下,一个人说话颠三倒四,前言不搭后语的样子,就理解我们为什么要尤其注重消息的时序一致性了。
对于点对点的聊天场景,时序一致性需要保证接收方的接收顺序和发送方的发出顺序一致;而对于群组聊天,时序一致性保证的是群里所有接收人看到的消息展现顺序都一样。

为什么保证消息的时序一致性很困难?

从理论上来说,保持消息的时序一致性貌似并不难。理论上,我们想象的消息收发场景中,只有单一的发送方、单一的接收方。
如果发送方和接收方的消息收发都是单线程操作,并且和 IM 服务端都只有唯一的一个 TCP 连接,来进行消息传输,IM 服务端也只有一个线程来处理消息接收和消息推送。这种场景下,消息的时序一致性是比较容易能得到保障的。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《即时消息技术剖析与实战》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(40)

  • Alexdown
    希望老师多补充一些流程图或代码来帮助理解,光看文字因经验不足,有时很难想象理解,甚至会引发歧义。谢谢

    编辑回复: 感谢您的建议,已增加了两张流程图来具体说明一下在离线消息推送过程中服务端整流的实现机制。

    2019-09-06
    6
    28
  • 深藏Blue
    最近遇到IM相关需求,好像是第一个收下这个专栏的极客er。请教栏主:能否指导下具体的技术选型?跪急!服务端主要由spring cloud系列开发,终端应用有 win pc、安卓以及ios

    作者回复: 具体的量级是多少呀?消息使用场景是写多读少还是读多写少?量级小的话存储用mysql就可以,量级大的话也可以考虑hbase。nio框架用netty就可以,离线缓存buffer可以用redis或者pika,未读数放redis的hash里就可以。手机端mqtt改造一下或者直接json也可以,浏览器端就websocket + json就行。

    2019-09-10
    3
    11
  • 王棕生
    问题: 在即时消息收发场景中,用于保证消息接收时序的序号生成器为什么可以不是全局递增的?
    答: 这是由业务场景决定的,这个群的消息和另一个群的消息在逻辑上是完全隔离的,只要保证消息的序号在群这样的一个局部范围内是递增的即可; 当然如果可以做到全局递增最好,但是会浪费很多的资源,却没有带来更多的收益。

    作者回复: 👍

    2019-09-06
    5
  • javaworker
    老师,关于服务员消息整流和消息接收端整流
    还有些不明白?
    1.文中说的服务员消息整流是不是只能在网关整流吗?

    2.在网关启动后订阅一个本IP的Topic,文中一直在说对离线消息整流,请问如果是在线消息需要服务端整流吗?

    3.文中说每个消息包生成一个 packageID,意思是可以多条消息用一个包发送吗?packageID就是这个包的ID?您能简述下用户A给用户B发送一条消息,和用户A给用户B发送多条消息时,服务端和接收端都是怎么整流的吗?我理解当发送一条消息时,服务端当接收到一条消息后,会有个超时时间,比如2秒,2秒内没收到新消息的话,就把当前的消息发送出去,这时这个消息包中只有一条消息。当发送多条消息时,2秒内有新消息,就一直自增seqID,直到2秒内没收到消息,然后一起把这个消息包发出去,这时消息包可能有比如7条消息,每一条都按照seqID自增。
    当发送给接收端,接收端在按照每个消息包中seqID顺序显示消息。
    按照我上面的理解,每个消息包之间的顺序怎么保证?是每个消息包packageID也有顺序吗?感觉如果按照我的理解在线发送多条消息会有延时啊,因为服务端一直在整流啊,整完在发送一个大包出去

    作者回复: 1. 是的,在最终推送前收集整流。
    2. 在线消息由于基本上不会有同时多条消息和信令一起发送的场景,所以这种在服务端就大量乱序的情况比较少,一般不需要进行整流,交给端上就可以了。
    3. 基本上理解正确,服务端整流一般用在离线消息推送时,如果上线的这个用户有多条消息,会把这多条消息通过同一个packageID来归类,并且从0自增的给每一条消息打上标记,离线推送的网关机通过这个packageID和seqID来识别这些离线消息的顺序,待消息全部到达后,按照seqID顺序再下推这些消息。但是这里的seqID并不会给到接收端,这里的seqID只是服务端这单次离线消息下推用到的,和接收到没关系。正常的在线消息推送没有这么多并发的情况,没必要整流。

    2019-09-12
    1
    4
  • javaworker
    老师,请教几个问题
    1.您文中说消息通过网关后,IM后台都是并发处理,所以先发的消息有可能后到达。
    我想问如果同时发给一个人的消息,也要IM后台并发处理吗?如果这样路由如何控制?比如其中一条消息丢了,我需要查问题,我如何能知道这条消息是路由到哪台机器哪个线程处理的?

    2.您文中说IM时序基准以IM服务器的时间为准也不行,因为是集群部署。我公司现有IM系统,就是按照IM服务器的时间做的时序基准,比如A和B两个用户都给C发送消息,系统会按照C的userid按照一定路由规则,最终路由到一台消息服务器(比如叫msgserver.1),我们会拿msgserver.1的本地时间作为时序基准,客户端接收到消息后显示就按照msgserver.1的这个时间顺序显示。
    我个人理解公司的IM系统从宏观上看多人的消息是并发处理的,但是针对到某个人其实是单线程处理的,这样好处就是系统设计简单一些,不会遇到您文中说的某台机器GC导致某条消息慢,自然也不会有服务端客户端整流的问题,因为单线程中某条消息慢,后面的消息也别想发出去,呵呵。
    我想问公司这个处理消息的逻辑会有什么问题吗?还想问一下如果针对个人消息也并发处理,一般这个服务端整流的时间设置成多长合适?如果服务端整流收到9条消息,但是还有1条消息由于GC慢了点,整流服务器怎么知道少收了一条消息?从而在这个整流的时间周期中选择不发送?这种情况下IM系统处理消息的服务器又是怎么处理(它怎么知道整流失败,它又怎么知道要重新发送哪些消息)?

    作者回复: 1. 同一个接收人的消息个人感觉没必要用一个线程处理呀,这样还会让服务有状态,如果这个线程挂了后续消息处理的迁移啥的都会比较复杂。对于同一时间有多条消息发送给这个用户的情况,业务上实际也不需要严格区分这几条消息到底哪个先到服务端,只需要保证消息落地存储后时序性固定好就行。
    2. 服务器集群规模太大的话依赖服务器时钟也是个问题,所以可以通过”全局的时间相关的序号生成器“来缓解。
    3. 服务端整流一般用在离线消息处理的场景,因为这个时候会同时有很多条消息会推送给用户,需要保证时序性。比如一次离线消息的下推共用一个packageId,然后自增的针对每一条消息来绑定一个序号(0 1 2 3 ...),这样下推网关根据packageId来有序收集所有消息,如果序号有缺失就知道有消息丢失了(但至少是有序的),这个时候可以根据业务需要来决定直接下推已收集消息还是放弃这一次离线下推。

    2019-09-20
    3
  • 一个爱编程的胖子
    可不可以适当添加一些代码示例。单纯的文字描述比较干

    作者回复: 加了两张流程图来说明一下服务端整流这一块的应用场景和过程,更新后再看下能不能理解。

    2019-09-08
    3
  • 卫江
    问题:为什么不需要全局自增id?原因:因为没有全局排序的需求。而且全局自增id肯定有单点和性能问题。我们目前的需求有两点:单聊和群聊。单聊我们可以通过针对于会话id的自增id解决,群聊通过基于群id的自增id解决,这样就拥有了不错的扩展性,避免单点和性能问题,当然了如果群很大也许也有问题,同时这种方式也可以控制某个id生成服务出问题影响的范围。

    作者回复: 嗯,也考虑如果有“最近联系人列表”页的需求,需要按照多个群或者多个直播间的最新一条消息的产生先后来排序,这种情况可能还需要考虑使用其他属性来进行全局排序了(比如消息产生的时间戳)。

    2019-09-06
    1
    3
  • moooofly
    老师能够提供一个示意图,在途中标明 packageID,seqID 和 xxxID 等都用在什么位置,感觉看过文章和留言问答后,都搞不清楚哪些 ID 用哪里了,多谢~

    作者回复: 添加了两张流程图来具体说明在离线消息推送过程中服务端整流的实现机制。

    2019-09-09
    2
  • Alpha
    生产者为每个消息包生成一个 packageID。
    ——请问这里的消息包是什么概念,是指多个消息的集合吗?如果是的话,什么场景下会将多个消息作为一个包一起发送呢?
    或者指的是包含 一条消息 + 一条指令 的集合吗?

    作者回复: 比如离线消息推送时,用户的多条消息需要推送,这多条消息在服务端进行多线程处理时可能出现乱序的情况,通过在取离线消息时,给每条消息使用同一个packageId并自增一个seq,那么网关机在最终推送时,就可以根据这个packageId来进行一次整流,保证最终下推时消息的有序。

    2019-09-07
    2
  • 小肚腩era
    对老师提的问题,提出一下自己的想法。
    因为实时通信里是会存在多个会话的,如果用于保证消息接收时序的序号生成器是全局递增的,即用于保证所有会话的接收时序,那么会存在可用性和性能瓶颈问题。
    其次,正如老师提到的,从业务层面考虑,对用户来说,只要保证单个会话里消息的时序是正确的即可,因为不同的会话相关性一般不强,不需要保证严格的时序同步,所以针对不同的会话,单独维护一个时钟序号生成器即可。

    作者回复: 是这样的,会话内的排序只需要保证会话内序号递增就可以。当然,如果有“最近联系人列表”这种需求,还需要考虑跨多个会话进行排序的情况,这个可能需要其他属性来协助全局排序了。

    2019-09-06
    2
  • 飞翔
    老师 不仅每个群要有一个全局序号生成器 每个点对点的聊天 比如a和b聊天也需要一个生成器 也就是每对聊天对象都有一个属于他俩的全局生成器

    作者回复: 全局序号生成器不管是点对点还是群聊,不需要针对会话维度来创建,都是可以共用的。

    2019-09-25
    1
  • 袁老师,在课程的最后是不是有实战的项目?我对这个IM几乎是一点不会,能听懂大概的意思,具体的代码我一点没有接触过,听着和看着您和同学们讨论,我很希望我以后也可以能有提问能力,我现在是想提问不知道问什😂 😂 😂 ,希望可以在您这里能入门!😁 😁 😁

    作者回复: 没问题的,11篇开始会有一个简单的聊天系统的示例,可以边看边试试。

    2019-09-25
    1
  • Cap
    对本文中提到的几个技术点有点其他不同的意见:
    1. 关于一致性:文中提到时序一致性,是指的『顺序一致性 Sequential Consistency』?实际上消息场景中数据的一致性可以为更低要求的『因果一致性 Causal Consistency』。
    2. 文中给的方案是以『时序基准』ID为主,『顺序ID』为辅来修正小范围的乱序。而消息场景中,应该是有三个不同的ID用于两个不同场景的消息处理:
        a. 消息ID:唯一性保证,主要用于去重。
        b. 同步顺序ID:本文还没提到消息的同步,对于读扩散或写扩散的消息同步,每次新消息拉取都是从最近一条消息的同步顺序ID往后拉取新消息;对于这个ID要求是在收件箱内自增的,任何依赖服务端时间戳来生成ID的规则都是错的。这个ID可以简单理解为队列中的位点。
        c. 会话顺序ID:这个是本文提到的顺序ID,这个ID不是辅助『时序基准』用的,而是用于保证会话中消息的『因果一致性』,为了让客户端能够对乱序接收的消息进行重排。

    作者回复: 1. 一致性这个要看具体的应用场景呀,比如说只是关注在一个群里的聊天,当然因果一致性就够了,但IM产品里面类似“最近联系人”的需求,是需要把多个群和多个点对点进行时序倒排的,这个时候只是单个会话维度的因果时序是不够的。
    2. 类似于离线消息的同步也不一定需要是顺序自增的呀,而且光是自增是不够的,自增只能保证时序性,不能保证同步时不丢消息。如果要在分布式场景下保证这个ID是“连续自增”实现上是非常困难的。如文中介绍,除了有序ID,还可以通过两个uuid组成的链表方式来保证同步时的顺序和不丢消息。
    另外,全局唯一的“时间相关”的消息ID既可以用来去重,当然也可以用于客户端排序呀。

    2019-09-09
    2
    1
  • 墙角儿的花
    多谢老师,受益良多。希望和老师多交流。
    老师讲的防止业务执行错乱的整流方案,是通过package打包,类似逻辑集装箱,将包内消息有序处理,原理上很清晰,但落地实现比较困难,不太好掌握package的边界。究竟从哪个时机到哪个时机范围的消息归为一个包呢?也希望老师赐教个方法。

    针对需要在业务上整流的问题,提出了三个自己可落地的方案,希望得到老师的指点,不知道究竟哪个可行。

    方案一、客户端单线程单tcp连接保证消息有序到达,服务端采用GO协程对连接一对一服务,保证单客户端发来的消息被服务端有序执行,这个不采用java的线程池技术就是为了防止线程和socket建多对多交替并行处理导致无序问题,但是GO协程这样一对一服务的方式不知道并发能力如何,需要指点。这个我会试验下。

    方案二、

    对于IM,业务执行顺序应该着重关注成员变动(删除好友或取消关注也是一个聊天窗口的成员变动)操作之间的顺序,以及成员变动操作和聊天消息之间的有序处理,前者影响成员变动结果,后者影响聊天消息接收范围,其他的业务错误倒不是''致命''的,产品定位弱一些可以不关注。

    因此,将消息分为聊天消息和信令消息

    信令消息是包括群里加人、离群、删除好友,这种影响聊天消息接收范围的命令,产生信令消息的操作必须在线同步操作,直到服务端明确返回执行结果信息。但也真的可能出现服务端执行了,但客户端恰巧断线,这应该通过网络重连获取最新状态,以保证客户端的信息和服务端同步。

    发信令消息时,消息体附带客户端当前需要服务端给ACK应答的聊天消息id列表,服务端处理信令消息时必须处理完所有前置聊天消息列表里的消息,否者算处理失败。

    这样保证信令消息一定会在合适的顺序得到服务端的处理。

    方案三 严格的链表式消息链

    在发送消息时都要附带其上次向服务端发送的消息id,服务端必须按着链表顺序整流并按序处理,但是,网络上会随时丢消息,一个消息丢失导致后置消息全链无法处理,毕竟不是金融软件没必要这么做,体验不好。


    作者回复: 多多交流~
    这里讲的服务端整流可以参考离线消息的的推送,比如离线消息推送时,用户的多条消息需要推送,这多条消息在服务端进行多线程处理时可能出现乱序的情况,通过在取离线消息时,给每条消息使用同一个packageId并自增一个seq,那么网关机在最终推送时,就可以根据这个
    packageId来进行一次整流,保证最终下推时消息的有序。

    方案1的思路上没问题哈,考虑下很多IM场景,由于服务端一般是多层的架构,比如业务层,网关层,会涉及到多个进程的处理,中间的流转可能需要经过消息队列,这些过程也可能会导致乱序出现。

    方案2的话主要是通用性上可能不是太好,需要区分消息类型啥的,处理逻辑也稍微复杂一些。

    实际上,大部分IM场景有了接收端的整流是不太需要服务端整流的,除了服务端可能存在短时间内推送多条连续消息的情况才可能需要服务端进行整流。

    2019-09-08
    1
  • clip
    如果我们应用场景就是 IM,消息接收端整流是不是可能导致收到消息之后也能又往上面插入了新消息?感觉这种体验可能不是很好。
    是不是要采取另外的措施?
    能想到的有:必须保证消息顺序下发,如果中间有比较耗时的消息可能先用一个占位符代替。自己发送的消息要等服务端确认后才真正进入消息流排序。

    作者回复: 保证服务端推送是必须有序也是可以的,看具体的需求场景是否真的需要,另外实现成本也需要考虑。接收端整流也可以是如果乱序先不显示而是等一段时间,来尽量避免显示上的跳变。

    2019-09-08
    1
  • clip
    服务端包内整流那块儿有点不理解。
    这个 pkg 指的是类似举例的“最后一条消息和取关操作”那种打包在一起的事件吗?还是群纬度的那种一个群的算一个 pkg?

    作者回复: 新补充了两个图,等更新了大家可以看一下哈~
    比如离线消息推送时,用户的多条消息需要推送,这多条消息在服务端进行多线程处理时可能出现乱序的情况,通过在取离线消息时,给每条消息使用同一个packageId并自增一个seq,那么网关机在最终推送时,就可以根据这个packageId来进行一次整流,保证最终下推时消息的有序。

    2019-09-08
    1
    1
  • 墙角儿的花
    对于"分手"、"取关"的严格业务顺序场景,通过单线程单tcp连接能保证消息一定按着发送顺序到达服务器吗?socket.send有序发送两条消息A 和 B,由于链路故障是否可能导致服务器先接收到B后接收到A,我一直保持着业务层消息即使同一tcp连接上有序发送也有可能出现乱序到达,所以需要接收端在业务层重排的认知,但好像也确实没有证明。

    作者回复: 单连接单线程的话TCP层的“有序接收”能够保证消息的有序到达。但这种模型的性能和可用性基本不能用在真实业务场景里。

    2019-09-06
    1
  • 邹文通
    老师您好,请教一个有关消息 ID 的问题。如果采用秒间有序的分布式消息 ID 生成器,一个会话中同一秒的消息可能是乱序的。那么如果同一秒内 A 和 B 对话产生了三条消息,A 向 B 发送消息 1,B 回复 A 消息 2,A 看完 2 后回复消息 3, 可能会出现消息 3 的 id 在消息 2 前面,请问应当如何避免这种情况呢?

    作者回复: 一般来说,多个用户在发送消息时,由于各自本地时钟的差异以及消息从发送的客户端到达服务端的延时并不一致的问题,所以对于多个发送方并没有一个绝对的时间基准存在,这种情况实际上很难避免的。

    2019-11-26
  • polk
    离线消息通过包内整流,对于在线的消息,整流会增加耗时,就不需要做整流了吧?

    作者回复: 是的,包内整流主要用于离线消息下推,在线消息一般不需要。

    2019-11-19
  • 唯我天棋
    在即时消息收发场景中,用于保证消息接收时序的序号生成器为什么可以不是全局递增的?

    1.全局递增,性能压力比较大。
    2.全局的这个出问题,会导致整体不可用。高可用性比较差。
    2019-11-05
收起评论
40
返回
顶部