RocketMQ学习笔记

一、简介

        RocketMQ是一种开源的分布式消息中间件系统,最初由阿里巴巴开发并开源。它是一种快速、可靠、可伸缩的消息队列系统,旨在解决分布式系统中的异步通信和数据传输问题,构建可靠的分布式系统和实时数据处理系统。

二、主要特点

        1. 高吞吐量:RocketMQ具有出色的性能表现,能够处理大规模消息的传输和处理。它通过支持并行化和异步传输等机制来实现高吞吐量。

        2. 可靠性:RocketMQ在消息传输过程中提供了可靠性保证。它采用了主从复制的方式来确保消息的高可用性,并提供了严格的消息顺序传输保证。

        3. 分布式支持:RocketMQ是为分布式系统设计的,它可以在多个节点上进行部署,并支持横向扩展。它提供了自动的负载均衡和故障转移机制,以实现高可用性和可伸缩性。

        4. 灵活的消息模型:RocketMQ支持多种消息模型,包括发布/订阅模型和点对点模型。这使得开发人员可以根据实际需求选择适合的消息传输方式。

        5. 实时性:RocketMQ具有低延迟和高实时性的特点,适用于需要快速响应的场景,如实时数据分析、流式处理等。

        6. 可管理性:RocketMQ提供了可视化的管理工具和监控指标,方便管理员对消息队列进行配置、监控和管理。

        7. 丰富的特性:RocketMQ还提供了许多其他功能,如事务消息、延迟消息、批量消息发送、消息过滤等,以满足不同场景下的需求。

三、应用场景

        1.削峰填谷
        大型活动会带来较高的流量脉冲,如果没有做相应的保护就会导致系统超负荷甚至是崩溃,如果要是限制的太过就会导致请求大量失败影响用户体验。

        2.异步解耦
        业务系统进行松耦合设计,就算一部分子系统出现不可用甚至是宕机,都不会影响到核心交易系统的正常运转。

        3.顺序消息
        与FIFO原理类似,MQ提供的顺序消息即消息的先进先出,可以应用于交易系统中的订单创建、支付等。

        4.分布式事务消息
        比如支付红包需要保证数据的最终一致性,需要引入MQ的分布式事务,既实现了解耦,又可以保证最终数据的一致性。

        5.NameServer的设计极其简单
  Topic路由信息无须在集群之间保持强一致,而是追求最终一致性,并且能容忍分钟级的不一致。正是基于这种特性,RocketMQ的NameServer集群之间互不通信,这样极大地降低了NameServer实现的复杂度,对网络的要求也降低了不少,性能相比较ZooKeeper还有了极大的提升。
        6.高效的I/O存储机制
  RocketMQ追求消息发送的高吞吐量,RocketMQ的消息存储文件被设计成文件组的概念,组内单个文件大小固定,方便引入内存映射机制,所有主题的消息存储按顺序编写,极大地提升了消息的写性能。同时为了兼顾消息消费与消息查找,引入了消息消费队列文件与索引文件。
        7.容忍存在设计缺陷
  消息中间件的实现者经常会遇到一个难题:如何保证消息一定能被消息消费者消费,并且只消费一次?RocketMQ的设计者给出的解决办法是不解决这个难题,而是退而求其次,只保证消息被消费者消费,在设计上允许消息被重复消费。这样极大地简化了消息中间件的内核,使得实现消息发送高可用变得非常简单和高效,消息重复问题由消费者在消息消费时实现幂等。

四、RocketMQ中重要的组件

        1.Producer(生产者):Producer负责将消息发送到RocketMQ的Broker,它将消息封装成消息对象并指定消息的主题(Topic)。生产者可以根据需要选择同步发送或异步发送消息。
        2.Consumer(消费者):Consumer从RocketMQ的Broker订阅消息,并对消息进行消费处理。消费者可以按照订阅的主题(Topic)和标签(Tag)来过滤需要的消息。
        3.Broker(消息代理):Broker是RocketMQ的核心组件,负责存储和转发消息。Broker接收生产者发送的消息,并将其存储在内部的存储引擎中,等待消费者订阅并消费。Broker还负责处理消费者的消费进度和消息的复制和同步。
        4.Name Server(命名服务器):Name Server是RocketMQ集群的管理节点,它负责管理和维护整个RocketMQ集群的元数据信息。Producer和Consumer通过Name Server来发现Broker的位置和状态信息。
        5.Topic(主题):主题是消息的逻辑分类,Producer将消息发送到特定的主题,而Consumer可以订阅并消费特定的主题。一个主题可以有多个消息队列,每个消息队列在一个Broker上。
       6. Message Queue(消息队列):消息队列是RocketMQ中消息的存储和传输单位。每个主题可以被分为多个消息队列,每个消息队列在一个Broker上,可以并行地处理消息。
        7.Consumer Group(消费者组):消费者组是一组具有相同消费逻辑的Consumer实例的集合。在一个消费者组中,每个消息队列只能由一个Consumer实例进行消费,但一个Consumer组可以有多个Consumer实例,从而实现负载均衡和高可用性。
        8.Offset:在Topic的消费过程中,由于消息需要被不同的组进行多次消费,所以消费完的消息并不会立即被删除,这就需要RocketMQ为每个消费组在每个队列上维护一个消费位置(Consumer Offset),这个位置之前的消息都被消费过,之后的消息都没有被消费过,每成功消费一条消息,消费位置就加一。也可以这么说,Queue 是一个长度无限的数组,Offset 就是下标。

五、消息队列的两种消息模型

消息队列有两种模型:队列模型发布/订阅模型

1.队列模型

        这是最初的一种消息队列模型,对应着消息队列“发-存-收”的模型。生产者往某个队列里面发送消息,一个队列可以存储多个生产者的消息,一个队列也可以有多个消费者,但是消费者之间是竞争关系,也就是说每条消息只能被一个消费者消费。

2.发布/订阅模型

        如果需要将一份消息数据分发给多个消费者,并且每个消费者都要求收到全量的消息。很显然,队列模型无法满足这个需求。解决的方式就是发布/订阅模型。

        在发布 - 订阅模型中,消息的发送方称为发布者(Publisher),消息的接收方称为订阅者(Subscriber),服务端存放消息的容器称为主题(Topic)。发布者将消息发送到主题中,订阅者在接收消息之前需要先“订阅主题”。“订阅”在这里既是一个动作,同时还可以认为是主题在消费时的一个逻辑副本,每份订阅中,订阅者都可以接收到主题的所有消息。

它和 “队列模式” 的异同:生产者就是发布者,队列就是主题,消费者就是订阅者,无本质区别。唯一的不同点在于:一份消息数据是否可以被多次消费。

3.那RocketMQ的消息模型呢?

        RocketMQ使用的消息模型是标准的发布-订阅模型,在RocketMQ的术语表中,生产者、消费者和主题,与发布-订阅模型中的概念是完全一样的。

RocketMQ本身的消息是由下面几部分组成:

六、RocketMQ消息的组成

1.Message(消息)

        就是要传输的信息。一条消息必须有一个主题(Topic),主题可以看做是你的信件要邮寄的地址。 一条消息也可以拥有一个可选的标签(Tag)和额处的键值对,它们可以用于设置一个业务 Key 并在 Broker 上查找此消息以便在开发期间查找问题。

2.Topic(主题)

        可以看做消息的归类,它是消息的第一级类型。比如一个电商系统可以分为:交易消息、物流消息等,一条消息必须有一个 Topic 。

        Topic 与生产者和消费者的关系非常松散,一个 Topic 可以有0个、1个、多个生产者向其发送消息,一个生产者也可以同时向不同的 Topic 发送消息。 一个 Topic 也可以被 0个、1个、多个消费者订阅。

3.Tag(标签)

        可以看作子主题,它是消息的第二级类型,用于为用户提供额外的灵活性。使用标签,同一业务模块不同目的的消息就可以用相同 Topic 而不同的 Tag 来标识。比如交易消息又可以分为:交易创建消息、交易完成消息等,一条消息可以没有 Tag 。

标签有助于保持你的代码干净和连贯,并且还可以为 RocketMQ 提供的查询系统提供帮助。

4.Group

        RocketMQ中,订阅者的概念是通过消费组(Consumer Group)来体现的。每个消费组都消费主题中一份完整的消息,不同消费组之间消费进度彼此不受影响,也就是说,一条消息被Consumer Group1消费过,也会再给Consumer Group2消费。

        消费组中包含多个消费者,同一个组内的消费者是竞争消费的关系,每个消费者负责消费组内的一部分消息。默认情况,如果一条消息被消费者Consumer1消费了,那同组的其他消费者就不会再收到这条消息。

5.Message Queue(消息队列)

        一个 Topic 下可以设置多个消息队列,Topic 包括多个 Message Queue ,如果一个 Consumer 需要获取 Topic下所有的消息,就要遍历所有的 Message Queue。

RocketMQ还有一些其它的Queue——例如ConsumerQueue。

6.Offset

        在Topic的消费过程中,由于消息需要被不同的组进行多次消费,所以消费完的消息并不会立即被删除,这就需要RocketMQ为每个消费组在每个队列上维护一个消费位置(Consumer Offset),这个位置之前的消息都被消费过,之后的消息都没有被消费过,每成功消费一条消息,消费位置就加一。

也可以这么说,queue是一个长度无限的数组,Offset 就是下标。

七、RocketMQ消费模式 

消息消费模式有两种:Clustering(集群消费)和Broadcasting(广播消费)。

两种消费模式

默认情况下就是集群消费,这种模式下一个消费者组共同消费一个主题的多个队列,一个队列只会被一个消费者消费,如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。

而广播消费消息会发给消费者组中的每一个消费者进行消费。

八、RocketMQ架构

         RocketMQ 一共有四个部分组成:NameServer,Broker,Producer 生产者,Consumer 消费者,它们对应了:发现、发、存、收,为了保证高可用,一般每一部分都是集群部署的。

类比一下我们生活的邮政系统——

        邮政系统要正常运行,离不开下面这四个角色, 一是发信者,二 是收信者, 三是负责暂存传输的邮局, 四是负责协调各个地方邮局的管理机构。对应到 RocketMQ 中,这四个角色就是 Producer、 Consumer、 Broker 、NameServer。

1.NameServer

        NameServer 是一个无状态的服务器,角色类似于 Kafka使用的 Zookeeper,但比 Zookeeper 更轻量。

1.1特点:

        1.每个 NameServer 结点之间是相互独立,彼此没有任何信息交互。

        2.Nameserver 被设计成几乎是无状态的,通过部署多个结点来标识自己是一个伪集群,Producer 在发送消息前从 NameServer 中获取 Topic 的路由信息也就是发往哪个 Broker,Consumer 也会定时从 NameServer 获取 Topic 的路由信息,Broker 在启动时会向 NameServer 注册,并定时进行心跳连接,且定时同步维护的 Topic 到 NameServer。

1.2功能主要有两个:

        1、和Broker 结点保持长连接。

        2、维护 Topic 的路由信息。

2.Broker

消息存储和中转角色,负责存储和转发消息。

        1.Broker 内部维护着一个个 Consumer Queue,用来存储消息的索引,真正存储消息的地方是 CommitLog(日志文件)。

        2.单个 Broker 与所有的 Nameserver 保持着长连接和心跳,并会定时将 Topic 信息同步到 NameServer,和 NameServer 的通信底层是通过 Netty 实现的。

3.Producer

消息生产者,业务端负责发送消息,由用户自行实现和分布式部署。

        1.Producer由用户进行分布式部署,消息由Producer通过多种负载均衡模式发送到Broker集群,发送低延时,支持快速失败。

        2.RocketMQ 提供了三种方式发送消息:同步、异步和单向

        3.同步发送:同步发送指消息发送方发出数据后会在收到接收方发回响应之后才发下一个数据包。一般用于重要通知消息,例如重要通知邮件、营销短信。

        4.异步发送:异步发送指发送方发出数据后,不等接收方发回响应,接着发送下个数据包,一般用于可能链路耗时较长而对响应时间敏感的业务场景,例如用户视频上传后通知启动转码服务。        

        5.单向发送:单向发送是指只负责发送消息而不等待服务器回应且没有回调函数触发,适用于某些耗时非常短但对可靠性要求并不高的场景,例如日志收集。

4.Consumer

消息消费者,负责消费消息,一般是后台系统负责异步消费。

        Consumer也由用户部署,支持PUSH和PULL两种消费模式,支持集群消费广播消费,提供实时的消息订阅机制

        1.Pull:拉取型消费者(Pull Consumer)主动从消息服务器拉取信息,只要批量拉取到消息,用户应用就会启动消费过程,所以 Pull 称为主动消费型。

        2.Push:推送型消费者(Push Consumer)封装了消息的拉取、消费进度和其他的内部维护工作,将消息到达时执行的回调接口留给用户应用程序来实现。所以 Push  称为被动消费类型,但其实从实现上看还是从消息服务器中拉取消息,不同于 Pull 的是 Push  首先要注册消费监听器,当监听器处触发后才开始消费消息。

九、消息存储

从存储模型来看,目前MQ中间件分为需要持久化和不需要持久化两种,大多数MQ都支持持久化存储,比如ActiveMQ、RabbitMQ、Kafka、RocketMQ。
  RocketMQ存储的文件主要包括:CommitLog文件、ConsumeQueue文件、Index文件。RocketMQ将所有主题的消息存储在同一个文件中,确保消息发送时按顺序写文件,尽最大的能力确保消息发送的高性能与高吞吐量
  因为消息中间件一般是基于消息主题的订阅机制,所以给按照消息主题检索消息带来了极大的不便。为了提高消息消费的效率,RocketMQ引入了ConsumeQueue消息消费队列文件,每个消息主题包含多个消息消费队列,每一个消息队列有一个消息文件。Index索引文件的设计理念是为了加速消息的检索性能,根据消息的属性从CommitLog文件中快速检索消息。

  CommitLog:消息存储,所有消息主题的消息都存储在CommitLog文件中。
  ConsumeQueue:消息消费队列,消息到达CommitLog文件后,将异步转发到ConsumeQuene文件中,供消息消费者消费。
  Index:消息索引,主要存储消息key与offset的对应关系。

1 存储文件的组织方式

RocketMQ在消息写入过程中追求极致的磁盘顺序写,所有主题的消息全部写入一个文件,即CommitLog文件。所有消息按抵达顺序依次追加到CommitLog文件中,消息一旦写入,不支持修改。

CommitLog文件的命名方式:使用存储在该文件的第一条消息在整个CommitLog文件组中的偏移量来命名,例如第一个CommitLog文件为0000000000000000000,第二个CommitLog文件为00000000001073741824,依次类推。
  这样做的好处是给出任意一个消息的物理偏移量,可以通过二分法进行查找,快速定位这个文件的位置,然后用消息物理偏移量减去所在文件的名称,得到的差值就是在该文件中的绝对地址。
  CommitlLog文件的设计理念是追求极致的消息写,但我们知道消息消费模型是基于主题订阅机制的,即一个消费组是消费特定主题的消息。根据主题从CommitlLog文件中检索消息,这绝不是一个好主意,这样只能从文件的第一条消息逐条检索,其性能可想而知,为了解决基于topic的消息检索问题,RocketMQ引入了ConsumeQueue文件:

ConsumeQueue文件是消息消费队列文件,是CommitLog文件基于topic的索引文件,主要用于消费者根据topic消费消息。
  RocketMQ与Kafka相比具有一个强大的优势,就是支持按消息属性检索消息,引入ConsumeQueue文件解决了基于topic查找消息的问题,但如果想基于消息的某一个属性进行查找,ConsumeQueue文件就无能为力了。故RocketMQ又引入了Index索引文件,实现基于文件的哈希索引。

CommitLog
  消息主体以及元数据的存储主体,存储消息生产端写入的消息主体内容,消息内容不是定长的。单个文件大小默认1GB,文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824。第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件。
ConsumerQueue
  消息消费队列引入的目的是提高消息消费的性能,由于RocketMQ是基于topic的订阅模式,消息消费是针对topic进行的,如果要根据topic遍历CommitLog文件中的消息是非常低效的。消息消费端可根据ConsumeQueue来查找待消费的消息。
  ConsumeQueue作为消费消息的索引,保存了指定topic下的队列消息在CommitLog中的起始物理偏移量,消息大小和消息tag的哈希码。ConsumeQueue文件可以看作基于topic的CommitLog索引文件。

2 内存映射

        虽然基于磁盘的顺序写消息可以极大提高I/O的写效率,但如果基于文件的存储采用常规的Java文件操作API,例如FileOutputStream等,将性能提升会很有限,故RocketMQ引入了内存映射,将磁盘文件映射到内存中,以操作内存的方式操作磁盘,将性能又提升了一个档次。
  在Java中可通过FileChannel的map方法创建内存映射文件。在Linux服务器中由该方法创建的文件使用的就是操作系统的页缓存(pagecache)。Linux操作系统中内存使用策略时会尽可能地利用机器的物理内存,并常驻内存中,即页缓存。在操作系统的内存不够的情况下,采用缓存置换算法,例如LRU将不常用的页缓存回收,即操作系统会自动管理这部分内存。
  如果RocketMQ Broker进程异常退出,存储在页缓存中的数据并不会丢失,操作系统会定时将页缓存中的数据持久化到磁盘,实现数据安全可靠。不过如果是机器断电等异常情况,存储在页缓存中的数据也有可能丢失。

3 .刷盘策略

        有了顺序写和内存映射的加持,RocketMQ的写入性能得到了极大的保证,但凡事都有利弊,引入了内存映射和页缓存机制,消息会先写入页缓存,此时消息并没有真正持久化到磁盘。
  RocketMQ提供了两种策略:同步刷盘、异步刷盘
  RocketMQ的存储与读写是基于JDK NIO的内存映射机制的,消息存储时首先将消息追加到内存中,再根据配置的刷盘策略在不同时间刷盘。如果是同步刷盘,消息追加到内存后,将同步调用MappedByteBuffer的force()方法;如果是异步刷盘,在消息追加到内存后会立刻返回给消息发送端。
  RocketMQ使用一个单独的线程按照某一个设定的频率执行刷盘操作。通过在broker配置文件中配置flushDiskType来设定刷盘方式,可选值为ASYNC_FLUSH(异步刷盘)、SYNC_FLUSH(同步刷盘),默认为异步刷盘。

同步刷盘
  同步刷盘指的是在消息追加到内存映射文件的内存中后,立即将数据从内存写入磁盘文件。
异步刷盘
  同步刷盘的优点是能保证消息不丢失,即向客户端返回成功就代表这条消息已被持久化到磁盘,但这是以牺牲写入性能为代价的,不过RocketMQ的消息是先写入pagecache,所以消息丢失的可能性较小,如果能容忍一定概率的消息丢失或者在丢失后能够低成本的快速重推,可以考虑使用异步刷盘策略。
  异步刷盘指的是broker将消息存储到pagecache后就立即返回成功,然后开启一个异步线程定时执行FileChannel的force方法,将内存中的数据定时写入磁盘,默认间隔时间为500ms。

4.过期文件删除机制

        因为RocketMQ操作CommitLog、ConsumeQueue文件是基于内存映射机制并在启动的时候会加载commitlog、consumequeue目录下的所有文件,所以为了避免内存与磁盘的浪费,不可能将消息永久存储在消息服务器上,这就需要引入一种机制来删除已过期的文件。RocketMQ顺序写CommitLog文件、ConsumeQueue文件,所有写操作全部落在最后一个CommitLog或ConsumeQueue文件上,之前的文件在下一个文件创建后
将不会再被更新。RocketMQ清除过期文件的方法:如果非当前写文件在一定时间间隔内没有再次更新,则认为是过期文件,可以被删除,RocketMQ不会关注这个文件上的消息是否全部被消费。默认每个文件的过期时间为72h,通过在broker配置文件中设置fileReservedTime来改变过期时间,单位为小时。

5.消息消费

        消息消费以组的模式开展,一个消费组可以包含多个消费者,每个消费组可以订阅多个主题,消费组之间有集群模式和广播模式两种消费模式。集群模式是当前主题下的同一条消息只允许被其中一个消费者消费。广播模式是当前主题下的同一条消息将被集群内的所有消费者消费一次。
  RocketMQ支持局部顺序消息消费,也就是保证同一个消息队列上的消息按顺序消费。不支持消息全局顺序消费,如果要实现某一主题的全局顺序消息消费,可以将该主题的队列数设置为1,牺牲高可用性。

定时消息机制
  定时消息是指消息发送到Broker后,不会立即被消费者消费,而是要等到特定的时间后才能被消费,RocketMQ并不支持任意的时间精度,因为如果要支持任意时间精度的定时调度,则不可避免地需要在Broker层做消息排序,再加上持久化方面的考量,将不可避免地带来具大的性能消耗,所以RocketMQ只支持特定级别的延迟消息。
  消息延迟级别在Broker端通过messageDelayLevel进行配置,默认为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h2h”,delayLevel=1表示延迟1s,delayLevel=2表示延迟5s,依次类推。

十、面试问题

1.如何保证消息的可用性/可靠性/不丢失呢?

消息可能在哪些阶段丢失呢?可能会在这三个阶段发生丢失:生产阶段、存储阶段、消费阶段。

在生产阶段,主要通过请求确认机制,来保证消息的可靠传递

  • 1、同步发送的时候,要注意处理响应结果和异常。如果返回响应OK,表示消息成功发送到了Broker,如果响应失败,或者发生其它异常,都应该重试。

  • 2、异步发送的时候,应该在回调方法里检查,如果发送失败或者异常,都应该进行重试。

  • 3、如果发生超时的情况,也可以通过查询日志的API,来检查是否在Broker存储成功。

存储阶段,可以通过配置可靠性优先的 Broker 参数来避免因为宕机丢消息,简单说就是可靠性优先的场景都应该使用同步。

  • 1、消息只要持久化到CommitLog(日志文件)中,即使Broker宕机,未消费的消息也能重新恢复再消费。

  • 2、Broker的刷盘机制:同步刷盘和异步刷盘,不管哪种刷盘都可以保证消息一定存储在pagecache中(内存中),但是同步刷盘更可靠,它是Producer发送消息后等数据持久化到磁盘之后再返回响应给Producer。

  • 3、Broker通过主从模式来保证高可用,Broker支持Master和Slave同步复制、Master和Slave异步复制模式,生产者的消息都是发送给Master,但是消费既可以从Master消费,也可以从Slave消费。同步复制模式可以保证即使Master宕机,消息肯定在Slave中有备份,保证了消息不会丢失。

从Consumer角度分析,如何保证消息被成功消费?

        Consumer保证消息成功消费的关键在于确认的时机,不要在收到消息后就立即发送消费确认,而是应该在执行完所有消费业务逻辑之后,再发送消费确认。因为消息队列维护了消费的位置,逻辑执行失败了,没有确认,再去队列拉取消息,就还是之前的一条。

 2.如何处理消息重复的问题呢?

对分布式消息队列来说,同时做到确保一定投递和不重复投递是很难的,就是所谓的“有且仅有一次” 。RocketMQ择了确保一定投递,保证消息不丢失,但有可能造成消息重复。

处理消息重复问题,主要有业务端自己保证,主要的方式有两种:业务幂等消息去重

业务幂等:第一种是保证消费逻辑的幂等性,也就是多次调用和一次调用的效果是一样的。这样一来,不管消息消费多少次,对业务都没有影响。

消息去重:第二种是业务端,对重复的消息就不再消费了。这种方法,需要保证每条消息都有一个惟一的编号,通常是业务相关的,比如订单号,消费的记录需要落库,而且需要保证和消息确认这一步的原子性。

具体做法是可以建立一个消费记录表,拿到这个消息做数据库的insert操作。给这个消息做一个唯一主键(primary key)或者唯一约束,那么就算出现重复消费的情况,就会导致主键冲突,那么就不再处理这条消息。

3.怎么处理消息积压?

 发生了消息积压,这时候就得想办法赶紧把积压的消息消费完,就得考虑提高消费能力,一般有两种办法:

消费者扩容:如果当前Topic的Message Queue的数量大于消费者数量,就可以对消费者进行扩容,增加消费者,来提高消费能力,尽快把积压的消息消费玩。

消息迁移Queue扩容:如果当前Topic的Message Queue的数量小于或者等于消费者数量,这种情况,再扩容消费者就没什么用,就得考虑扩容Message Queue。可以新建一个临时的Topic,临时的Topic多设置一些Message Queue,然后先用一些消费者把消费的数据丢到临时的Topic,因为不用业务处理,只是转发一下消息,还是很快的。接下来用扩容的消费者去消费新的Topic里的数据,消费完了之后,恢复原状。

4.顺序消息如何实现

顺序消息分为全局顺序消息和部分顺序消息:

全局顺序消息:某个Topic下的所有消息都要保证顺序;

RocketMQ 默认情况下不保证顺序,比如创建一个 Topic ,默认八个写队列,八个读队列,这时候一条消息可能被写入任意一个队列里;在数据的读取过程中,可能有多个 Consumer ,每个 Consumer 也可能启动多个线程并行处理,所以消息被哪个 Consumer 消费,被消费的顺序和写人的顺序是否一致是不确定的。
  要保证全局顺序消息, 需要先把 Topic 的读写队列数设置为 一,然后Producer Consumer 的并发设置,也要是一。简单来说,为了保证整个 Topic全局消息有序,只能消除所有的并发处理,各部分都设置成单线程处理 ,这时候就完全牺牲RocketMQ的高并发、高吞吐的特性了。

部分顺序消息:只要保证每一组消息被顺序消费即可,比如订单消息,只要保证同一个订单 ID 个消息能按顺序消费即可。

部分顺序消息相对比较好实现,生产端需要做到把同 ID 的消息发送到同一个 Message Queue ;在消费过程中,要做到从同一个Message Queue读取的消息顺序处理——消费端不能并发处理顺序消息,这样才能达到部分有序。
  由producer发送到broker的消息队列是满足FIFO的,所以发送是顺序的,单个queue里的消息是顺序的。多个Queue同时消费是无法绝对保证消息的有序性的。所以,同一个topic,同一个queue,发消息的时候一个线程发送消息,消费的时候一个线程去消费一个queue里的消息。

5. 如何实现消息过滤

有两种方案:
  一种是在 Broker 端按照 Consumer 的去重逻辑进行过滤,这样做的好处是避免了无用的消息传输到 Consumer 端,缺点是加重了 Broker 的负担,实现起来相对复杂。
  另一种是在 Consumer 端过滤,比如按照消息设置的 tag 去重,这样的好处是实现起来简单,缺点是有大量无用的消息到达了 Consumer 端只能丢弃不处理。

6.延时消息

RocketMQ是支持延时消息的,只需要在生产消息的时候设置消息的延时级别:

messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

RocketMQ怎么实现延时消息:
  临时存储+定时任务。
  Broker收到延时消息了,会先发送到主题(SCHEDULE_TOPIC_XXXX)的相应时间段的Message Queue中,然后通过一个定时任务轮询这些队列,到期后,把消息投递到目标Topic的队列中,然后消费者就可以正常消费这些消息。

7.怎么实现分布式消息事务  

        RocketMQ 的事务消息是一种保证消息可靠性的机制。在RocketMQ中,事务消息的实现原理主要是通过两个发送阶段和一个确认阶段来实现的。

        发送消息的预处理阶段:在发送事务消息之前,RocketMQ 会将消息的状态设置为“Preparing”,并将消息存储到消息存储库中。
执行本地事务:当预处理阶段完成后,消息发送者需要执行本地事务,并返回执行结果(commit 或 rollback)。
        消息的二次确认阶段:根据本地事务的执行结果,如果是 commit,则 RocketMQ 将消息的状态设置为“Committing”;否则将消息的状态设置为“Rollback”。
        完成事务:最后在消息的消费者消费该消息时,RocketMQ 会根据消息的状态来决定是否提交该消息。如果消息的状态是“Committing”,则直接提交该消息;否则忽略该消息。
        需要注意的是,如果在消息发送的过程中出现异常或者网络故障等问题,RocketMQ 会触发消息回查机制。在回查过程中,RocketMQ 会调用消息发送方提供的回查接口来确认事务的提交状态,从而解决消息投递的不确定性。

8.死信队列

死信队列用于处理无法被正常消费的消息,即死信消息。
  当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中,该特殊队列称为死信队列。
  死信消息的特点:

  不会再被消费者正常消费。
  有效期与正常消息相同,均为 3 天。3 天后会被自动删除。因此,需要在死信消息产生后的 3 天内及时处理。

  死信队列的特点:

  一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。
  如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。
  一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。

  RocketMQ 控制台提供对死信消息的查询、导出和重发的功能。

9.如何保证RocketMQ的高可用

NameServer因为是无状态,且不相互通信的,所以只要集群部署就可以保证高可用。
  RocketMQ的高可用主要是在体现在Broker的读和写的高可用,Broker的高可用是通过集群和主从实现的

Broker可以配置两种角色:Master和Slave,Master角色的Broker支持读和写,Slave角色的Broker只支持读,Master会向Slave同步消息。也就是说Producer只能向Master角色的Broker写入消息,Cosumer可以从Master和Slave角色的Broker读取消息。
  Consumer 的配置文件中,并不需要设置是从 Master 读还是从 Slave读,当 Master 不可用或者繁忙的时候, Consumer 的读请求会被自动切换到从 Slave。有了自动切换 Consumer 这种机制,当一个 Master 角色的机器出现故障后,Consumer 仍然可以从 Slave 读取消息,不影响 Consumer 读取消息,这就实现了读的高可用。
  如何达到发送端写的高可用性呢?在创建 Topic 的时候,把 Topic 的多个Message Queue 创建在多个 Broker 组上(相同 Broker 名称,不同 brokerId机器组成 Broker 组),这样当 Broker 组的 Master 不可用后,其他组Master 仍然可用, Producer 仍然可以发送消息 RocketMQ 目前还不支持把Slave自动转成 Master ,如果机器资源不足,需要把 Slave 转成 Master ,则要手动停止 Slave 色的 Broker ,更改配置文件,用新的配置文件启动 Broker。

10.RocketMQ的整体工作流程

简单来说,RocketMQ是一个分布式消息队列,也就是消息队列+分布式系统。
  作为消息队列,它是发-存-收的一个模型,对应的就是Producer、Broker、Cosumer;作为分布式系统,它要有服务端、客户端、注册中心,对应的就是Broker、Producer/Consumer、NameServer。

1、Broker在启动的时候去向所有的NameServer注册,并保持长连接,每30s发送一次心跳。
2、Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器来发送消息。
3、Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费。

11.Broker是怎么保存数据的

 RocketMQ主要的存储文件包括CommitLog文件、ConsumeQueue文件、Indexfile文件。

CommitLog
  消息主体以及元数据的存储主体,存储Producer端写入的消息主体内容,消息内容不是定长的。单个文件大小默认1G, 文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件。
  CommitLog文件保存于${Rocket_Home}/store/commitlog目录中。
ConsumeQueue
  消息消费队列,引入的目的主要是提高消息消费的性能,由于RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进行的,如果要遍历commitlog文件中根据topic检索消息是非常低效的。
  Consumer即可根据ConsumeQueue来查找待消费的消息。其中,ConsumeQueue(逻辑消费队列)作为消费消息的索引,保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值。
  ConsumeQueue文件可以看成是基于Topic的CommitLog索引文件,故ConsumeQueue文件夹的组织方式如下:topic/queue/file三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样ConsumeQueue文件采取定长设计,每一个条目共20个字节,分别为8字节的CommitLog物理偏移量、4字节的消息长度、8字节tag hashcode,单个文件由30W个条目组成,可以像数组一样随机访问每一个条目,每个ConsumeQueue文件大小约5.72M。
IndexFile
  IndexFile(索引文件)提供了一种可以通过key或时间区间来查询消息的方法。Index文件的存储位置是:{fileName},文件名fileName是以创建时的时间戳命名的,固定的单个IndexFile文件大小约为400M,一个IndexFile可以保存 2000W个索引,IndexFile的底层存储设计为在文件系统中实现HashMap结构,故RocketMQ的索引文件其底层实现为hash索引。

总结
  RocketMQ采用的是混合型的存储结构,即为Broker单个实例下所有的队列共用一个日志数据文件(即为CommitLog)来存储。
  RocketMQ的混合型存储结构(多个Topic的消息实体内容都存储于一个CommitLog中)针对Producer和Consumer分别采用了数据和索引部分相分离的存储结构,Producer发送消息至Broker端,然后Broker端使用同步或者异步的方式对消息刷盘持久化,保存至CommitLog中。
  只要消息被刷盘持久化至磁盘文件CommitLog中,那么Producer发送的消息就不会丢失。正因为如此,Consumer也就肯定有机会去消费这条消息。当无法拉取到消息后,可以等下一次消息拉取,同时服务端也支持长轮询模式,如果一个消息拉取请求未拉取到消息,Broker允许等待30s的时间,只要这段时间内有新消息到达,将直接返回给消费端。
  RocketMQ的具体做法是,使用Broker端的后台服务线程—ReputMessageService不停地分发请求并异步构建ConsumeQueue(逻辑消费队列)和IndexFile(索引文件)数据。

12.RocketMQ怎么对文件进行读写的

RocketMQ对文件的读写巧妙地利用了操作系统的一些高效文件读写方式——PageCache、顺序读写、零拷贝。

顺序读写
  磁盘的顺序读写性能要远好于随机读写。因为每次从磁盘读数据时需要先寻址,找到数据在磁盘上的物理位置。对于机械硬盘来说,就是移动磁头,会消耗时间。顺序读写相比于随机读写省去了大部分的寻址时间,它只需要寻址一次就可以连续读写下去,所以性能比随机读写好很多。
  RocketMQ 利用了这个特性。它所有的消息数据都存放在一个无限增长的文件队列 CommitLog 中,CommitLog 是由一组 1G 内存映射文件队列组成的。写入时就从一个固定位置一直写下去,一个文件写满了就开启一个新文件顺序读写下去。
页缓存
  页缓存(PageCache)是OS对文件的缓存,用于加速对文件的读写。一般来说,程序对文件进行顺序读写的速度几乎接近于内存的读写速度,主要原因就是由于OS使用PageCache机制对读写访问操作进行了性能优化,将一部分的内存用作PageCache。对于数据的写入,OS会先写入至Cache内,随后通过异步的方式由pdflush内核线程将Cache内的数据刷盘至物理磁盘上。对于数据的读取,如果一次读取文件时出现未命中PageCache的情况,OS从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取。
零拷贝
  RocketMQ主要通过MappedByteBuffer对文件进行读写操作。其中,利用了NIO中的FileChannel模型将磁盘上的物理文件直接映射到用户态的内存地址中(这种Mmap的方式减少了传统IO,将磁盘文件数据在操作系统内核地址空间的缓冲区,和用户应用程序地址空间的缓冲区之间来回进行拷贝的性能开销),将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率(正因为需要使用内存映射机制,故RocketMQ的文件存储都使用定长结构来存储,方便一次将整个文件映射至内存)。
  在操作系统中,使用传统的方式,数据需要经历几次拷贝,还要经历用户态/内核态切换。

  1. 从磁盘复制数据到内核态内存;
  2. 从内核态内存复制到用户态内存;
  3. 然后从用户态内存复制到网络驱动的内核态内存;
  4. 最后是从网络驱动的内核态内存复制到网卡中进行传输。

可以通过零拷贝的方式,减少用户态与内核态的上下文切换和内存拷贝的次数,用来提升I/O的性能。零拷贝比较常见的实现方式是mmap,这种机制在Java中是通过MappedByteBuffer实现的。

13. 消息刷盘怎么实现

RocketMQ提供了两种刷盘策略:

同步刷盘:在消息达到Broker的内存之后,必须刷到commitLog日志文件中才算成功,然后返回Producer数据已经发送成功。
异步刷盘:异步刷盘是指消息达到Broker内存后就返回Producer数据已经发送成功,会唤醒一个线程去将数据持久化到CommitLog日志文件中。

        Broker 在消息的存取时直接操作的是内存(内存映射文件),这可以提供系统的吞吐量,但是无法避免机器掉电时数据丢失,所以需要持久化到磁盘中。
  刷盘的最终实现都是使用NIO中的 MappedByteBuffer.force() 将映射区的数据写入到磁盘,如果是同步刷盘的话,在Broker把消息写到CommitLog映射区后,就会等待写入完成。
  异步而言,只是唤醒对应的线程,不保证执行的时机。

14.RocketMQ消息长轮询

RocketMQ和Kafka都是利用“长轮询”来实现拉模式。

        长轮询,就是Consumer拉取消息,如果对应的Queue如果没有数据,Broker 不会立即返回,而是把PullReuqest hold起来,等待queue有了消息后,或者长轮询阻塞时间到了,再重新处理该queue上的所有PullRequest。

RocketMQ 中的长轮询
  RocketMQ中的PushConsumer其实是披着拉模式的方法,只是看起来像推模式而已。因为RocketMQ在被背后偷偷的帮我们去Broker请求数据了。
  Consumer后台会有个RebalanceService线程,这个线程会根据topic的队列数量和当前消费组的消费者个数做负载均衡。每个队列产生的pullRequest放入阻塞队列pullRequestQueue中。然后又有个PullMessageService线程不断的从阻塞队列pullRequestQueue中获取pullRequest,然后通过网络请求broker,这样实现的准实时拉取消息。
  然后Broker的PullMessageProcessor里面的processRequest方法是用来处理拉消息请求的,有消息就直接返回;没消息就挂起。

        PullRequestHoldService这个线程,会每5秒从pullRequestTable取PullRequest请求,然后看看待拉取消息请求的偏移量是否小于当前消费队列最大偏移量。如果条件成立则说明有新消息了,则会调用notifyMessageArriving,最终调用PullMessageProcessor的 executeRequestWhenWakeup()方法重新尝试处理这个消息的请求,也就是再来一次,整个长轮询的时间默认30秒。

简单的说,就是5秒会检查一次消息是否到了,如果到了则调用processRequest再处理一次。
  还有个ReputMessageService线程,这个线程用来不断地从commitLog中解析数据并分发请求,构建出ConsumeQueue和IndexFile两种类型的数据,并且也会有唤醒请求的操作,来弥补每5s一次这么慢的延迟,就是消息写入并且会调用 pullRequestHoldService#notifyMessageArriving。

RocketMQ采用“长轮询”的机制,具体的做法都是通过消费者等待消息,当有消息的时候Broker会直接返回消息,如果没有消息都会采取延迟处理的策略,并且为了保证消息的及时性,在对应队列或者分区有新消息到来的时候都会提醒消息来了,及时返回消息。
  一句话说就是消费者和Broker相互配合,拉取消息请求不满足条件的时候hold住,避免了多次频繁的拉取动作,当消息一到就提醒返回。

15.什么时候清理过期消息

4.6版本默认48(默认是72,但是broker配置文件默认改成了48,所以新版本都是48)小时后会删除不再使用的CommitLog文件。

检查这个文件最后访问时间。
判断是否大于过期时间。
指定时间删除,默认凌晨4点。

16.RocketMQ的负载均衡是如何实现的

 RocketMQ是分布式消息服务,负载均衡是在生产和消费的客户端完成的。

生产者的负载均衡
  实质是在选择MessageQueue对象(内部包含了brokerName和queueId),第一种是默认策略,从MessageQueue列表中随机选择一个,算法时通过自增随机数对列表打下取余得到位置信息,但获得的MessageQueue所在集群不能是上次失败集群。第二种是超时容忍策略,先随机选择一个MessageQueue,如果因为超时等异常发送失败,会优先选择该broker集群下其他MessageQueue发送,如果没找到就从之前发送失败的Broker集群中选一个进行发送,若还没有找到才使用默认策略。
消费者的负载均衡
  可选的有六种算法。
  1、平均分配算法。

        2、环形算法。

        3、指定机房算法。
  4、就近机房算法。
  5、统一哈希算法。使用一致性哈希算法进行负载,每次负载都会重建一致性hash路由表,获取本地客户端负责的所有队列信息。默认的hash算法为MD5,假设有4个消费者客户端和2个消息队列mq1和mq2,通过hash后分布在hash环的不同位置,按照一致性hash的顺时针查找原则,mq1被client2消费,mq2被client3消费。

    6、手动配置算法。

17.RocketMQ消息体过大的解决方案

官方定义消息体默认大小为 4MB,普通顺序消息类型。事务、定时、延时类消息默认大小为64KB。如果超过限制则会抛出异常。

方案一:消息压缩
  通常我们都是传递json消息数据,然后底层使用字节流进行传输。如果此时json数据超过4MB,则可以考虑进行消息压缩。原理其实很好理解,比如我们经常使用的压缩包,可以把大文件进行压缩,依次减小文件大小。那么我们这里需要使用到的就是字符压缩,把json字符串进行压缩,然后进行传输。
方案二:消息分割
  简而言之,就是把一个大消息体,进行分割成多个小消息体进行传输。
  一个大消息分割成多个小消息。
  多个小消息拥有相同的消息标识,如UUID。
  分割后小消息需要有一些元数据来标识自己,如 消息标识、一共分割了多少个、自己是第几个。
  传输后,消费者消费,然后根据元数据进行数据聚合还原。
  将还原后的消息走正常消费流程即可。

18.如何保证幂等性

1、全局唯一id
  如果使用全局唯一ID,就是根据业务的操作和内容生成一个全局ID,在执行操作前先根据这个全局唯一ID是否存在,来判断这个操作是否已经执行。如果不存在则把全局ID存储到存储系统中,比如数据库、redis等。如果存在则表示该方法已经执行。
  使用全局ID做幂等可以作为一个业务的基础的微服务存在,在很多的微服务中都会用到这样的服务,在每个微服务中都完成这样的功能,会存在工作量重复。另外打造一个高可靠的幂等服务还需要考虑很多问题,比如一台机器虽然把全局ID先写入了存储,但是在写入之后挂了,这就需要引入全局ID的超时机制。
  使用全局唯一ID是一个通用方案,可以支持插入、更新、删除业务操作。但是这个方案看起来很美但是实现起来比较麻烦,下面的方案适用于特定的场景,实现起来比较简单。
2、去重表
  这种方法适用于在业务中有唯一标的插入场景中,比如在支付场景中,如果一个订单只会支付一次,所以订单ID可以作为唯一标识。这时,我们就可以建一张去重表,并且把唯一标识作为唯一索引,在我们实现时,把创建支付单据和写入去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。
3、插入或更新
  这种方法插入并且有唯一索引的情况,比如我们要关联商品类,其中商品的ID和品类的ID可以构成唯一索引,并且在数据表中也增加了唯一索引。这时就可以使用InsertOrUpdate操作。

4、多版本控制
  这种方法适合在更新的场景中,比如我们要更新商品的名字,这时我们就可以在更新的接口中增加一个版本号,来做幂等。

5.状态机控制
  这种方法适合在有状态机流转的情况下,比如就会订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用int类型,并且通过值类型的大小来做幂等,比如订单的创建为0,付款成功为100。付款失败为99。在设计状态字段时,使用int类型,并且通过值类型的大小来做幂等,比如订单的创建为0,付款成功为100。付款失败为99。在做状态机更新时,这可以这样控制.

  • 31
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值