RocketMQ

单机运行:
运行nameserver:bin/mqnamesrv
运行broker:export NAMESRV_ADDR=localhost:9876 bin/mqbroker -n localhost:9876 启动脚本中为jvm分配8g内存,可能造成无法启动,改小一些即可

命令行发送接收消息:

export NAMESRV_ADDR=localhost:9876
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer

# 关闭
sh bin/mqshutdown broker
sh bin/mqshutdown namesrv

docker 部署

docker run -d 
	-p 10911:10911 
	-p 10909:10909 
	-v `pwd`/data/broker/logs:/root/logs 
	-v `pwd`/data/broker/store:/root/store 
	-v `pwd`/broker.conf:/home/rocketmq/rocketmq-4.5.0/conf/broker.conf 
	--name rmqbroker 
	--link rmqnamesrv:namesrv 
	-e "NAMESRV_ADDR=namesrv:9876" 
	apache/rocketmq:latest
	sh mqbroker  -c /home/rocketmq/rocketmq-4.5.0/conf/broker.conf

docker-compose

version: '2'
services:
  namesrv:
    image: apache/rocketmq:4.5.0
    container_name: rmqnamesrv
    ports:
      - 9876:9876
    volumes:
      - ./data/namesrv/logs:/home/rocketmq/logs
    command: sh mqnamesrv
  broker:
    image: apache/rocketmq:4.5.0
    container_name: rmqbroker
    ports:
      - 10909:10909
      - 10911:10911
      - 10912:10912
    volumes:
      - ./data/broker/logs:/home/rocketmq/logs
      - ./data/broker/store:/home/rocketmq/store
      - ./data/broker/conf/broker.conf:/home/rocketmq/rocketmq-4.5.0/conf/broker.conf
    command: sh mqbroker -n namesrv:9876 -c ../conf/broker.conf
    depends_on:
      - namesrv

在 conf/ 下可以看见示例配置文件,如 2m-2s-sync 就是2个节点的集群的配置文件,相互同步、不分主从。

这4个文件分别为2个节点a和b上的 master broker 和 slave broker 的配置文件,在文件第一行添加 namesrvAddr=192.168.0.1; 192.168.56.0.

bin/mqadmin 是命令行管理工具,可手动管理 topic 等、也可查看信息。

主要概念

  • message
  • Topic:一类消息的集合,Topic与消息是一对多的关系,一个生产者可同时生产多种Topic的消息,一个消费者只能订阅一种Topic
  • Tag:每个消息可携带的不限制数量的标志
  • queue:存储消息的物理实体,一个Topic可包含多个Queue。一个Topic中的Queue也被成为一个Topic的分区,类似 Kafka 中的 Partition
    在一个消费者组中一个Queue只能由一个消费者消费,但组中一个消费者可能消费多个Queue
  • 消息标识
    • messageId:生产者生产,producerIP+pid+MessageClientIDSetter类的classloader的hashcode+当前时间+计数器
    • offsetMessageId:Broker生产,brokerIP+物理分区offset
    • key:用户手动指定,业务相关

分片与分区不同(不重要):

  • 分区:一个Topic有多个不同的Queue
  • 分片:一个Queue会在多个Broker上有副本,一个Broker上某个Topic下的Queue集合就是这个Topic在Broker上的分片

  • 生产者组:生产同一类topic消息的生产者的集合
  • 消费者组:RocketMQ中消费者都是以消费者组的形式出现,一个组订阅同一个Topic的消息,且只能订阅一个,消费者组使得负载均衡容错很容易
    一个消费者组中消费者数大于订阅Topic的queue数量时多余的消费者将接收不到消息,当小于时一个消费者会消费多个queue
    一个topic可以被多个消费者组同时消费
    • 负载均衡:一个Topic中的queue平均分给不同消费者组的消费者,注意不是对消息负载均衡
    • 容错:当一个消费者离线后,同一个消费者组中的消费者会沿着offset继续消费

NameServer

NameServer 是无状态的,相互间不通信(与 zk nacos 等不同)broker启动时轮询所有的nameserver,与其建立长连接进行注册。NameServer内部维护broker列表,动态存储broker信息。为证明自己存活,broker每30秒向nameserver发送一个心跳包,维持长连接即时更新信息。

NameServer无状态的优点:集群搭建简单。缺点:扩容时broker必须重启才能向新的nameserver注册。

  • 路由剔除:没收到broker心跳包,nameserver将broker从其列表中删除。NameServer内有定时任务,定时检查broker是否过时。
  • 路由发现:采用Pull模型,Topic发生变化时namesrv不会主动推送,而是由客户端定时主动拉去路由。默认30秒拉去一次。

push:建立长连接,mq收到消息后主动推给消费者,实时性高;但加大了mq的负担,无法评估消费者的能力,适合client数量不多,mq数据变化较频繁
pull:消费者通过循环从mq拉取数据,主动权在client能控制消费者的资源利用率;循环时间间隔太短会浪费资源,太长则不能即使处理
long pulling:长轮询模型,是对push和pull模型的整合

rocketmq的消费者选用了”长连接“,主动权在消费者手中,不会出现压力过大的情况;局限是HOLD住请求时也要占用资源。使用客户端数量可控的情况。

client选择broker的策略:先随机选择一个,若连接失败则轮询。

Topic 的创建:

  • 集群模式:集群中所有Broker的Queue数量相同
  • Broker模式:每个Broker的Queue数量可以不同

自动创建时采用Broker模式,每个Broker默认创建4个Queue,可由配置文件设置

读/写队列

物理上的同一个队列,在逻辑上分为读队列和写队列。一般读队列和写队列数量相同。创建Topoic时可以指定读写队列的数量。

如果读队列大于写队列,则有消费者读不到数据;写队列多于读对列,有些生产者生产的数据堆在MQ不会被消费。这个设计是为了方便Topic对应的Queue进行缩容:现在某个Topic对应16个Queue,先将写队列缩容到8个,等后8个队列内容消费完后,再将读队列缩容到8个。

Broker

转发存储消息,同时也存储消息元数据,如消费者组offset、主题、队列等。

HA

为保证高可用,每个broker节点都有一个备份节点。这个主备集群有一个master和多个slave,它们有相同的BrokerName不同的BrokerID,BrokerID为0是master非零是slave。它们都与nameserver建立长连接。

工作流程

  1. 启动nameserver,监听端口等待broker、producer、consumer连接
  2. 启动broker,与每个nameserver建立长连接,每30秒发送一个心跳包
  3. Topic可在发送消息前创建、或发送时自动创建。创建时会将Topic和Broker的关系写入NameServer
  4. producer发消息时先和一台nameserver建立长连接,从中获取目标目标Topic的所有queue与Broker的地址映射,根据算法策略选择一个Queue,与其所在broker建立连接并发消息。从nameserver获取路由消息后缓存在本地,每30秒更新一次。
  5. consumer与producer类似,但是它还会向namesrv发送心跳,保证存活状态

集群数据复制与刷盘策略

  • 同步复制:消息写入master后,等slave成功同步数据才向生产者返回ACK
  • 异步复制:消息写入master后,立即返回ACK给生产者,无需等slave复制成功

刷盘策略

  • 同步刷盘:消息持久化到broker的磁盘后才算消息成功写入
  • 异步刷盘:消息写入broker内存后即表示写入成功,无需等待消息持久化到磁盘
    消息写入PageCache,不会立即落盘,而是等PageCache到达一定量时自动落盘

Broker集群模式:

  • 单master
  • 多master
    broker集群又多个master构成,无slave。同一Topic的各个Queue平均 分布在各个master节点上
    • 优点:配置简单,单Master宕机或重启也不影响应用,磁盘配置为RAID10时可靠和性能都有保障(异步刷盘丢失少量,同步刷盘不会丢失)
      (以上优点的前提是配置了RAID)
    • 缺点:单台机器宕机期间,未被消费的消息在机器恢复前不可被消费,消息实时性受影响
  • 多master多slave -- 异步复制
    broker集群由多个master构成(配置RAID后一般配置一个slave即可),每个master配置了多个slave,主备关系。采用异步复制效率更高,但是slave从master同步数据时有毫秒级延迟,所以当master宕机时可能丢失少量数据。
  • 多master多slave -- 同步双写(目前版本的master宕机后slave不会自动切换)
    消息写入master后等master与slave同步数据成功后才向生产者返回ACK。安全性更高,不存在消息丢失,但单个消息的RT

集群最佳实践:

使用一个master一个slave,配置RAID10磁盘阵列。即利用了磁盘阵列的高效、安全,又解决了影响订阅的问题

RAID磁盘阵列效率高于master-slave集群,因为RAID是硬件支持但搭建成本更高

多master+RAID,多master+多slave集群 区别:

多master+RAID:仅保证数据不丢失,即不影响写入,但可能影响消息订阅。效率远高于多master多slave集群
多master+多slave集群:保证了数据的不丢失,且不影响写入。但效率更低


RocketMQ 工作原理

消息生产

生产过程:

  1. 发消息前先向NameServer获取消息Topic的路由信息
  2. NameServer返回该Topic的路由表Broker列表
  3. Producer根据指定的Queue选定策略,从Queue列表选一个队列用于后续存储
  4. Producer对消息做特殊处理,如压缩超过4M的消息
  5. Producer向选定Queue的Broker发出RPC请求,将消息发到Queue

路由表:是一个Map,key为Topic名称,value是QueueData实例,是相关BrokerName列表

Broker列表:也是Map,key为brokerName,value为BrokerData。一个BrokerData实例对应一套名称相同的 Master-Slave 小集群
                    BrokerData中包含brokerName与一个map,key为brokerId,value为broker对应的地址

Queue选择算法:

对于无序消息,常用:

  • 轮询算法:可能某些Broker上Queue投递延迟严重,从而导致Producer接收不到上一个消息的ack进而无法向下一个Queue投递,导致生产者缓存队列出现消息积压,影响消息投递
  • 最小投递延迟算法:可能出现负载不均衡、消息在MQ堆积

消息存储

消息默认存储在当前用户家目录的store目录下

  • abort:Broker启动后自动创建,正常关闭Broker这个文件会消失。没启动Broker时发现此文件也存在,说明Broker非正常关闭
  • checkpoint 存储的是commitlog、consumequeue、index文件最后刷盘时间
  • commitlog:消息写在commitlog文件
  • config:存放Broker运行时的配置数据
  • consumequeue:队列存放在这个目录
  • index:存放着消息索引 indexFile
  • lock:运行期间使用的全局资源锁

commitlog文件

很多资料称为 commitlog 文件,源码中称为 mappedFile 文件。文件尺寸小于等于1G,文件名由20位十进制数构成,表示当前文件第一条消息起始位置偏移量。一个Broker仅有一个 commitlog 目录,所有mappedFile 都在这里。

多个 mappedFile 文件在物理上不连续,在逻辑上连续offset是连续的偏移量
第一个文件名一定是20位0构成,因为第一个commitlog offset为0,假设文件大小为1073741820字节(1G = 1073741824字节)
第一个文件放满时自动生成第二个继续存放,文件名为00000000001073741824
第n个文件名应该是前 n-1 个文件大小之和

mappedFile 文件顺序读写,访问效率高,对SSD和SATA都是。

消息单元:

文件内容是一个个的消息单元:

  • 消息总长度 MsgLen
  • 消息物理位置 physicalOffset
  • 消息体内容 Body
  • 消息体长度 BodyLength
  • 消息主题 Topic
  • Topic 长度
  • TopicLength
  • 消息生产者 BornHost
  • 消息发送时间戳 BornTimestamp
  • 消息所在队列QueueId
  • 消息在Queue中存储的偏移量QueueOffset

comsumequeue

将消息顺序存储在 mappedFile 文件中,在 ~/store/consumequeue/TopicName/queueId/consumequeue索引文件 可定位到具体的消息。文件名称也是20位数字构成,表示当前文件第一条索引起始偏移位置,因为索引大小和文件大小都是固定的,所以不同文件下consumequeue文件名都是相同的。

索引条目:

每个consumequeue文件可包含30w个索引条目,每个包含3个属性,占20字节,每个文件固定 30w*20字节

文件读写

消息写入:

在消息被Broker接收后

  • Broker根据queueId获取该消息对应索引条目要在consumequeue目录中的写入偏移量,即QueueOffset
  • 将 queueId、queueOffset 等数据与消息一起封装为消息单元
  • 将消息单元写入 commitlog
  • 同时形成消息索引条目
  • 将消息索引条目分发到相应的 consumequeue

消息拉取:

  • Consumer获取要消费消息所在Queue的消费偏移量offset,计算出其要消费消息的消息offset
    消费offset即消费进度,即消费到某个Queue的第几条消息
    消息offset = 消费offset + 1
  • Consumer向Broker发送拉取请求,其中会包含要拉取消息的Queue、消息offset及消息Tag
  • Broker计算在该consumequeue中queueOffset
    queueOffset = 消息offset * 20字节
  • 从该queueOffset处开始向后查找第一个指定Tag的索引条目
  • 解析该索引条目前8个字节,定位到该消息在commitlog中commitlog offset
  • 从对应位置读取消息单元,并发送给Consumer

性能提升

使用磁盘存储消息如何还能保存高性能?

  1. mmap零拷贝
  2. 磁盘顺序读写(速度接近内存,因为系统使用PageCache机制优化顺序读取)
  3. PageCache预读取机制

对RocketMQ性能影响最大的是commitlog文件的随机读取。但如果使用的系统IO调度算法合适,随机读的性能也会提升。

与 Kafka 对比

RocketMQ来源于Kafka,commitlog目录与consumequeue目录类似Kafka中partition分区目录。mappedFile文件类似Kafka中segment段。

  • Kafka中Topic被分为多个partition,它是物理概念对应系统上的topic目录下一个或多个目录。每个partition包含的文件称为segment,是具体存放消息的文件。
  • kafka中消息存放目录结构:topic目录/partition目录/segment文件
  • kafka中没有二级分类标签Tag
  • kafka无需索引文件,因为生产者将消息直接写在partition中,消费者直接从partition中读取

indexFile

提供根据消费者提供的key进行查询的功能,通过 ~/store/index/indexFile 进行索引实现快速查询。这个indexFile中索引数据是Broker接收到含有key的消息时写入的

索引条目结构

每个Broker包含一组indexFile,每个都以创建时间戳命名,由三个部分构成:indexHeader、slots槽位、indexes索引数据。每个indexFile文件包含500w个slot槽。每个slot槽挂载很多index索引单元。

indexHeader固定40字节,存放如下数据:

存放indexFile第一条消息、最后一条消息的存储时间和在commitlog中偏移量  commitlog offset,已经填充有index的slot数量,该indexFile包含的索引单元数量之和

实际存储时Indexs在Slots后面,但是逻辑上存在关系:

 key的hash对500w取余得到slot槽位,将该slot值修改为该index索引单元的indexNo,根据indexNo计算出该index单元在indexFile中位置。这样重复率高,为此每个index索引单元中增加preIndexNo指向前一个index索引单元。slot槽中始终指向最新的indexNo。indexNo是一个indexFile中的流水号,从0开始递增。

index 索引单元默认20字节:

  • key的hash
  • 对应消息在commitlog中偏移量 commitlog offset
  • 对应消息的存储时间与当前indexFile创建时间的时间差
  • 下一个index索引单元的indexNo(没有当前索引的indexNo字段,因为索引单元大小固定,直接数个数即可)

文件名的作用

使用索引文件创建时间戳,用于简化查询时的时间条件

创建时机:

  • 第一条带key消息发送来时
  • 当一个indexFile中挂载的index索引单元超过2000w个时,创建新的indexFile文件,通过indexHeader的最后4字节判断。
    可推算出,一个indexFile最大位(40 + 500w*4 + 2000w*20)字节

查询流程

# 计算指定消息key的slot槽号
slot槽序号 = hash(key) % 500w

# 计算槽位序号为n的slot在indexFile中起始位置
slot(n) = 40 + (n-1)*4                    # 40 为 indexHeader 字节数,4 为indexNo 长度

# 计算indexNo为m的index在indexFile中位置
index(m) = 40 + 500w*4 + (m-1)*20         # 20 为 index 长度

查询流程:

 

消息的消费

获取消息方式:

  • pull:Consumer主动从Broker拉取,主动权在Consumer,但实时性较弱
    用户自己实现Queue的拉取,指定拉取的时间间隔,注意平衡实时性和性能
  • push:实时性较高,但不能很好的考虑到消费者的负载情况。如典型的发布订阅模式,一旦发现新消息就触发回调函数,这需要消耗资源维持长连接

消费者组对消息消费的模式:

  • 集群消费Clustering:一个 ConsumerGroup 中每个实例平均分摊同一个Topic的消息,即一条消息被发给某一个消费者
    消费记录是所有消费者共享的,所以由Broker保存
  • 广播消费Broadcasting:一个ConsumerGroup中每个实例都接收同一个Topic的全量消息,即一条消息被发给所有消费者
    每个消费者消费进度不同,各自保存自己的消费记录

Rebalance 机制(集群消费)

将一个Topic下多个Queue在同一个 Consumer Group 中的多个Consumer间重新分配的过程。
例如:4个Queue分配给一个Consumer,现在增加一个Consumer后Queue均匀分配给两个Consumer。

Relalance本是为了提升并行消费能力,但是当某个消费者组实例数大于队列数量时多余的消费者实例将分配不到任何队列。

危害:

  • 消费暂停:当Consumer从1个增加到多个时,原Consumer要暂停部分队列的消费,等重新分配完成后暂停的队列才重新消费
  • 消费重复:新消费者消费队列的offset应当接着旧消费者的记录,但默认offset是异步提交的,可能存在差值导致重复消费消息
  • 消费突刺:如果Rebalance导致要重复消费的消息很多,或消费暂停时积压了部分消息,可能导致Rebalance结束时要消费很多消息

同步提交:consumer消费消息后发送offset给broker,阻塞等待broker返回ACK,收到ACK后才继续消费下一批消息

异步提交:不等收到返回的ACK就继续消费

对一次性读取消息的数量,要根据具体业务选择一个适当的值。数量过大,系统性能提升,重复消费的消息数也增加;数量国小,系统性能下降,被重复消费的消息数也减少

产生原因:消费者订阅的Queue数量变化,或消费者组中消费者数量发生变化

Queue 分配算法

Broker维护多个Map存储Topic、Queue、ConsumerGroup、Consumer的信息。一旦发现消费者订阅的Queue数量变化、消费者组中数量发生变化,立即向ConsumerGroup中每个实例发出Rebalance通知。

Consumer收到Rebalance通知后自行选择Queue分配算法自主进行Rebalance,不存在Group Leader

Kafka触发Rebalance条件后,调用Group Coordinator完成Rebalance

Coordinator时Broker中一个进程,在Consumer Group 中选出一个Group Leader。Group Leader 根据所在组情况完成Partition分区的再分配。将结果上报给Coordinator并同步给Group中所有Consumer实例。

  • 平均分配策略:按照 avg = QueueCount/ConsumerCount 进行平均分配,多余的顺序分配
  • 环形分配策略:根据消费者顺序依次在由queue队列组成的环形图中逐个分配,不同管个数,直接一个一个分配即可
  • 同机房策略:根据queue部署机房位置和consumer位置,过滤当前consumer相同机房的queue,然后按其他算法对同机房queue进行分配。没有同机房queue则直接分配
  • 一致性hash策略:可能分配不均

一致性hash缺点:两种平均分配效率高,一致性hash分配结果可能存在很大不平均

一致性hash优点:有效减少消费者组扩容或缩容带来的Rebalance

至少消费一次:每条消息必须被成功消费一次,Consumer在消费完成后向消费记录器提交消费消息的offset,成功记录后就算被成功消费。
对于广播消费模式,Consumer本身就是消费记录器
对于集群消费模式,Broker就是消费记录器

订阅关系的一致性

同一消费者组(Group ID相同)下所有Consumer实例订阅的TopicTag对消息的处理逻辑必须完全一致,否则消息消费的逻辑就会混乱导致消息丢失。

offset管理

Consumer的消费进度offset,记录每个Queue的不同消费组的消费进度。分为:本地模式、远程模式

  • offset 本地管理模式
    消费模式为广播消息时,offset用本地模式存储。offset及相关数据以json形式持久化到Consumer本地磁盘中,默认 ~/.rocketmq_offsets/${clientId}/${groupName}/Offset.json ,其中 clientId 默认为 ip@DEFAULT
  • offset 远程管理模式
    消费模式为集群模式时,offset使用远程模式管理。offset与相关数据以json格式持久化到 ~/store/config/consumerOffset.json。Broker启动时加载此文件,写入一个双层map,外层map的key为topic@group,value为内层map。内层map的key为queueId,value为offset。当Rebalance时,新的Consumer从该Map中获取相应数据继续消费
    主要为了保证缩容时的Rebalance

offset 用途

消费者消费的第一条消息由用户通过 consumer.setConsumeFromWhere() 指定,有三种选择:

  1. CONOSUME_FROM_LAST_OFFSET
  2. CONOSUME_FROM_FIRST_OFFSET
  3. CONOSUME_FROM_TIMESTAMP

Consumer在消费完一批信息后提交消费进度offset给Broker,Broker接收消费进度后更新到双层Map(ConsumerOffsetManager)与consumerOffset.json中,然后向Consumer进行ACK,ACK包含:当前queue最小offset,最大offset,下层消费起始offset

重试队列

rocketMQ对消息消费异常时,将异常消息的offset提交到Broker的重试队列中,系统在发生消息消费异常时为当前 Topic@group 创建一个重试队列,其以%RETRY%开头,到达重试时间后进行消费重试。

offset 同步与异步提交

集群消费模式下,消费完信息后向Broker提供消费进度offset,提交方式:

  • 同步提交:消费消息后向broker提交消息offset,阻塞等待响应。从ACK中获取nextBeginOffset进行下一批消息的消费。没有收到响应则重新提交
  • 异步提交:消费消息并发送消息offset后无需等待broker响应,继续读取并消费下一批消息。如果没有收到ACK,则Consumer的请求发送到Broker后再从中获取nextBeginOffset

消费幂等

重复消费的结果与消费一次的结果是相同的,且无负面影响。在网络情况不稳定时消息很可能重复发送或重复消费。

消息重复场景

  • 发送时重复:当一条消息在Broker持久化后,此时网络断开使得应答失败。生成者意识到发送失败并再次发送消息,此时Broker中可能出现两条内容和ID相同的消息,后续Consumer会消费消息两次
  • 消费时重复:消费者完成消费后的确认消息丢失,Broker为保证消息至少被消费一次,在网络恢复后再次投递,此时消费者再次消费内容id都相同的消息
  • Rebalabce时重复

通用解决方案

幂等解决方案设计涉及两个要素:

  • 幂等令牌:生产者和消费者间的既定协议,常指唯一业务标识字符串。如:订单号、流水号等,一般由生产者随着消息发送
  • 唯一性处理:服务端通过一定算法保证同一个业务逻辑不会被重复执行。如:对一笔订单的多次支付只会成功一次

常见解决方案:

  1. 通过缓存去重,若缓存中存在幂等令牌则说明操作是重复的,没命中则进入下一步
  2. 在唯一性处理前,先在数据库查询幂等令牌所谓索引的数据是否存在,存在则说明本次操作为重复性操作,不存在则进入下一步
  3. 唯一性处理后,将幂等令牌写入缓存,并将幂等令牌作为唯一索引的数据写入DB,以上操作在一个事务中完成

第1步的查询是在缓存中进行,缓存中数据具有有效期,超时后缓存穿透到DBMS中。所以第2步操作不是多余的。

解决方案举例

支付场景:

  1. 支付请求到达后先在Redis中获取key为支付流水号的缓存value。value为不为空,本次操作重复,返回重复支付标识;value不为空,进入下一步
  2. 在DB中根据支付流水号查询是否存在,若存在则是重复操作,业务系统返回调用侧重复支付标识;若不存在,则说明本次操作是首次操作,进入支付处理
  3. 在分布式事务中完成:
    1. 完成支付
    2. 将支付流水号写入redis缓存,通过 set(key, value, expireTime)
    3. 将当前支付流水号作为主键,和相关数据一起写入DBMS

消费幂等实现

为消息指定不会重复的唯一标识。MessageID可能出现重复,故不建议使用MessageID作为处理依据,而是使用业务唯一标识作为幂等处理的关键依据,可通过消息Key设置。

以支付为例,可将消息Key设置为订单号,作为幂等处理依据:

Message message = new Message();
message.setKey("订单流水号");
SendResult sendResult = producer.send(message);

消费者根据消息的Key,即订单号实现消费幂等

consumer.registerMessageListener(new MessageListenerconcurrently(){
	@Override
	public Consumeconcurrentlystatus consumeMessage(List<MessageExt> msgs, Consumeconcurrent1yContext context){
		for(MessageExt msg:msgs){
			string key msg.getkeys();
			//根据业务唯一标识Key做幂等处理
			//…
		}
		return ConsumeconcurrentlyStatus.CONSUME_SUCCESS;
	}
});

RocketMQ 能保证消息不丢失,但不能保证消息不重复。MQ消息重复的概率很低,如果由其完成去重操作代价太大;所以由业务层自行解决。

消息堆积与消费延迟

消费速度小于生产速度,MQ中会越来越多的堆积消息,这导致消费延迟

  • 业务系统上下游能力不匹配造成持续堆积,无法自行恢复
  • 业务系统对消息的消费实时性要求较高,即使短暂堆积造成的消息延迟也无法接受

产生原因分析

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

  • 拉取消息:通过长轮询Pull就安静拉取的消息缓存到本地缓冲队列,内网下拉取的吞吐量很高,这一阶段不会成为消息堆积的瓶颈
  • 消费消息:业务消费逻辑对消息进行处理,处理后得到结果。此时消费能力完全依赖于消息的消费耗时消费并发度。若业务复杂导致单条消息耗时较长,则整体吞吐量不高,本地缓冲达到上限后则停止拉取消息
    消费耗时的权重高于消费并发度

一个单线程单分区低规格主机(Consumer,4C8G)可达到几万TPS。若是多个分区多个线程,则可轻松达到几十万TPS。所以拉取消息一般不是瓶颈。

 

  • 消费耗时
    主要是代码逻辑:内部CPU计算代码、外部IO操作代码。通常没有复杂递归和循环内部计算耗时相对外部IO可以忽略。主要考虑DB性能瓶颈、下游服务RPC故障
  • 消费并发度
    通常优先调节单节点的线程数,单机硬件资源达上限后通过横向扩展提高消费并发度
    对于普通消息、延时消息、事务消息,并发度由 单节点线程数 * 节点数 决定的
    对于顺序消息,消费并发度等于 Topic 的 Queue 分区数量
    • 全局顺序消息:该类型消息的Topic只有一个Queue分区,保证该Topic所有消息被顺序消费。为保证全局顺序性,ConsumerGroup在一个时刻只能有一个Consumer线程进行消费,并发度为1
    • 分区顺序消息:该类型消息的Topic对应多个Queue分区,保证每个Queue分区中消息被顺序消费。每个时刻ConsumerGroup中只能有一个Consumer的一个线程消费一个Queue分区,并发度为Topic的分区数量

理想环境下节点最优线程数为:CPU内核数 * (1 + IO耗时 / CPU耗时) 现实环境中应该从小到大慢慢尝试

避免

为避免消息堆积和消费延迟,要

  • 梳理消息的消费耗时
    通过压测获得消费耗时,分析代码
    • 计算复杂度,是否有无限循环和递归等
    • IO能否优化,能否用本地缓存优化
    • 是否可异步化
  • 设置消息消费的并发度
    • 逐步调大单个Consumer节点线程数
    • 根据上下游链路的流量峰值计算要设置的节点数
      节点数 = 流量峰值 / 单节点消息吞吐量

消息的清理

被消费的消息不会立即被清理。消息顺序存在commitlog中,且消息大小不定。清理以commitlog文件为单位进行清理。

commitlog文件存在一个过期时间,默认72小时。除用户手动清理,以下情况也会自动清理:

  • 文件过期,且到达清理时间
  • 文件过期,且磁盘使用率达到过期警戒线(默认75%),不考虑是否到指定清理时间
  • 磁盘占用率达清理警戒线(默认85%)按规则清理文件,无论是否过期,默认从最老文件开始清理
  • 磁盘占用率达危险警戒线(默认90%)Broker将拒绝消息写入

RocketMQ 应用

 

普通消息

  1. 同步发送:生产者发出消息后收到MQ返回的ACK才接着发送下一条消息,可靠性高,效率低
    默认3秒内重试,最多2次。投递完成不代表成功,通过SendResult.sendStatus 判断:成功、刷盘超时、从节点同步超时等。默认只有 SendStatus == SEND_OK 才算成功投递
  2. 异步发送:生产者发送消息后无需等待ACK,直接发下一条消息,异步接受ACK,可靠性和效率都可以
    线程过早退出会导致回调函数来不及执行,不会重试,失败调用 onException 方法。一般用于链路耗时较长,对RT响应时间较敏感业务场景,例如视频上传后通知启动转码,转码完成后通知推送转码。
  3. 单向发送:仅发送消息,不处理MQ的ACK,也不关心是否发送成功
生产者
public class SyncProducer {
	public static void main(String[]args)throws Exception {
		DefaultMQProducer producer new DefaultMQProducer("ProducerGroup");
		producer.setNamesrvAddr("NameServerAddress:9876");
		// 可以设置 超时时间、重试次数
		producer.setXXX();
		producer.start();

		for (int i 0;i<10;i++){
			byte[] body = ("Hi,"+i).getBytes();
			Message msg = new Message("someTopic","someTag",body);
			msg.setKeys("xx"); // 指定key
			SendResult sendResult = producer.send(msg);
			System.out.println(sendResult);
		}
		producer.shutdown();
	}
}

public class AsyncProducer {
	public static void main(String[]args)throws Exception {
		DefaultMQProducer producer new DefaultMQProducer("ProducerGroup");
		producer.setNamesrvAddr("NameServerAddress:9876");
		//指定异步发送失败后不进行重试发送
		producer.setRetryTimesWhenSendAsyncFailed(0);
		//指定新创建Topic的Queue数量为2,默认为4
		producer.setDefaultTopicQueueNums(2);
		producer.start();

		for (int i 0;i<10;i++){
			byte[] body = ("Hi,"+i).getBytes();
			try {
				Message msg new Message("myTopicA","myTag",body);
				producer.send(msg,new SendCallback(){
					@Override
					public void onSuccess(SendResultsendResult) {
						System.out.println(sendResult);
					}
					@Override
					public void onException(Throwable e) {
						e.printstackTrace();
					}
				});
			} catch (Exception e){
				e.printstackTrace();
				System.out.println(sendResult);
			}
		}
		// 因为是异步发送,这里如果不sleep,则消息还未发送就会将producer关闭,会导致报错
		TimeUnit.SECONDS.sleep(3);
		producer.shutdown();
	}
}

// 单向发送使用 sendOneway 即可

消费者

消费者
public class SomeConsumer {
	public static void main(String[]args)throws MQclientException{
		DefaultMQPushConsumer consumer new DefaultMQPushConsumer("cg");
		consumer.setNamesrvAddr("rocketmqos:9876");
		consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
		consumer.subscribe("someTopic","*")
		consumer.setMessageModel(MessageModel.BROADCASTING); // 默认 MessageModel.CLUSTERING
		consumer.registerMessageListener(new MessageListenerConcurrently(){
			// 一旦broker中订阅的topic有消息就触发该方法执行
			@Override
			public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt>msgs, ConsumeConcurrentlyContext context){
				for (MessageExt msg : msgs){
					System.out.println(msg);
				}
				return ConsumeConcurrentlyStatus.CONSUMESUCCESS;
			}
		});
		consumer.start();
		System.out.println("Consumer Started");
	}
}

顺序消息(并发不高)

严格按照发送顺序进行消费的消息,消息仅发送到同一个Queue中,消费时也只能从这个Queue上拉取消息,这就严格保证了消息的顺序性。

例如对于Topic ORDER_STATUS 包含4个不同Queue队列,订单状态存在:未支付、已支付、发货中、发货成功、发货失败。根据以上订单状态,生产者从时序上可以生成以下消息:订单T0000001:未支付->订单T0000001:已支付->订单T0000001:发货中->订单T0000001:发货失败 消息可能发送到不同Queue中,消费的顺序也是不确定的。

有序性分类:

  • 全局有序:参与发送和消费的queue只有一个

    producer.setDefaultTopicQueueNums(1); // 通过指定自动创建时的queue数量实现
    mqadmin 命令 // 或者在手动创建Topic并指定queue数量
  • 分区有序:有多个Queue参与,仅保证在该Queue分区队列上消息有序

    在定义Producer时指定消息队列选择器,实现 MessageQueueSelector 接口定义。一般使用MessageKey用于选择Queue,也可选其他唯一key
    Queue选择
    Message msg new Message("TopicA","TagA",body);
    SendResult sendResult = producer.send(msg,new MessageQueueSelector(){
    	// 具体的选择算法在该方法中定
    	@Override
    	public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg){
            // 1. 传入一个id
            // Integer id = (Integer)arg;
            
            // 2. 获取消息key作为选择算法
            String keys = msg.getKeys();
            Integer id = Integer.valueOf(keys);
            
    		int index = id % mqs.size();
    		return mqs.get(index);
    	}
    }, orderId); // 这个orderId作为回调函数的第3个参数传入

延时消息

消息写入Broker后在指定时间后才被消费的消息。采用延时消息可实现定时任务功能,而无需定时器。如电商中关闭超时未支付的订单,12306取消未支付订票。

电商平台中,订单创建时发送一条延迟消息,在30分钟后投递给后台业务系统(Consumer),后台系统收到消息后判断订单是否完成支付。未完成则取消订单,商品放回库存;若完成支付,则忽略此消息。

注意:rocketmq中 DelayLevel 从1开始计数,使用序号指定延时等级

RocketMQ不支持任意时长的延迟,而是通过 org.apache.rocketmq.store.config.MessageStoreConfig 下的 private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"; 进行指定延时等级,在发送消息前指定 msg.setDelayTimeLevel(1);。如需要自定义延时等级,可通过在broker加载的 conf/ 配置中新增 messageDelayLevel 配置。

存储

Producer将消息发送到Broker后,Broker会首先将消息写入到commitlog文件,然后需要将其分发到相应的 consumequeue。不过,在分发之前,系统会先判断消息中是否带有延时等级。若没有,则直接正常分发;若有则需要经历一个复杂的过程:

  1. 修改消息的Topic为SCHEDULE_TOPIC_XXXX
  2. 根据延时等级,在consumequeue目录中SCHEDULE TOPIC_XXXX主题下创建出相应的queueld目录与consumequeue文件(如果没有这些目录与文件的话)
    queueId = 延时等级delayLevel - 1 按需创建
  3. 修改消息索引单元内容。索引单元中的Message Tag HashCode部分原本存放的是消息的Tag的Hash值。现修改为消息的投递时间投递时间是指该消息被重新修改为原Topc后再次被写入 commitlog 中的时间投递时间 = 消息存储时间 + 延时等级时间。消息存储时间指的是消息被发送到Broker时的时间戳
  4. 将消息索引写入 SCHEDULE_TOPIC_XXXX 主题下相应的 consumequeue 中
  5. 定时消费消息
  6. 再次投递到commitlog中,并再次形成新的消息索引条目,分发到相应Queue(由定时器类实现的普通发送)

SCHEDULE_TOPIC_XXXX 目录中各个延时等级Queue中消息按照消息投递时间排序。同一等级的所有延时消息被写入到consumequeue目录中 SCHEDULE_TOPIC_XXXX 目录下同一Queue中。即一个Queue 中消息延时投递时间的延时等级时相同的。

投递延时消息

Broker内部有一个延迟消息服务类ScheuleMessageService,其会消费SCHEDULE_TOPIC_XXXX中的消息,即按照每条消息的投递时间,将延时消息投递到目标Topic中。不过,在投递之前会从commitlog中将原来写入的消息再次读出,并将其原来的延时等级设置为0,即原消息变为了一条不延迟的普通消息。然后再次将消息投递到目标Topict中。

ScheuleMessageService在Broker启动时,会创建并启动个定时器Tmer,用于执行相应的定时任务。系统会根据延时等级的个数,定义相应数量的TimerTask,每个TimerTask负责一个延迟等级消息的消费与投递。每个TimerTaski部会检测相应Queue队列的第一条消息是否到期。若第一条消息未到期,则后面的所有消息更不会到期(消息是按照投递时间排序的),若第一条消息到期了,则将该消息投递到目标Topic,即消费该消息。

事务消息

例如跨银行转账需求

  1. 工行发送一个给B增款1万元的同步消息M,发送给Broker
  2. 消息被Broker成功接收后,向工行发送成功ACK
  3. 工行系统收到成功ACK后从用户A中扣款1万元
  4. 建行系统从Broker中获取消息M
  5. 建行系统消费消息M,向用户B增加1万元

若第3步扣款操作失败,但消息已发送到Broker。消息成功写入MQ后就可被消费,即即使扣款失败建行系统也会给用户B增加1万元。出现数据不一致。

解决思路:

让步骤1 2 3 具有原子性,即消息发送成功后必须保证扣款成功;若扣款失败则回滚发送成功的消息。该思路就是使用事务消息。

  1. 事务管理器TM向事务协调器T℃发起指令,开启全局事务
  2. 工行系统发一个给B增款1万元的事务消息M给TC
  3. TC会向Broker发送半事务消息prepareHalf,将消息M预提交到Broker。此时的建行系统是看到Brokert中的消息M的
  4. Broker会将预提交执行结果Report给TC
  5. 如果预提交失败,则TC会向TM上报预提交失败的响应,全局事务结束;如果预提交成功,TC会调用工行系统的回调操作,去完成工行用户A的预扣款1万元的操作
  6. 工行系统会向TC发送预扣款执行结果,即本地事务的执行状态
  7. TC收到预扣款执行结果后,会将结果上报给TM  
  8. TM会根据上报结果向TC发出不同的确认指令
    1. 若预扣款成功(本地事务状态为COMMIT MESSAGE),则TM向TC发送Global Commit指令
    2. 若预扣款失败(本地事务状态为ROLLBACK MESSAGE),则TM向TC发送Global Rollback指令
    3. 若现未知状态(本地事务状态为UNKNOW),则会触发工行系统的本地事务状态回查操作。回查操作会将回查结果,即COMMIT_MESSAGE或ROLLBACK MESSAGE Report给TC。TC将结果上报给TM,TM会再向TC发送最终确认指令Global Commit或Global Rollback
  9. TC在接收到指令后会向Broker与工行系统发出确认指令
    1. TC接收的若是Global Commit指令,则向Broker与工行系统发送Branch Commit指令。此时Brokert中的消息M才可被建行系统看到;此时的工行用户A中的扣款操作才真正被确认
    2. TC接收到的若是Global Rollback指令,则向Broker与工行系统发送Branch Rollback指令。此时Broker中消息M将被撤销;工行用户A中的扣款操作将被回滚

以上方案为了确保消息投递和扣款操作在一个事务中实现

以上方案不是一个典型的XA模式。因为XA模式中分支事务是异步并行的,而事务消息方案中消息预提交扣款操作是同步的。

消息回查

引发消息回查的原因一般有两个:

  1. 回调操作返回 UNKNWON
  2. TC 没有接收到 TM 的最终全局事务确认指令

消息回查设置:

transactionTimeout = 20 # 当TM没在20秒内将最终状态发送给TC,触发回查
transactionCheckMax = 5 # 最大回查5次,超过后丢弃消息并记录错误日志
transactionCheckInterval = 10 # 消息回查的时间间隔未 10 秒

分布式事务 XA 模式

XA 是一种分布式事务解决方案,一种分布式事务处理模式。作为资源管理器和事务管理器的接口标准。

  1. TC(Transaction Coordinator):事务协调者,维护全局和分支事务状态,驱动全局事务提交或回滚
    对应rocketmq中的Broker
  2. TM(Transaction Manager):事务管理器,定义全局事务范围:开始全局事务、提交或回滚全局事务。是全局事务的发起者
    对应事务消息的生产者
  3. RM(Resource Manager):资源管理器,管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚
    事务消息的Producer和Broker都是 RM

XA 模式是一个典型的 2PC,执行过程如下:

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

事务消息的注意点

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

事务消息的接受者就是普通的消息接受者

发送事务消息
 package org.example.dir01.transaction;

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;

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) {
        TransactionMQProducer producer = new TransactionMQProducer("transaction_producer_group");
        producer.setNamesrvAddr("rocketmqOS:9876");

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("client-transaction-msg-check-thread");
                return thread;
            }
        });
        // 为生产者指定线程池
        producer.setExecutorService(threadPoolExecutor);
        // 为生产者执行事务监听器
        producer.setTransactionListener(new MyTransactionListener());
        try {
            producer.start();
        } catch (MQClientException e) {
            throw new RuntimeException(e);
        }

        String[] tags = {"tag1", "tag2", "tag3"};
        for (int i = 0; i < 3; i++) {
            try {
                Message message = new Message("topic01", tags[i], "key1", ("hello" + i).getBytes());
                // 发送事务消息,第二个参数指定在执行本地事务时需要的业务参数
                // 这行命令相当于开启全局事务
                producer.sendMessageInTransaction(message, null);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

class MyTransactionListener implements TransactionListener {
    // 回调函数,消息预提交成功后,执行本地事务
    @Override
    public LocalTransactionState executeLocalTransaction(Message message, Object o) {
        System.out.println("预提交消息成功:" + message);
        switch (message.getTags()) {
            case "tag1": {
                return LocalTransactionState.COMMIT_MESSAGE;
            }
            case "tag2": {
                return LocalTransactionState.ROLLBACK_MESSAGE;
            }
            case "tag3": {
                return LocalTransactionState.UNKNOW;
            }
        }
        return LocalTransactionState.UNKNOW;
    }
    // 消息回查方法,创建触发原因:
    // 1. 回调操作返回 UNKNOW
    // 2. 服务器长时间未收到TM的最终全局事务确认指令
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
        System.out.println("消息回查:" + messageExt);
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}

批量消息

生产者发送消息时可一次发送多条消息,这可大大提升效率,但注意:

  • 批量发送的消息必须有相同Topic
  • 必须有相同刷盘策略
  • 不能是延迟消息或事务消息

默认一批消息总大小不能超过4MB,解决尺寸过大的方案:

  1. 将消息进行拆分,分多批次发送
  2. 在Producer端和Broker端修改属性
    1. Producer端在发送前设置 Producer 的 maxMessageSize 属性
    2. Broker端要修改其加载的配置文件中 maxMessageSize 属性

生产者使用send发送的Message并不是直接序列化后发送,而是通过此Message生成一个字符串并发送。字符串有4部分:Topic、消息Body、消息日志(20字节)、描述消息的KV。

常用监听接口的方法的第一个参数是一个列表,但默认每次只消费一条消息。可通过修改Consumer的 consumeMessageBatchMaxSize 属性指定每次消费的数量。不可超过32,默认每次最多拉取32条,可通过修改Consumer的pullBatchSize属性指定拉取上限。

consumer 的 pullBatchSize 与 consumeMessageBatchMaxSize 并非越大越好

  • pullBatchSize 越大,Consumer每次拉取需要的时间就越长,出现网络问题的可能性越大。如果出现问题,本次拉取的所有消息都要重新拉取
  • consumeMessageBatchMaxSize 越大,Consumer的消息并发能力越低,且这批消息有相同的消费结果。因为这一个Batch的消息只会用一个线程进行处理,且处理过程只要有一个消息处理异常,这批消息都要全部重新消费

消息过滤

Topic过滤、Tag过滤、SQL过滤

  • Tag过滤:通过consumer的subscribe() 方法指定要订阅的Tag。订阅多个Tag可用 || 分割
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("Consumer Group");
    consumer.subscribe("Topic", "TAG1 || TAG2 || TAG3");
  • SQL过滤:通过特定表达式对事先埋入到消息的用户属性(消息尾部的properties)进行筛选过滤的方式。通过SQL过滤可实现对消息的复杂过滤,只有Push模式消费者可用。支持多种常量类型和运算符:数字、字符串、布尔、NULL、数值字符比较、逻辑运算、IS NULL、IS NOT NULL
    FilterBySQL
    public class FilterBySQLConsumer {
    	public static void main(String[]args)throws Exception {
    		DefaultMQPushConsumer consumer new DefaultMQPushConsumer("pg");
    		consumer.setNamesrvAddr("rocketmqos:9876");
    		consumer.setConsumeFromWhere(ConsumeFromwhere.CONSUME_FROM_FIRST_OFFSET);
    		// 从指定Topic中过滤出properties中age在 0 到 6 之间的消息
    		consumer.subscribe("myTopic", MessageSelector.bySql("age between 0 and 6"));
    		consumer.registerMessageListener(new MessageListenerConcurrently(){
    			@Override
    			public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt>msgs, ConsumeConcurrentlyContext context){
    				for (MessageExt me : msgs){
    					System.out.println(me);
    				}
    				return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    			}
    		});
    		consumer.start();
    		System.out.println("Consumer Started");
    	}
    }

SQL 过滤功能默认不开启,需要在Broker配置文件中手动指定属性才能开启该功能:

enablePrepertyFilter = true

消息发送重试机制

Producer 对发送失败的消息进行重新发送:

  • oneway 消息发送方式没有重试机制
  • 普通消息有重试机制,顺序消息没有重试机制
  • 消息重试尽可能保证消息不丢失,但可能造成消息重复。消息重复在RocketMQ中无法避免
  • 消息重复一般不会发生,当消息量大、网络抖动时有很大概率发生
  • producer主动重发、consumer负载变化(Rebalance)也会导致重复消息
  • 消息重复无法避免,要避免重复消费
  • 避免重复消费:为消息添加唯一标识,使消费者对消息进行消费判断来避免重复消费
  • 消息发送重试有三种策略:
    • 同步发送失败策略
    • 异步发送失败策略
    • 消息刷盘失败策略

同步发送失败策略

消息发送默认采用轮询选择队列,默认失败时重试2次。重试时会选择新的Broker,只有一个Broker时会选一个新Queue。

Broker 还有失败隔离功能,Producer会优先选择未曾发生过失败的Broker作为目标。

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

异步发送失败策略

异步发送失败重试时,选择在同一个broker上重试,所以该策略无论保证消息不丢。

消息刷盘失败策略

消息刷盘超时或slave不可用(返回非 SEND_OK)时,默认不会将消息尝试发送到其他Broker。对重要消息可通过在Broker的配置文件设置 retryAnotherBrokerWhenNotStoreOKtrue 来开启。

消息消费重试机制

顺序消费没有发送重试,但有消费重试。

  • 顺序消息
    Consumer消费失败后,为保证消息顺序性,会自动不断进行消息试错,知道消费成功。重试期间可能出现消费被阻塞
    顺序消费的重试是无休止的,要及时监控并处理消费失败的情况,避免永久性阻塞
  • 无序消息(普通、延时、事务消息)
    可通过设置返回状态达到消息重试效果。只对集群模式生效广播消费失败后不会重试

无序消息集群消费的重试,每条消息最多重试16次,每次重试的间隔时间逐渐增长,放弃重试后将消息投递到死信队列。重试策略会应用到整个消费者组。

重试队列

对于要重试的消息并不是等待指定时长后再次拉取,而是将其放入一个特殊Topic的队列中,而后再次消费。这个特殊队列叫重试队列。出现要重试消费的消息时,Broker为每个消费者组设置一个Topic名为 %RETRY%consumerGroup@consumerGroup 的重试队列。

重试的延时间隔延时消息的延时等级有很大重合。Broker对重试消息的处理就是通过延时消息实现的,先保存到 SCHEDULE_TOPIC_XXXX 延迟队列,到时间后再投递到  %RETRY%consumerGroup@consumerGroup 重试队列

消息重试配置

需要一下配置的一种才能触发重试:

  • 返回 ConcumeConcurrentlyStatus.RECONSUME_LATER(官方推荐)
  • 返回 Null
  • 抛出异常

返回 ConcumeConcurrentlyStatus.CONSUME_SUCCESS 则不会进行重试

死信队列

达到最大重试次数后,消息发到特殊队列,死信队列(Dead-Letter Queue,DLQ),其中消息为死信消息(Dead-Letter Message,DLM)。

DLQ用于处理无法被正常消费的消息。

特征:

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

DLQ内部消息应当由开发人员特殊处理

posted @ 2023-05-29 16:26  某某人8265  阅读(30)  评论(0编辑  收藏  举报