RocketMQ底层原理及实战(前面是原理,代码在后面)

RocketMQ

启动命令

启动Name Server:

nohup sh bin/mqnamesrv &

启动Broker:

nohup sh bin/mqbroker -n localhost:9876 &

关闭命令

关闭Broker:

sh bin/mqshutdown broker

关闭Name Server:

sh bin/mqshutdown namesrv

复制模式

复制模式主要指的master主机slave从机之间的数据同步策略。

同步复制

master主机在收到producer的消息之后,需要等待自己和slave从机都写入到本地才会向producer发送成功ack

异步复制

master将Producer的消息写入到本地后,直接向producer发送成功ack,无需等待slave从机是否将此条消息写入成功

优点:降低broker向producer响应延迟,RT变小,提高了系统的吞吐

缺点:可能会发生消息丢失

刷盘模式

刷盘策略指的是broker中消息的落盘方式,即消息发送到broker内存后消息持久化到磁盘的方式。分为同步刷盘与异步刷盘

同步刷盘

消息持久化到磁盘才算消息写入成功

异步刷盘

消息写入内存就算消息写入成功(一般存入page cache,而且并不是第一时间将内存中的数据持久化到磁盘,而是等待一定量后再持久化)

集群模式

单Master

只有一个broker,存在单点问题,适用于测试,此broker崩掉了,整个MQ集群都不可用了

多Master

  • 同一个topic的queue会平均分布在多个master上
  • 解决了单点问题,在一个broker宕机后,其他broker仍能顶上
  • 但是此宕机的broker中的消息无法被消费
  • 在RAID(磁盘阵列)参数为10时且采用同步刷盘策略,宕机瞬间不会导致消息丢失

多Master多slave-异步复制

  • 一般情况下,Master提供读写,slave主要作备份
  • Master宕机时,slave自动切换为Master,避免消息无法被消费
  • 若采用异步复制策略,在Master宕机瞬间,可能会有消息丢失问题

多Master多slave-同步复制

  • master会等待slave同步数据成功后才向producer返回成功ACK,即master与slave都要写入成功后才会
  • 返回成功ACK,也即双写。
  • 该模式与异步复制模式相比,优点是消息的安全性更高,不存在消息丢失的情况。但单个消息的RT略高,从而导致性能要略低(大约低10%)。
  • 该模式存在一个大的问题:对于目前的版本,Master宕机后,Slave不会自动切换到Master

磁盘阵列(RAID)

磁盘阵列通俗易懂一点就是磁盘备份方式

RAID0(数据条带技术)

在这里插入图片描述

  • 将数据分散到不同的地方进行存储
  • RAID0 具有低成本、高读写性能、 100% 的高存储空间利用率等优点,但是它不提供数据冗余保护,一
  • 旦数据损坏,将无法恢复
  • 应用场景:对数据的顺序读写要求不高,对数据的安全性和可靠性要求不高,但对系统性能要求很高的
  • 场景

RAID1(镜像技术)

在这里插入图片描述

  • 将数据进行复制备份,一旦工作磁盘发生故障,系统将自动切换到镜像磁盘,不会影响使用
  • 应用场景:对顺序读写性能要求较高,或对数据安全性要求较高的场景

RAID10

在这里插入图片描述

  • 先将数据均匀分散到不同的存储地方,然后再对这些数据单独做镜像备份处理
  • 先做条带,再做镜像。RAID10是一个RAID1与RAID0的组合体,所以它继承了RAID0的快速和RAID1的安全

消息的生产过程

  1. 消费者根据生产时指定的topic去nameserver中获取路由表broker表
  2. 消费者根据代码中指定的queue选择策略,在QueueData中选择一个Queue,进行消息存储
  3. 消费者会对消息做一些特殊处理,比如消息大小超过4M,就会对消息进行压缩
  4. Producer向选择出的Queue所在的Broker发出RPC请求,将消息发送到选择出的Queue

路由表:是一个map,key为topic名字,value为QueueData

broker表:是一个map,key为BrokerName,value是BrokerData

Queue选择算法

轮询算法

默认选择算法。该算法保证了每个Queue中可以均匀的获取到消息

该算法存在一个问题:由于某些原因,在某些Broker上的Queue可能投递延迟较严重。从而导致

Producer的缓存队列中出现较大的消息积压,影响消息的投递性能

最小投递延时算法

优先投递给返回延时最小、响应最快的Queue,若每个Queue延时时间相同,则采用轮询算法

该算法也存在一个问题:消息在Queue上的分配不均匀。投递延迟小的Queue其可能会存在大量

的消息。而对该Queue的消费者压力会增大,降低消息的消费能力,可能会导致MQ中消息的堆

积。

消息的存储

在这里插入图片描述

  • abort:该文件在Broker启动后会自动创建,正常关闭Broker,该文件会自动消失。若在没有启动Broker的情况下,发现这个文件是存在的,则说明之前Broker的关闭是非正常关闭。
  • checkpoint:其中存储着commitlog、consumequeue、index文件的最后刷盘时间戳
  • commitlog:其中存放着commitlog文件,而消息是写在commitlog文件中的
  • conæg:存放着Broker运行期间的一些配置数据
  • consumequeue:其中存放着consumequeue文件,队列就存放在这个目录中
  • index:其中存放着消息索引文件indexFile
  • lock:运行期间使用到的全局资源锁

commitlog

  • commitlog目录主要负责存储MapperFile文件,MapperFile是用来存储生产者生产的消息的。
  • 每个MapperFile文件可以存储1G的数据,每个MapperFile的文件名由20位十进制数构成,是当前文件第一条消息的偏移量
  • 一个Broker中仅包含一个commitlog目录,所有的mappedFile文件都是存放在该目录中的
  • 消息都是被顺序写入到了mappedFile文件中的
  • 这些消息在Broker中存放时并没有被按照Topic进行分类存放
    在这里插入图片描述

mappedFile文件内容由一个个的消息单元构成。每个消息单元中包含消息总长度MsgLen、消息的物理位置physicalOffset、消息体内容Body、消息体长度BodyLength、消息主题Topic、Topic长度TopicLength、消息生产者BornHost、消息发送时间戳BornTimestamp、消息所在的队列QueueId、消息在Queue中存储的偏移量QueueOffset等近20余项消息相关属性

consumequeue

在这里插入图片描述

  • consumequeue主要保存Queue队列信息
  • consumequeue文件是commitlog的索引文件,可以根据consumequeue定位到具体的消息
  • consumequeue目录下以topic进行分类,topic目录下以Queue ID进行分类
  • 每个consumequeue文件可以包含30w个索引条目,每个索引条目包含了三个消息重要属性:消息在mappedFile文件中的偏移量CommitLog Offset、消息长度、消息Tag的hashcode值。这三个属性占20个字节,所以每个文件的大小是固定的30w * 20字节。
    在这里插入图片描述
    在这里插入图片描述

对文件的读写

消息写入

在这里插入图片描述

  1. Broker在接收到一条消息后,会根据Topic和Queue ID,到相应的consumequeue文件中获取写入Queue Offset(写入偏移量)
  2. Broker将Queue ID、Queue Offset(写入偏移量)、消息体等数据,封装为消息单元
  3. 将消息单元写入commit log中
  4. 同时形成索引条目,分发写入到相应的consumequeue中
消息拉取
  1. 消费者会获取指定Queue ID的消费偏移量,然后计算出应该要消费的Queue Offset
  2. 消费者带着这个Queue Offset、Queue ID及消息Tag等,向Broker发送拉取请求
  3. Broker根据Queue Offset找到第一个与消息tag相等的索引单元,并解析该索引单元的前8个字节,即commitlog offset
  4. Borker根据解析出来的commitlog offset,将commit log目录下的该消息单元发送给消费者

为什么RocketMQ性能好

mmap实现零拷贝

传统IO:read() + write()

  1. 用户进程通过read()方法向系统发起调用,上下文从用户态转换为内核态
  2. DMA控制器把数据从硬盘拷贝到内核读缓冲区
  3. CPU内核缓存区的数据拷贝到用户缓冲区
  4. 内核态转换为用户态,read()方法执行完毕
  5. 用户进程通过write()方法向系统发起调用,上下文又从用户态转换为内核态
  6. CPU将用户缓冲区的数据拷贝到socket缓冲区/内核写缓冲区
  7. DMA控制器将socket缓冲区/内核写缓冲区的数据拷贝到网卡
  8. 内核态转换为用户态,write()方法执行完毕

mmap实现零拷贝:mmap主要实现方式是将内核读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和用户缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次CPU拷贝

  1. 用户进程通过mmap()方法向系统发起调用,上下文从用户态转换为内核态
  2. DMA控制器把数据从硬盘拷贝到内核读缓冲区
  3. 内核态转换为用户态,mmap()方法执行完毕
  4. 用户进程通过write()方法向系统发起调用,上下文又从用户态转换为内核态
  5. CPU将用户缓冲区的数据拷贝到socket缓冲区/内核写缓冲区
  6. DMA控制器将socket缓冲区/内核写缓冲区的数据拷贝到网卡
  7. 内核态转换为用户态,write()方法执行完毕

引入Page Cache

  1. PageCache机制,页缓存机制,是OS对文件的缓存机制,用于加速对文件的读写操作。
  2. 一般来 说,程序对文件进行顺序读写的速度几乎接近于内存读写速度,主要原因是由于OS使用 PageCache机制对读写访问操作进行性能优化,将一部分的内存用作PageCache
  • 写操作:OS会先将数据写入到PageCache中,随后会以异步方式由pdæ ush(page dirty flush)内核线程将Cache中的数据刷盘到物理磁盘
  • 读操作:若用户要读取数据,其首先会从PageCache中读取,若没有命中,则OS在从物理磁盘上加载该数据到PageCache的同时,也会顺序对其相邻数据块中的数据进行预读取。

indexFile

  • indexFile文件主要是方便根据key进行消息查询的功能。
  • 这个indexFile中的索引数据是在包含了key的消息被发送到Broker时写入的,若消息中没有key数据,则不会写入
  • indexFile文件名为创建时的时间戳,方便在进行指定时间查找时,选择合适的目录

索引条目结构

  • 每个indexFile都是以一个时间戳命名的(这个indexFile被创建时的时间戳)。
  • 每个indexFile文件由三部分构成:indexHeader,slots槽位,indexes索引数据。
  • 每个indexFile文件中包含500w个slot槽。而每个slot槽又可能会挂载很多的index索引单元
    在这里插入图片描述

indexHeader固定40个字节,其中存放着如下数据:
在这里插入图片描述

  • beginTimestamp:该indexFile中第一条消息的存储时间
  • endTimestamp:该indexFile中最后一条消息存储时间
  • beginPhyoffset:该indexFile中第一条消息在commitlog中的偏移量commitlog offset
  • endPhyoffset:该indexFile中最后一条消息在commitlog中的偏移量commitlog offset
  • hashSlotCount:已经填充有index的slot数量(并不是每个slot槽下都挂载有index索引单元,这
  • 里统计的是所有挂载了index索引单元的slot槽的数量)
  • indexCount:该indexFile中包含的索引单元个数(统计出当前indexFile中所有slot槽下挂载的所
  • 有index索引单元的数量之和)

indexFile中最复杂的是Slots与Indexes间的关系。在实际存储时,Indexes是在Slots后面的,但为了便

于理解,将它们的关系展示为如下形式:
在这里插入图片描述

key的hash值 % 500w的结果即为slot槽位,然后将该slot值修改为该index索引单元的indexNo,根

据这个indexNo可以计算出该index单元在indexFile中的位置。不过,该取模结果的重复率是很高的,

为了解决该问题,在每个index索引单元中增加了preIndexNo,用于指定该slot中当前index索引单元的

前一个index索引单元

indexNo是一个流水号,是依次递增的,indexNoindex索引单元中是没有体现的,其是通过indexes中依次数出来的

index索引单元默写20个字节,其中存放着以下四个属性:
在这里插入图片描述

  • keyHash:消息中指定的业务key的hash值
  • phyOffset:当前key对应的消息在commitlog中的偏移量commitlog offset
  • timeDiff:当前key对应消息的存储时间与当前indexFile创建时间的时间差
  • preIndexNo:当前slot下当前index索引单元的前一个index索引单元的indexNo

indexFile的创建

  • 在接收第一个含有key的消息时创建
  • 当一个indexFile中挂载的index索引单元数量超出2000w个时,会创建新的indexFile(当带key的消息发送到来后,系统会找到最新的indexFile,并从其indexHeader的最后4字节中读取到indexCount。若indexCount >= 2000w时,会创建新的indexFile)

由于可以推算出,一个indexFile的最大大小是:*(40 + 500w * 4 + 2000w * 20)*字节

一条消息的查询流程

计算槽位
slot槽位序号 = key的hash % 500w
计算槽位序号为n的slot在indexFile中的起始位置: 
slot(n)位置 = 40 + (n - 1) * 4
计算indexNo为m的index在indexFile中的位置: 
index(m)位置 = 40 + 500w * 4 + (m - 1) * 20

具体查询流程
在这里插入图片描述

消息的消费

消费类型

拉取式消费
  • consumer定时主动向Broker获取消息,一旦获取了批量消息, consumer就开始消费。
  • 但是Broker中一旦有了新消息,consumer并不能够知道,只能等待consumer下次主动拉取消费

consumer主动拉取broker的消息是有时间间隔的,这个间隔是自定义的

如果间隔过短,空请求比例会增加;如果间隔过长,消息的实时性太差

推送式消费
  • broker与consumer建立长连接
  • 典型的发布-订阅模型,consumer向其关联的queue注册了监听器,broker中一旦有新消息产生,broker主动向consumer推送,告诉consumer自己有新的消息产生,可以过来拉取消息进行消费了
  • 然后consumer向broker发送拉取消息请求,获取批量消息进行消费
对比

pull:需要应用去实现对queue的遍历。consumer自己主动拉取,实时性较差

push:实时性较强,封装了对queue的遍历,但是需要维护长连接等,占用系统资源

消费模式

集群消费

在这里插入图片描述

相同Consumer Group的每个Consumer实例平均分摊同一个Topic的消息

同一种topic消息只能被一个Consumer Group里面的一个consumer消费,消费进度保存在broker中

广播消费

在这里插入图片描述

Consumer Group里面的每一个consumer都要消费topic中的每一条消息,消息消费进度保存在每个consumer

本地

消息进度保存

集群消费:保存在broker中。消费进度会参与到了消费的负载均衡中,故消费进度是需要共享的

广播消费:保存在每个consumer本地。

Rebalance机制

什么是Rebalance
  • Rebalance机制讨论的前提是:集群消费。因为广播消费是所有consumer都会消费同一条信息
  • rebalance是在queue进行扩缩容、broker扩缩容、consumer扩缩容时,重新给每个consumer分配所对应的queue
    在这里插入图片描述

Rebalance机制的本意是为了提升消息的并行消费能力。例如,⼀个Topic下5个队列,在只有1个消费者的情况下,这个消费者将负责消费这5个队列的消息。如果此时我们增加⼀个消费者,那么就可以给其中⼀个消费者分配2个队列,给另⼀个分配3个队列,从而提升消息的并行消费能力。

也就是将每个consumer最大化使用。

Rebalance的限制

因为每个queue最多被分配给一个consumer,所以当consumer数量大于queue数量时,多的consumer只能处于空闲状态

Rebalance危害
消费暂停

当在对consumer重新分配queue时,全部consumer都处于阻塞状态,无法正常消费消息,直到重新分配到属于自己的queue

消费重复

在重新分配的那一刻,可能有consumer正在消费消息,但是消息消费进度还没更新好就阻塞了,当重新分配完以后,后序的consumer可能会重新消息这条已经"消费"过的消息

消费突刺

由于存在消费重复,可能需要重复消费的消息很多,或者因为消费暂停时间过长,产生了消息堆积,rebalance后,下一个消费者可能需要突然消费很多消息,优点吃不消

Rebalance产生的原因
  1. Queue数量发生变化
  2. Consumer Group中Consumer数量发生变化

1)Queue数量发生变化的场景

Broker发生扩容或缩容

Broker升级运维

Broker与NameServer间的网络异常

Queue扩容或缩容

2)Consumer数量发生变化的场景

Consumer发生扩容或缩容

Consumer升级运维

Consumer与NameServer的网络异常

Rebalance的过程

在Broker中维护着多个Map集合,这些集合中存放着queue信息、consumer group中的consumer信息,一旦消费者所订阅的queue数量发生变化或者consumer数量发生变化,立即向consumer group中的每个实例发出Rebalance请求。

Queue的分配算法

一个queue只能被一个consumer消费,但是一个consumer可以消费多个queue

常见的queue分配策略有四种,这些策略是通过在创建consumer时的构造器传进去的

平均分配策略

在这里插入图片描述

每个 C o n s u m e r 分配的 Q u e u e = Q u e u e 数量 C o n s u m e r + 1 个余数 每个Consumer分配的Queue=\frac{Queue数量}{Consumer} + 1个余数 每个Consumer分配的Queue=ConsumerQueue数量+1个余数
该算法是要根据avg = QueueCount / ConsumerCount 的计算结果进行分配的。如果能够整除,则按顺序将avg个Queue逐个分配Consumer;如果不能整除,则将多余出的Queue按照Consumer顺序逐个分配

环形分配策略

在这里插入图片描述

把Queue串成一个环,依次给每个Consumer分配一个Queue,直到分配完为止

该算法不用事先计算每个Consumer需要分配几个Queue,直接一个一个分即可

一致性hash策略

在这里插入图片描述

将Queue的hash值和Consumer的hash值算出来串成一个环,通过顺时针方向,Consumer离哪些Queue近,就将这些Queue分配给它

可能会造成Queue的分配不均匀

同机房策略

在这里插入图片描述

  • 会根据Queue部署的机房位置和consumer的部署位置,筛选出与当前consumer相同的queue,然后采用平均分配算法或环形平均算法进行Queue分配
  • 若该机房没有queue,则按照平均分配策略或环形平均策略对所有Queue进行分配
对比
  • 一致性hash算法有hash计算,算法较为复杂,分配效率较低,且一致性hash可能造成Queue分配不均

    一致性hash存在的意义:有效减少由于消费者组扩容或缩容所带来的大量的Rebalance

    一致性hash算法的应用场景:Consumer数量变化较频繁的场景

  • 平均分配算法和环形平均算法效率较高

订阅关系的一致性

订阅关系的一致性是指同一个Consumer Group中所有Consumer必须订阅相同的Topic和Tag,否则就会造成消息的处理逻辑混乱,甚至消息丢失

正确订阅关系

多个消费者组订阅了多个Topic,并且每个消费者组里的多个消费者实例的订阅关系保持了一致。

同一个Consumer Group下:

每个Consumer订阅的Topic名称和数量要相同

每个Consumer订阅的数量要相同
在这里插入图片描述

错误订阅关系

每个Consumer订阅的Topic不同

每个Consumer订阅的Tag数量不同

每个Consumer订阅的Topic数量不同

Offset管理(消费进度Offset)

本地管理

广播消费模式下,消费进度Offset保存在每个consumer的本地

远程管理

集群模式下,消费进度Offset保存在broker中,防止一条消息被重复消费,保证一致性

Offect用途

Offect用来确定consumer从Queue中哪条消息开始消费,通过consumer.setConsumeFromWhere()方法来设置Offect,有三个值

  • CONSUME_FROM_FIRST_OFFECT:从Queue中第一条消息开始消费
  • CONSUME_FROM_LAST_OFFECT:从Queue中最后一条消息开始消费
  • CONSUME_FROM_TIMESTAMP:从指定时间戳开始消费

当Consumer消费完一批消息后,就像broker发送信息,broker将消费进度保存到consumerOffectManager和consumerOffect.json文件中,然后broker向Consumer发送一个ack,ack中包含了下一次消费的最小Offect(minOffect)、最大Offect(maxOffect)、以及Offect消费起始位置(nextBegainOffect)

重试队列

在Broker发生异常时,会主动为创建该Topic Group创建以%RETRY%为名重试队列,并将异常的Offect存进重试队列中,在Broker恢复异常时,重新消费重试队列里面的消息

Offect的同步提交和异步提交

集群模式下,Consumer提交Offect的方式有两种:

同步提交

Consumer提交Offect到Broker后,需要等待Broker返回的的ack,若等待时间超时,则Consumer会重新提交Offect,直到拿出ack中的nextBegainOffect,才开始下一次消费

异步提交

Consumer提交Offect到Broker后,无需等待Broker返回ack,直接读取Broker中的nextBegainOffect开始下一次消费,但是最终Broker仍然会返回ack给Consumer

消费幂等性

什么是消费幂等性

消费幂等性就是:一条消息被Consumer消费一次或多次后,结果都是相同的,但是对系统没有任何影响

消息重复的场景

发送消息时重复

Producer生产消息发送给Broker,Broker将消息存到自己本地后,由于网络中断,无法正常给Producer返回MessageID,Producer由此消息生产不成功,会再次向Broker生产同样的消息。此后Broker中就有两条甚至多条重复的消息

消费时消息重复

Consumer在消费完消息后,会将消费进度Offect返回给Broker,但是此时由于网络中断,Broker没有收到消费进度Offect,以为Consumer没有消费成功,就重新发送这批消息的ack给Consumer。造成Consumer重复消费

Rebalance时消息重复

在Queue数量发生变化,或Consumer数量发生变化时,会发生Rebalance。此时Consumer可能会收到已经消费过的消息

通用解决方案
  • 幂等令牌

    幂等令牌也就是唯一性标识,从Producer到Broker,从Broker到Consumer,都以这个唯一性标识来判断是不是重复消息

  • 唯一性处理

    通过一定的算法策略,保证同一条消息不会被执行多次

  1. 首先通过缓存去重。一条消息进来时,先看缓存中是否已经有了此幂等令牌,若有,则代表消息重复,舍弃。若缓存中没有(可能是缓存过期,也有可能真不是重复消息),则进入下一步
  2. 查看数据库中是否有次幂等令牌,若有,则代表消息重复,舍弃。若没有,则证明不是重复消息,则进入下一步
  3. 将此幂等令牌存入缓存中,并将此幂等令牌作为唯一索引持久化到数据库中

消息堆积和消费延迟

什么是消费堆积

消费堆积就是Producer生产消息太快,Consumer消息的消费速度慢于Producer生产的速度,长时间后,造成消息堆积

什么是消费延迟

一条消息消费的时间太长,就会造成消费延迟

产生原因分析

在这里插入图片描述

Consumer使用长轮询Pull模式消费消息时,分为以下两个阶段:

消息拉取

Consumer通过长轮询Pull模式批量拉取的方式从服务端获取消息,将拉取到的消息缓存到本地缓冲队列中。对于拉取式消费,在内网环境下会有很高的吞吐量,所以这一阶段一般不会成为消息堆积的瓶颈。

一个单线程单分区的低规格主机*(Consumer*,4C8G),其可达到几万的TPS。如果是多个分区多个线程,则可以轻松达到几十万的TPS

消息消费

Consumer将本地缓存的消息提交到消费线程中,Consumer的消费能力完全取决于消费耗时消费并发度,如果消费耗时过长,Consumer的吞吐量肯定不高,就会导致Consumer本地缓冲队列达到上限,停止从Broker中拉取消息

结论

消息堆积和消费延迟的瓶颈主要在与消费耗时消费并发度。并且消费耗时的优先度是要大于消费并发度的,在保证了消费耗时的合理性前提下,再考虑消费并发度问题

消费耗时

影响消息处理时长的主要因素是代码逻辑。而代码逻辑中可能会影响处理时长代码主要有两种类型:

CPU内部计算代码

通常情况下代码中如果没有复杂的递归和循环的话,内部计算耗时相对外部I/O操作来说几乎可以忽略

对外部的I/O操作

  • 读写外部数据库,例如对远程MySQL的访问
  • 读写外部缓存系统,例如对远程Redis的访问
  • 下游系统调用,例如DubboRPC远程调用,Spring Cloud的对下游系统的Http接口调用

通常消息堆积都是由于下有系统出现服务异常或达到了数据库容量限制

还有可能是因为网络带宽问题

总之,下游系统的高可用和响应快是消费耗时的决定性因素

消费并发度

一般情况下,消费端的消费并发度由单节点线程数 × 节点数决定

对于普通消息,消费并发度由单节点线程数 × 节点数决定

对于顺序消息,消费并发度由Queue的数量决定

  1. 对于全局顺序消息:该Topic只有一个Queue分区,同一时刻只能有一个线程消费Queue,所以并发度为1
  2. 对于分区顺序消息:该Topic有多个Queue分区,同一时刻只能有一个线程消费某个Queue的消息,所以并发度为该Topic有多个Queue数量

单机线程数计算公式

对于一台主机中线程池中线程数的设置需要谨慎,不能盲目直接调大线程数,设置过大的线程数反而会带来大量的线程切换的开销。理想环境下单节点的最优线程数计算模型为:
C × ( T 1 + T 2 ) T 1 C ×\frac{(T1 + T2)}{T1} C×T1(T1+T2)

C:CPU内核数

T1:CPU内部逻辑计算耗时

T2:外部IO操作耗时

注意,该计算出的数值是理想状态下的理论数据,在生产环境中,不建议直接使用。而是根据当前环境,先设置一个比该值小的数值然后观察其压测效果,然后再根据效果逐步调大线程数,直至找到在该环境中性能最佳时的值

如何避免

梳理消息的消费耗时

  • 优化代码中复杂度较高的代码
  • 查看外部IO操作是否必须的,能否用本地缓存代替
  • 业务逻辑能否异步处理

设置合理的消费并发度

  • 根据上游的消息生产,设置合理的Consumer Group节点数
  • 逐步调大单个Conmser的线程数进行压测,以求最优解

节点数 = 流量峰值 单个节点消息吞吐量 节点数 = \frac{流量峰值}{单个节点消息吞吐量} 节点数=单个节点消息吞吐量流量峰值

消息文件的清理

消息都存储在commitlog文件中,一个commitlog文件的大小是1G,系统是以单个文件为单位进行消息文件清理

commitlog文件存在过期时间,默认为72小时,即三天。除了手动清理外,以下情况也会被清理,无论消息是否被消费过

  • 文件过期,且达到文件清理时间(默认为每天的凌晨四点),自动清理过期文件
  • 文件过期,且磁盘空间占用率超过75%,无论是否达到清理时间,都会自动清理过期文件
  • 磁盘空间占用率超过85%,立马从最老的文件开始清理,无论是否过期
  • 磁盘占用率超过90%,Broker直接拒绝写入

普通消息

消息发送分类

同步发送消息

同步发送消息是指,Producer在发出一条消息后,需要等待Broker返回的ACK,才会发送下一条消息。这样可靠性比较高,但是发送效

率较低
在这里插入图片描述

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

/**
 * 同步消息生产者
 */
public class SyncProducer {
    public static void main(String[] args) throws MQBrokerException, RemotingException, InterruptedException, MQClientException {
        //创建同步消息生产者
        DefaultMQProducer syncProducer = new DefaultMQProducer("sync"); //sync为生产者组名字
        //设置Broker中Queue的数量
        syncProducer.setDefaultTopicQueueNums(2);
        //设置发送失败后重试次数 默认2次
        syncProducer.setRetryTimesWhenSendFailed(3);
        //设置发送超时时间 默认5秒
        syncProducer.setSendMsgTimeout(5000);
        //设置mq地址
        syncProducer.setNamesrvAddr("192.168.56.104:9876");
        //启动producer
        syncProducer.start();
        for (int i = 0; i < 100; i++) {
            byte[] bytes = ("sync_message_" + i).getBytes();
            //创建消息
            Message message = new Message("sync_topic","sync_tag", bytes);
            //发送消息
            SendResult sendResult = syncProducer.send(message);
            //打印消息
            System.out.println(sendResult);
            Thread.sleep(1000);
        }
        //关闭消息
        syncProducer.shutdown();
    }
}
异步发送消息

Producer在发出一条消息后,无需等待Broker返回的ACK,直接发送下一条消息。该方式的消息可靠性可以得到保障,消息发送效率也可以
在这里插入图片描述

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.util.concurrent.TimeUnit;

/**
 * 异步消息生产者
 */
public class AsyncProducer {
    public static void main(String[] args) throws RemotingException, InterruptedException, MQClientException {
        //创建异步生产者
        DefaultMQProducer asyncProducer = new DefaultMQProducer("async");
        //设置mq地址
        asyncProducer.setNamesrvAddr("192.168.56.104:9876");
        //异步发送失败时,重试次数
        asyncProducer.setRetryTimesWhenSendAsyncFailed(0);
        //开启生产者
        asyncProducer.start();
        for (int i = 0; i < 100; i++) {
            byte[] bytes = ("async_message_" + i).getBytes();
            //创建message
            Message message = new Message("async_topic", "async_tag", bytes);
            //异步发送回调
            asyncProducer.send(message, new SendCallback() {
                //发送成功时回调
                @Override
                public void onSuccess(SendResult sendResult) {
                    System.out.println(sendResult);
                }

                //发送失败时回调
                @Override
                public void onException(Throwable e) {
                    System.out.println(e.getMessage());
                }
            });
        }
        TimeUnit.SECONDS.sleep(3);
        asyncProducer.shutdown();
    }
}
单向发送消息

Producer一直向Broker发送消息,不管Broker有没有收到。可靠性无法保证
在这里插入图片描述

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

/**
 * 单向消息生产者
 */
public class OneWayProducer {
    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException {
        //创建异步生产者
        DefaultMQProducer oneWayProducer = new DefaultMQProducer("one_way");
        //设置mq地址
        oneWayProducer.setNamesrvAddr("192.168.56.104:9876");
        //异步发送失败时,重试次数
        oneWayProducer.setRetryTimesWhenSendAsyncFailed(0);
        //开启生产者
        oneWayProducer.start();
        for (int i = 0; i < 100; i++) {
            byte[] bytes = ("oneway_" + i).getBytes();
            Message message = new Message("oneway_tipic", "oneway_tag", bytes);
            oneWayProducer.sendOneway(message);
        }
        oneWayProducer.shutdown();
    }
}
消费者
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

/**
 * 同步消息生产者
 */
public class SyncProducer {
    public static void main(String[] args) throws MQBrokerException, RemotingException, InterruptedException, MQClientException {
        //创建同步消息生产者
        DefaultMQProducer syncProducer = new DefaultMQProducer("sync"); //sync为生产者组名字
        //设置Broker中Queue的数量
        syncProducer.setDefaultTopicQueueNums(2);
        //设置发送失败后重试次数 默认2次
        syncProducer.setRetryTimesWhenSendFailed(3);
        //设置发送超时时间 默认5秒
        syncProducer.setSendMsgTimeout(5000);
        //设置mq地址
        syncProducer.setNamesrvAddr("192.168.56.106:9876");
        //启动producer
        syncProducer.start();
        for (int i = 0; i < 100; i++) {
            byte[] bytes = ("sync_message_" + i).getBytes();
            //创建消息
            Message message = new Message("sync_topic","sync_tag", bytes);
            //发送消息
            SendResult sendResult = syncProducer.send(message);
            //打印消息
            System.out.println(sendResult);
        }
        //关闭消息
        syncProducer.shutdown();
    }
}

顺序消息

什么是顺序消息

顺序消息就是严格按照消息发送的顺序进行消费

默认情况下生产者会把消息以Round Robin轮询方式发送到不同的Queue分区队列;而消费消息时会从多个Queue上拉取消息,这种情况下的发送和消费是不能保证顺序的。如果将消息仅发送到同一个Queue中,消费时也只从这个Queue上拉取消息,就严格保证了消息的顺序性。

有序性的分类

全局有序

在这里插入图片描述

当只有一个Queue时,消费者按照Queue中消息的存储顺序消费,即为全局有序

在创建Topic时。有三种指定方式:

1)在创建Producer时,指定该Topic的Queue数量

2)在Rocket DashBoard,修改Queue的数量

3)使用mqadmin命令手动创建Topic时指定Queue的数量

分区有序

在这里插入图片描述

当有多个Queue时,其仅可保证在该Queue分区队列上的消息顺序,则称为分区有序。

我们可以定义一个Queue选择器,使用一定的算法,将相同业务的消息发送到指定的Queue上,进行顺序消费。

这个key可以是选择key,也可以是消息key。但是无论谁做key,都不能重复,都是唯一的

一般性的选择算法是,让选择key(或其hash值)与该Topic所包含的Queue的数量取模,其结果即为选择出的QueueQueueId

取模运算会造成不同的key消息发送到同一个Queue中,同一个Consuemr可能会消费到不同选择key的消息。一般性的作法是,从消息中获取到选择key,对其进行判断。若是当前Consumer需要消费的消息,则直接消费,否则,什么也不做。这种做法要求选择key要能够随着消息一起被Consumer获取到。此时使用消息key作为选择key是比较好的做法。

以上做法会不会出现如下新的问题呢?不属于那个Consumer的消息被拉取走了,那么应该消费该消息的Consumer是否还能再消费该消息呢?同一个Queue中的消息不可能被同一个Group中的不同Consumer同时消费。所以,消费现一个Queue的不同选择key的消息的Consumer一定属于不同的Group。而不同的Group中的Consumer间的消费是相互隔离的,互不影响的

代码示例
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.util.List;

public class QueueSelector {

    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("sync");
        producer.setDefaultTopicQueueNums(4);
        producer.setNamesrvAddr("192.168.56.106:9876");
        //全局有序
        producer.setDefaultTopicQueueNums(1);
        producer.start();
        for (int i = 0; i < 50; i++) {
            int orderId = i;
            byte[] bytes = ("test_" + i).getBytes();
            Message message = new Message("SelectorTest","test",bytes);
            producer.send(message, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> list, Message message, Object arg) {
                    //使用args作为key,arg就是orderId
                    Integer id = (Integer) arg;
                    int index =  id % list.size();
                    return list.get(index);
                    //使用消息key进行计算
//                    String key = message.getKeys();
//                    Integer id = Integer.valueOf(key);
//                    int index =  id / list.size();
                }
            }, orderId);
        }
        producer.shutdown();
    }
}

延迟消息

什么是延迟消息

当消息写入到Broker后,在指定的时长后才可被消费处理的消息,称为延时消息.

无需我们自己设置定时器,只需要设置延时的时间,RocketMQ自动会在时间到期后,自动将消息交给Consumer由其消费

延时等级

延时消息的延迟时长不支持随意时长的延迟,是通过特定的延迟等级来指定的。延时等级定义在RocketMQ服务端的MessageStoreConfig类中的如下变量中:
在这里插入图片描述

即,若指定的延时等级为3,则表示延迟时长为10s,即延迟等级是从1开始计数的。

我们也可以在Broker中的conf文件中自定义延时等级,可以通过在broker加载的配置中新增一下配置(例如下面增加了1天这个等级1d)。配置文件在RocketMQ安装目录下的conf目录中。

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

延时消息实现原理

在这里插入图片描述

修改消息

broker将收到Producer发送过来的消息保存到commitlog后,会判断此条消息是否带有延时等级,若没有延时等级,则会直接分发到对应的Topic中,若有延时等级,则会经过一系列复杂的过程:

  • 修改消息的Topic为SCHEDULE_TOPIC_XXXX

  • 根据延时等级,在SCHEDULE_TOPIC_XXXX下创建QueueID目录与consumequeue文件(若之前没有创建过)

    QueueID = 延时等级 - 1

    Queue ID并不是一次性创建所有的,而是有哪些延时等级的消息,才创建对应的QueueID

    相同延时等级的消息,会被存入同一个QueueID文件中

  • 修改消息索引单元的内容。将consumequeue文件中该消息索引单元的tag hashcode改为投递时间,投递时间 = 消息写入commit时间 + 延时时长。

    SCHEDULE_TOPIC_XXXX目录中各个延时等级Queue中的消息是如何排序的?

    是根据消息写入commitlog文件中的时间由小到大进行排序的,因为根据延时消息的存储规则,相同延时等级的消息会被存储在同一个文件中,他们的延时时间是相同的,此消息的投递时间就全看消息的生产时间

投递延时消息

Boroker内部有一个延迟消息服务类ScheuleMessageService,它会为每个Queue创建一个定时器TimerTask,负责延时消息等级的消费与投递,每个TimerTask负责该Queue消息的消费与投递,ScheuleMessageService一旦发现该Queue中的第一条延时时间到了,就会将该消息从commitlog从读出,并将该消息的延时等级设为0,然后将该消息投递到原来指定的Topic中

将消息重新写入commitlog

延迟消息服务类ScheuleMessageService将延迟消息再次发送给了commitlog,并再次形成新的消息索引条目,分发到相应Queue

整个过程无非就是ScheuleMessageService拦截了延时消息并将延时消息暂时存放在了SCHEDULE_TOPIC_XXXX文件中,并生成计时器对其进行延时到期计算,当时间一到,就再将这条消息发送到消息指定的Topic中,供Consumer消费

代码示例

定义DelayProducer类

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

public class DelayProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("delay");
        producer.setNamesrvAddr("192.168.56.106:9876");
        producer.start();
        byte[] content = "delay-content".getBytes();
        Message message = new Message("delay_topic", content);
        //设置延时等级
        message.setDelayTimeLevel(3);
        producer.send(message);
        producer.shutdown();
    }
}

定义DelayConsumer类

import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.Arrays;
import java.util.List;

public class DelayConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultLitePullConsumer consumer = new DefaultLitePullConsumer("delay");
        consumer.subscribe("delay_topic", "*");
        consumer.setNamesrvAddr("192.168.56.106:9876");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.start();
        while (true) {
            List<MessageExt> list = consumer.poll(1000);
            for (MessageExt messageExt : list) {
                System.out.println(Arrays.toString(messageExt.getBody()));
            }
        }
    }
}

事务消息

RocketMQ提供了类似X/Open XA的分布式事务功能,通过事务消息能达到分布式事务的最终一致。XA是一种分布式事务解决方案,一种分布式事务处理模式

本地事务状态

Producer回调操作执行的结果为本地事务状态,其会发送给TC,而TC会再发送给TM。TM会根据各个Producer的回调结果决定事务是全局提交还是全局回滚

// 描述本地事务执行状态
public enum LocalTransactionState {
COMMIT_MESSAGE, // 本地事务执行成功
ROLLBACK_MESSAGE, // 本地事务执行失败
UNKNOW, // 不确定,表示需要进行回查以确定本地事务的执行结果
}

消息回查

在这里插入图片描述

消息回查:即重新查询Producer的回调结果,看Producer的本地事务是否执行成功

引发消息回查的两种最常见场景:

1)Producer向TM返回Unknow状态

2)因为网络波动等原因,TC一直未收到TM的全局事务确认指令

RocketMQ中的消息回查设置

关于消息回查,有三个常见的属性设置。它们都在broker加载的配置文件中设置,例如:

  • transactionTimeout = 60,指定TM需要在20S内将全局结果发送给TC,否则TC会引发消息回查。
  • transactionCheckMax = 15,最多回查15次,吵过后将丢弃消息并记录错误日志。
  • transactionCheckInterval = 60,每次执行回查的时间间隔。

XA模式三剑客

XA协议

XA(Unix Transaction)是一种分布式事务解决方案,一种分布式事务处理模式,是基于XA协议的。XA协议由Tuxedo(Transaction for Unix has been Extended for Distributed Operation,分布式操作扩展之后的Unix事务系统)首先提出的,并交给X/Open组织,作为资源管理器与事务管理器的接口标准。

XA模式中有三个重要组件:TC、TM、RM。

TC

Transaction Coordinator,事务协调者。维护全局和分支事务的状态,驱动全局事务提交或回滚。

RocketMQ中Broker充当着TC

TM

Transaction Manager,事务管理器。定义全局事务的范围:开始全局事务、提交或回滚全局事务。它实际是全局事务的发起者。

RocketMQ中事务消息的Producer充当着TM

RM

Resource Manager,资源管理器。管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

RocketMQ中事务消息的Producer及Broker均是RM

XA模式架构

在这里插入图片描述

XA模式是一个典型的2PC,其执行原理如下:

  1. TM向TC发送指令,开启一个全局事务
  2. 根据业务要求,各个RM会逐个向TC注册分支事务,然后TC会逐个向RM发出预执行指令。
  3. 各个RM在接收到指令后会在进行本地事务预执行。
  4. RM将预执行结果Report给TC。当然,这个结果可能是成功,也可能是失败。
  5. TC在接收到各个RM的Report后会将汇总结果上报给TM,根据汇总结果TM会向TC发出确认指令。
    • 若所有结果都是成功响应,则向TC发送Global Commit指令。
    • 只要有结果是失败响应,则向TC发送Global Rollback指令。
  6. TC在接收到指令后再次向RM发送确认指令。

注意

  • 事务消息不支持延时消息
  • 对于事务消息要做好幂等性检查,因为事务消息可能不止一次被消费(因为存在回滚后再提交的情况)

代码示例

定义事务监听器
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;

/**
 * 全局事务监听器
 */
public class ICBCTransactionListener implements TransactionListener {

    /**
     * 执行本地事务
     * @param message
     * @param o 参数
     * @return
     */
    @Override
    public LocalTransactionState executeLocalTransaction(Message message, Object o) {
        String tags = message.getTags();
        System.out.println(tags);
        switch (tags) {
            case "TAGA":
                return LocalTransactionState.COMMIT_MESSAGE;
            case "TAGB":
                return LocalTransactionState.ROLLBACK_MESSAGE;
            case "TAGC":
                return LocalTransactionState.UNKNOW;
        }
        return LocalTransactionState.UNKNOW;
    }

    //消息回查
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}
定义事务消息生产者
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 全局事务生产者
 */
public class TransactionProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        TransactionMQProducer producer = new TransactionMQProducer("transaction");
        producer.setNamesrvAddr("192.168.56.106:9876");
        /**
         * 定义一个线程池
         * @param corePoolSize 线程池中核心线程数量
         * @param maximumPoolSize 线程池中最多线程数
         * @param keepAliveTime 多余空闲线程的存活时长
         * @param unit 时间单位
         * @param workQueue 临时存放任务的队列,其参数就是队列的长度
         * @param threadFactory 线程工厂
         */
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread();
                thread.setName("client-transaction-msg-check-thread");
                return thread;
            }
        });
        //设置线程池
        producer.setExecutorService(executorService);
        //设置监听器
        producer.setTransactionListener(new ICBCTransactionListener());
        producer.start();
        String[] tags = {"TAGA","TAGB","TAGC"};
        for (int i = 0; i < 3; i++) {
            byte[] bytes = ("Hi, " + i).getBytes();
            Message message = new Message("transaction", tags[i], bytes);
            SendResult sendResult = producer.sendMessageInTransaction(message, null);
            System.out.println(sendResult);
        }
    }
}
定义消费者

直接使用普通消息的SomeConsumer作为消费者即可

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

public class TransactionConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("transaction");
        consumer.setNamesrvAddr("192.168.56.106:9876");
        consumer.subscribe("transaction","*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                for (MessageExt messageExt : list) {
                    System.out.println(new String(messageExt.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
}

批量消息

批量发送消息

发送限制

生产者可以同时发送多条消息,这可以大大提升Producer的发送效率。不过要注意以下几点:

  • 批量发送的消息必须要具有相同的Topic
  • 批量发送的消息必须具有相同的刷盘策略
  • 批量发送的消息不能是事务消息或延时消息
批量发送大小限制

默认情况下,一批发送的消息总大小不能超过4MB字节。如果想超出该值,有两种解决方案:

  • 方案一:将批量消息进行拆分,拆分为若干不大于4M的消息集合分多次批量发送
  • 方案二:在Producer端与Broker端修改属性

若要修改大小限制:

  • Producer端需要在发送之前设置Producer的maxMessageSize属性
  • Broker端需要修改其加载的配置文件中的maxMessageSize属性
生产者发送的消息大小

在这里插入图片描述

批量消费消息

在这里插入图片描述

  • 通过consumer.consumeMessageBatchMaxSize()设置并发消费的个数,最大32。不能超过这个值。
  • 通过consumer.pullBatchSize()设置一次从broker中拉取消息的个数

Consumer的pullBatchSize属性与consumeMessageBatchMaxSize属性是否设置的越大越好?

  • pullBatchSize值设置的越大,Consumer每拉取一次需要的时间就会越长,且在网络上传输出现问题的可能性就越高。若在拉取过程中若出现了问题,那么本批次所有消息都需要全部重新拉取。
  • consumeMessageBatchMaxSize值设置的越大,Consumer的消息并发消费能力越低,且这批被消费的消息具有相同的消费结果。因为consumeMessageBatchMaxSize指定的一批消息只会使用一个线程进行处理,且在处理过程中只要有一个消息处理异常,则这批消息需要全部重新再次消费处理。

代码示例

定义批量消息生产者

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class BatchMessageProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("batch");
        //设置发送最大大小
        producer.setMaxMessageSize(4 * 1024 * 1024);
        producer.setNamesrvAddr("192.168.56.106:9876");
        producer.start();
        List<Message> messagesList = new LinkedList<>();
        for (int i = 0; i < 10; i++) {
            byte[] content = ("batch-content_" + i).getBytes();
            Message message = new Message("batch_topic", content);
            messagesList.add(message);
        }
        producer.send(messagesList);
        TimeUnit.SECONDS.sleep(3);
        producer.shutdown();
    }
}

定义批量消息消费者

public class BatchMessageConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("batch");
        //设置每次的拉取数量
        consumer.setPullBatchSize(10);
        //设置并发消费数量
        consumer.setConsumeMessageBatchMaxSize(32);
        consumer.subscribe("batch_topic", "*");
        consumer.setNamesrvAddr("192.168.56.106:9876");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                for (MessageExt messageExt : list) {
                    System.out.println(new String(messageExt.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
}

消息过滤

消息者在进行消息订阅时,除了可以指定要订阅消息的Topic外,还可以对指定Topic中的消息根据指定条件进行过滤,即可以订阅比Topic更加细粒度的消息类型。

过滤的两种方式:

  • Tag过滤
  • SQL过滤

Tag过滤

通过consumer的subscribe()方法指定要订阅消息的Tag。如果订阅多个Tag的消息,Tag间使用或运算符(双竖线||)连接

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE");
consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC");

SQL过滤

SQL过滤是一种通过特定表达式对事先埋入到消息中的用户属性进行筛选过滤的方式。通过SQL过滤,可以实现对消息的复杂过滤。不过,只有使用PUSH模式的消费者才能使用SQL过滤。

但是SQL过滤并不是指的过滤message中的SQL语句,二是通过SQL语句来过滤message中的属性。

SQL过滤表达式中支持多种常量类型与运算符

支持的常量类型:

  • 数值:比如:123,3.1415
  • 字符:必须用单引号包裹起来,比如:‘abc’
  • 布尔:TRUE 或 FALSE
  • NULL:特殊的常量,表示空

支持的运算符有:

  • 数值比较:>,>=,<,<=,BETWEEN,=
  • 字符比较:=,<>,IN
  • 逻辑运算 :AND,OR,NOT
  • NULL判断:IS NULL 或者 IS NOT NULL

默认情况下Broker没有开启消息的SQL过滤功能,需要在Broker加载的配置文件中添加如下属性,以开启该功能:

enablePropertyFilter = true

在启动Broker时需要指定这个修改过的配置文件。例如对于单机Broker的启动,其修改的配置文件是conf/broker.conf,启动时使用如下命令:

sh bin/mqbroker -n localhost:9876 -c conf/broker.conf &

代码举例

定义Tag过滤Producer

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

public class FilterByTagProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("filterByTag");
        producer.setNamesrvAddr("192.168.56.106:9876");
        producer.start();
        String[] tags = {"TAGA","TAGB","TAGC"};
        for (int i = 0; i < 10; i++) {
            byte[] bytes = ("filterByTag_" + i).getBytes();
            Message message = new Message("filterByTag_topic", tags[i % tags.length], bytes);
            producer.send(message);
        }
    }
}

定义Tag过滤Consumer

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

public class FilterByTagConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("filterByTag");
        consumer.setNamesrvAddr("192.168.56.106:9876");
        //过滤tag,只消费TAGA and TAGB
        consumer.subscribe("filterByTag_topic", "TAGA || TAGB");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                for (MessageExt messageExt : list) {
                    System.out.println(messageExt.getTags());
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
}

定义SQL过滤Producer

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

public class FilterBySQLProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("filterBySQL");
        producer.setNamesrvAddr("192.168.56.106:9876");
        producer.start();
        String[] tags = {"TAGA","TAGB","TAGC"};
        for (int i = 0; i < 20; i++) {
            byte[] bytes = ("filterByTag_" + i).getBytes();
            Message message = new Message("filterBySQL_topic", tags[i % tags.length], bytes);
            message.putUserProperty("age", String.valueOf(i));
            producer.send(message);
        }
    }
}

定义SQL过滤Consumer

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.MessageSelector;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

public class FilterBySQLConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("filterBySQL");
        consumer.setNamesrvAddr("192.168.56.106:9876");
        //过滤tag,只消费年龄小于10的
        consumer.subscribe("filterBySQL_topic", MessageSelector.bySql("age < 10"));
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                for (MessageExt messageExt : list) {
                    System.out.println(messageExt.getProperty("age"));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
}

消息发送重试机制

Producer对发送失败的消息进行重新发送的机制,称为消息发送重试机制,也称为消息重投机制

  • 生产者在发送消息时,若采用同步或异步发送方式,发送失败会重试,但oneway消息发送方式发送失败是没有重试机制的
  • 只有普通消息具有发送重试机制,顺序消息是没有的
  • 消息重投机制可以保证消息尽可能发送成功、不丢失,但可能会造成消息重复。消息重复在RocketMQ中是无法避免的问题
  • 消息重复在一般情况下不会发生,当出现消息量大、网络抖动,消息重复就会成为大概率事件
  • producer主动重发、consumer负载变化(发生Rebalance,不会导致消息重复,但可能出现重复消费)也会导致重复消息
  • 消息重复无法避免,但要避免消息的重复消费。
  • 避免消息重复消费的解决方案是,为消息添加唯一标识(例如消息key),使消费者对消息进行消费判断来避免重复消费
  • 消息发送重试有三种策略可以选择:同步发送失败策略、异步发送失败策略、消息刷盘失败策略

同步发送失败策略

对于普通消息,会采用轮询算法选择所发送到的队列。如果消息发送失败后,默认重试发送2次,默认重试间隔是3秒。在重试时,采用失败隔离机制,不会将消息发送到上次失败的Broker中,而是发送到其他Broker,来增加重试的效率。

// 创建一个producer,参数为Producer Group名称
DefaultMQProducer producer = new DefaultMQProducer("pg");
// 指定nameServer地址
producer.setNamesrvAddr("rocketmqOS:9876");
// 设置同步发送失败时重试发送的次数,默认为2次
producer.setRetryTimesWhenSendFailed(3);
// 设置发送超时时限为5s,默认3s
producer.setSendMsgTimeout(5000);

如果超过重试次数,则抛出异常,由Producer去保证消息不丢。当然当生产者出现RemotingException、MQClientException和MQBrokerException时,Producer会自动重投消息。

异步发送失败策略

Producer采用异步策略发送消息时,不会采用失败隔离机制,还是会在原来发送失败的Broker上进行重试,所以该策略无法保证消息不丢失

DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("rocketmqOS:9876");
// 指定异步发送失败后不进行重试发送
producer.setRetryTimesWhenSendAsyncFailed(0);

消息刷盘失败策略

在消息刷盘超时或者slave同步失败时,消息也不会在其他Broker上进行重试(默认),但是可以通过Broker的配置文件设置retryAnotherBrokerWhenNotStoreOK属性为true来开启

消息消费重试机制

顺序消息的消费重试机制

当顺序消息消费失败时,Consumer会一直重试这条消息,直到这条消息消费成功,才会去消费其他消息。此时整个Consumer处于阻塞状态

无序消息的消费重试机制

在集群模式下,无序消息消费失败时才会进行重试,广播模式下的无序消息消费失败时,是不会进行重试的。所以我们在这里只讨论集群模式下的无序消息重试机制。但是在这里的重试消息并不会导致Consumer阻塞,Broker会将消费失败的重试消息存入重试队列%RETRY%consumerGroup@consumerGroup中(底层基于延时队列实现)

Broker对于重试消息的处理是通过延时消息实现的。先将消息保存到SCHEDULE_TOPIC_XXXX延迟队列中,延迟时间到后,会将消息投递到%RETRY%consumerGroup@consumerGroup重试队列中。

消费重试次数与间隔

对于无序消息集群消费下的重试机制,每条消息默认最多重试16次,但是每次重试的间隔时间是不同的,会逐渐增长。每次重试的间隔时间如下图:
在这里插入图片描述

当重试次数大于我们设定的重试次数或大于默认的16次仍然没有消费成功的话,此消息就会被存入死信队列

死信队列

什么是死信队列

当一条消息初次消费失败,消息队列会自动进行消费重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。这个队列就是死信队列(Dead Letter Queue,DLQ),而其中的消息则称为死信消息(Dead-Letter Message,DLM)。

死信队列是用于处理无法被正常消费的消息的

死信队列特征

  • 死信队列中的消息不会再被消费者正常消费,即DLQ对于消费者是不可见的
  • 死信存储有效期与正常消息相同,均为 3 天(
  • commitlog文件的过期时间),3 天后会被自动删除
  • 死信队列就是一个特殊的Topic,名称为%DLQ%consumerGroup@consumerGroup ,即每个消
  • 费者组都有一个死信队列
  • 如果⼀个消费者组未产生死信消息,则不会为其创建相应的死信队列

死信消息的处理

死信队列更多的是作为日志文件的功能,当我们查看死信队列,就可以及时排查出代码中的bug

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值