RocketMQ

一、介绍

RocketMq是一款使用Java语言开发的消息队列中间件,没有遵循常见的MQ协议而采用自研协议,由Alibaba开发后捐献给Apache基金会,历经十余年的大规模场景打磨,具有极高的性能和稳定性。

官网:GitHub - apache/rocketmq: Apache RocketMQ is a cloud native messaging and streaming platform, making it simple to build event-driven applications.

常见的MQ协议

  1. JMS

    JMS即Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(Message Oriented Middleware)的API,它便于消息系统中的Java应用程序进行消息交换,并且通过提供标准的产生、发送、接收消息的接口简化企业应用的开发【ActiveMQ遵循该协议】

  2. STOMP

    STOMP(Streaming Text Orientated Message Protocol)是流文本定向消息协议,是一种为MOM设计的简单文本协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互【ActiveMQ遵循该协议,RabbitMq也支持】

  3. AMQP

    AMQP(Advanced Message Queuing Protocol)是一个提供统一消息服务为MOM设计的应用层标准高级消息队列协议。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制【RabbitMq遵循该协议】

  4. MQTT

    MQTT(Message Queuing Telemetry Transport)是基于发布/订阅范式的消息协议,是为在硬件性能低的IoT设备以及网络状况糟糕的情况下设计的【RabbitMq支持该协议】

二、基本概念

1.组成部分

RocketMQ_第1张图片

生产者(Producer):负责生产消息,然后将消息发送到Broker中,一个生产者可以生产多种Topic的消息

消费者(Consumer):负责拉取Broker中消息进行消费,一个消费者只能消费一种Topic中一个分区(Queue)的消息

代理服务器(Broker):负责消息的存储、投递和查询,一般以集群方式保证高可用性

RocketMQ_第2张图片

队列(Queue):为增加消息的写入能力,对Topic进行分区,也称为队列,一个 Topic 可能有多个队列,并且可能分布在不同的 Broker 上,一个Broker中的相同Topic的所有分区组成一个分片

主题(Topic):表示一类消息的集合,每一条消息只能属于一个Topic

消息主要由以下部分构成:

  • topic:消息的主题

  • body:消息的内容

  • properties:消息属性

  • transactionId:在事务消息中使用

其中properties中还包括Tag

  • Topic:消息主题,通过 Topic 对不同的业务消息进行分类

  • Tag:消息标签,用来进一步区分某个 Topic 下的消息分类

消息标识

  • msgId:由生产者发送消息时产生

  • offsetMsgId:当消息到达Broker中时产生

  • keys:业务层面的唯一标识码,方便查询

2.系统模型

RocketMQ_第3张图片

NameServer是一个简单的Topic路由注册中心,支持Topic、Broker的动态注册与发现,主要包括两个功能

  1. Broker管理:NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活

  2. 路由信息管理:每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。Producer和Consumer通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费

NameServer通常会有多实例部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,客户端仍然可以向其它NameServer获取路由信息。

Broker需要以集群方式部署,将其分为Master和Slave,指定相同的BrokerName,不同的BrokerId,BrokerId为0表示Master,非0表示Slave。Master负责处理读写操作,Slave负责对Master的数据进行备份,当Master宕机,Slave会自动切换成Master工作,保证系统的高可用性。

  • 每个 BrokerNameServer 集群中的所有节点建立长连接,定时注册 Topic 信息到所有 NameServer

  • ProducerNameServer 集群中的其中一个节点建立长连接,定期从 NameServer 获取Topic路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master 发送心跳

  • ConsumerNameServer 集群中的其中一个节点建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,且定时向 Master、Slave发送心跳。Consumer 既可以从 Master 订阅消息,也可以从Slave订阅消息

RocketMQ_第4张图片

复制策略

Broker Master和Slave间数据同步策略,分为:

  • 同步复制(SYNC_MASTER):消息写入Master成功后,等待Slave同步数据成功才会返回确认Ack

  • 异步复制(ASYNC_MASTER):消息写入Master成功后,直接返回确认Ack,无须等待Slave同步数据

刷盘策略

Broker中消息落盘方式,即消息从内存持久化到磁盘,分为:

  • 同步刷盘:当消息持久化到broker磁盘成功才会算写入成功

  • 异步刷盘:当消息写入内存就算写入成功,无须等待消息持久化到磁盘

为增加消费能力,将多个订阅同一个Topic的消费者组成一个消费者组,这些个消费者根据分配策略分摊同一个Topic的消息

分配策略

  • 平均分配策略(默认)

  • 机房优先分配策略

  • 一致性Hash分配策略

在平均分配策略下,一般可以通过增加消费者的数量来提高消费的并行度,必须在Topic中队列数量大于消费者数量的前提下

RocketMQ_第5张图片

RocketMQ_第6张图片

RocketMq工作流程

  1. 启动NameServer,监听端口,等待Broker、Producer、Consumer连接

  2. 启动Broker,与所有NameServer保持长连接,定时发送包含Broker信息以及所有Topic信息的心跳包。注册成功后,NameServer集群中存在Topic和Broker的映射关系

  3. 创建Topic指定存储在那个Broker上

  4. 生产者发送消息。先跟NameServer集群中的一个节点建立长连接,并从中获取和发送的Topic相关的Broker信息,默认通过轮询的方式从队列列表中选出一个队列,然后开始发送消息

  5. 消费者接收消息。先跟NameServer集群中的一个节点建立长连接,并从中获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,然后开始消费消息

三、docker安装

1.单节点单副本模式

 
  

//拷贝出runserver.sh、runbroker.sh进行修改
docker run -it --net=host --name rmq_nameserv apache/rocketmq:4.5.1 ./mqnamesrv
docker cp rmq_nameserv:/home/rocketmq/rocketmq-4.5.1/bin/runserver.sh /docker/registry/rocketmq/runserver.sh
docker cp rmq_nameserv:/home/rocketmq/rocketmq-4.5.1/bin/runbroker.sh /docker/registry/rocketmq/runbroker.sh
docker rm -f rmq_nameserv
//安装nameserver
docker run -d -p 9876:9876 --name rmq_nameserv --privileged=true 
-v /docker/registry/rocketmq/logs/nameserv/:/home/rocketmq/logs/rocketmqlogs 
-v /docker/registry/rocketmq/runserver.sh:/home/rocketmq/rocketmq-4.5.1/bin/runserver.sh
apache/rocketmq:4.5.1 ./mqnamesrv

修改runserver.sh文件:

RocketMQ_第7张图片

编写broker.conf配置文件:

 
  

 
  
# 所属集群名字
brokerClusterName=DefaultCluster
# broker 名字
brokerName=broker-a
# 0 表示 Master, > 0 表示 Slave
brokerId=0
# nameServer地址, 分号分割
namesrvAddr=服务器外网IP:9876
# 启动IP,如果 docker 报 com.alibaba.rocketmq.remoting.exception.RemotingConnectException: connect to <192.168.0.120:10909> failed
# 解决方式1 加上一句 producer.setVipChannelEnabled(false)
# 解决方式2 brokerIP1 设置服务器外网IP,不要使用docker内部IP
brokerIP1=服务器外网IP
# 在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
defaultTopicQueueNums=4
# 是否允许 Broker 自动创建 Topic,建议线下开启,线上关闭
autoCreateTopicEnable=true
# 是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup=true
# 开启ACL权限控制
aclEnable=true
# Broker 对外服务的监听端口
listenPort=10911
# 删除文件时间点,默认凌晨4点
deleteWhen=04
# 文件保留时间,默认48小时
fileReservedTime=72
# 限制的消息大小
maxMessageSize=65536
# Broker 的角色
# - ASYNC_MASTER 异步复制Master
# - SYNC_MASTER 同步双写Master
# - SLAVE
brokerRole=ASYNC_MASTER
# 刷盘方式
# - ASYNC_FLUSH 异步刷盘
# - SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH

参考 rocketmq/plain_acl.yml at 4.9.x · apache/rocketmq · GitHub,编写plain_acl.yml

RocketMQ_第8张图片

 
  

 
  
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
# 全局白名单, 匹配的Ip直接放行
globalWhiteRemoteAddresses:
  - 192.168.0.*
# 设置用户信息
accounts:
# 普通用户
-  accessKey: rocketMQ
    secretKey: 12345678
    whiteRemoteAddress:
    admin: false
    defaultTopicPerm: DENY # 默认Topic权限为DENY
    defaultGroupPerm: SUB # 默认消费者组权限为DENY, 设置为SUB
    topicPerms: # 设置特殊Topic的权限 
      - topicA=DENY 
      - topicB=PUB|SUB
      - topicC=SUB
    groupPerms: # 设置特殊消费组的权限
      - groupA=DENY 
      - groupB=PUB|SUB
      - groupC=SUB
# 对于TopicA、groupA直接拒绝;对于TopicB, groupB可以发送、订阅消息;对于TopicC,groupC只能订阅消息
# 管理员
  - accessKey: 用户名
    secretKey: 签名
    whiteRemoteAddress: # 设置白名单
    # 开启管理员 允许访问所有资源
    admin: true
 
  

 
  
//安装broker
docker run -d 
-p 10909:10909 -p 10911:10911 -p 10912:10912 --name rmq_broker --privileged=true 
-v /docker/registry/rocketmq/broker.conf:/home/rocketmq/rocketmq-4.5.1/conf/broker.conf 
-v /docker/registry/rocketmq/plain_acl.yml:/home/rocketmq/rocketmq-4.5.1/conf/plain_acl.yml
-v /docker/registry/rocketmq/store/:/home/rocketmq/store 
-v /docker/registry/rocketmq/logs/:/home/rocketmq/logs 
-v /docker/registry/rocketmq/runbroker.sh:/home/rocketmq/rocketmq-4.5.1/bin/runbroker.sh apache/rocketmq:4.5.1 ./mqbroker -c ../conf/broker.conf

修改runbroker.sh文件:

RocketMQ_第9张图片

注意:

如果日志rocketmq_client.log出现org.apache.rocketmq.client.exception.MQBrokerException:error=Algorithm HmacSHA1 not available异常

将jre中\lib\ext目录里找到sunjce_provider.jar,复制到lib目录然后重启broker即可

参照官网:GitHub - apache/rocketmq-dashboard: The state-of-the-art Dashboard of Apache RoccketMQ provides excellent monitoring capability. Various graphs and statistics of events, performance and system information of clients and application is evidently made available to the user.

编写users.properties文件:

 
  

 
  
# This file supports hot change, any change will be auto-reloaded without Dashboard restarting.
# Format: a user per line, username=password[,N] #N is optional, 0 (Normal User); 1 (Admin)
# Define Admins 设置登录用户格式: 用户名=密码,1(1代表管理员, 0代表普通用户)
用户名=密码,1
# Define Users
#user1=123456,0
 
  

 
  
//安装console
docker run -d -p 10000:8080 --name rmq_console --privileged=true 
-v /docker/registry/rocketmq/console/data:/tmp/rocketmq-console/data 
-v /docker/registry/rocketmq/logs/console/:/root/logs 
-e JAVA_OPTS="-Drocketmq.namesrv.addr=服务器外网Ip:9876 
-Drocketmq.config.loginRequired=true 
-Drocketmq.config.dataPath=/tmp/rocketmq-console/data
-Drocketmq.config.accessKey=用户名
-Drocketmq.config.secretKey=签名
-Drocketmq.config.isVIPChannel=false" 
apacherocketmq/rocketmq-dashboard:latest

服务器需要打开安全组10911、10000端口,输入自定义的用户密码登录:

RocketMQ_第10张图片

RocketMQ_第11张图片

运行tool.sh测试消息的生产和消费

 
  

 
  
docker exec -it rmq_broker bash
//发送消息
export NAMESRV_ADDR=服务器外网IP:9876
sh tools.sh org.apache.rocketmq.example.quickstart.Producer
//消费消息
sh tools.sh org.apache.rocketmq.example.quickstart.Consumer

RocketMQ_第12张图片

RocketMQ_第13张图片

RocketMQ_第14张图片

2.集群模式

多节点单副本模式

一个集群内全部部署 Master 角色,不部署Slave 副本,这种模式的优缺点如下:

  • 优点:配置简单性能最高,在磁盘配置为RAID10时,单个Master宕机或重启维护对应用无影响,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢失(异步刷盘丢失少量消息,同步刷盘一条不丢)

  • 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响

多节点多副本-异步复制模式

每个Master配置一个Slave,有多组 Master-Slave,采用异步复制方式,Master和Slave属于主备关系,Master处理消息的读写,Slave仅用于消息备份和Master宕机后的角色切换。这种模式的优缺点如下:

  • 优点:Master宕机后,Slave会自动切换为Master

  • 缺点:不过Slave同步数据过程有短暂消息延迟(毫秒级),存在消息少量丢失的情况

主节点配置:

 
  

 
  
namesrvAddr=x.x.x.x:9876 # 多个namesrvAddr以;隔开
brokerClusterName=DefaultCluster
brokerName=broker-a
brokerId=0 # Master节点
deleteWhen=04
fileReservedTime=72
brokerRole=ASYNC_MASTER # 异步复制Master
flushDiskType=ASYNC_FLUSH

从节点配置:

 
  

 
  
namesrvAddr=x.x.x.x:9876 # 多个namesrvAddr以;隔开
brokerClusterName=DefaultCluster
brokerName=broker-a # Slave和Master指定相同的名称表明为同一个小集群
brokerId=1 # Slave节点
deleteWhen=04
fileReservedTime=48
brokerRole=SLAVE # Slave节点
flushDiskType=ASYNC_FLUSH

多节点多副本-同步双写模式

每个Master配置一个Slave,有多对Master-Slave,采用同步双写方式,即消息写入Master成功后,会等待Slave同步数据成功后才向Producer返回ACK。这种模式的优缺点如下:

  • 优点:不存在消息丢失,服务可用性与数据可用性都非常高

  • 缺点:性能比异步复制模式略低(大约低10%左右),发送单个消息的RT会略高,且目前版本在主节点宕机后,备机不能自动切换为主机

主节点配置:

 
  

 
  
namesrvAddr=x.x.x.x:9876 # 多个namesrvAddr以;隔开
brokerClusterName=DefaultCluster
brokerName=broker-a
brokerId=0 # Master节点
deleteWhen=04
fileReservedTime=48
brokerRole=SYNC_MASTER # 同步双写Master
flushDiskType=ASYNC_FLUSH

从节点配置:

 
  

 
  
namesrvAddr=x.x.x.x:9876 # 多个namesrvAddr以;隔开
brokerClusterName=DefaultCluster
brokerName=broker-a # Slave和Master指定相同的名称表明为同一个小集群
brokerId=1 # Slave节点
deleteWhen=04
fileReservedTime=48
brokerRole=SLAVE # Slave节点
flushDiskType=ASYNC_FLUSH

生产环境下,搭建集群会为Master配置RAID10磁盘阵列,然后给Master配置一个Slave,利用磁盘阵列的安全性,也不会出现某个节点宕机后消息无法订阅的问题。

磁盘阵列RAID(Redundant Arrays of Independent Disks)

磁盘阵列是将很多块独立的磁盘组合成一个容量巨大的磁盘组,利用个别磁盘提供数据所产生加成效果提升整个磁盘系统效能。利用这项技术,将数据切割成许多区段,分别存放在各个硬盘上。磁盘阵列还能利用同位检查的观念,当数组中任意一个硬盘发生故障时仍可读出数据

RAID主要利用镜像、数据条带和数据校验技术来实现高性能、可靠性、容错能力和扩展性,根据这山中技术不同的组合,将RAID分为不同的等级。原始RAID等级为RAID0~RAID6,后续出现RAID7、RAID10、RAID01、RAID50、RAID53、RAID100等。这些个等级没有高下之分,在实际应用中,需要根据特定情况、性能和成本进行选择。

  • 镜像:一种磁盘备份的冗余技术,防止磁盘发生故障而造成数据丢失。

  • 数据条带:一种自动将I/O操作负载均衡到多个物理磁盘上的技术,即将一块连续的数据分成很多份分别存储在不同的磁盘上,提高I/O的并行能力和性能

  • 数据校验:在写入数据同时进行校验计算并存储校验结果,在某个磁盘故障数据出错时,通过对剩余数据进行反校验得到丢失的数据

JBOD(Just a Bunch Of Disks)

RocketMQ_第15张图片

将多块硬盘串联起来组成一个大的存储设备,从某种意义上说这种类型不被算作RAID。总体容量是所有磁盘的总和,性能相当一块磁盘。

RAID0

RocketMQ_第16张图片

将数据分割成不同条带(Stripe)分散写入到所有的硬盘中同时进行读写。多块硬盘的并行操作使同一时间内磁盘读写的速度提升N倍。

RAID1

RocketMQ_第17张图片

把一个磁盘的数据备份到另一个磁盘上,也就是说数据在写入一块磁盘的同时,会在另一块闲置的磁盘上生成镜像文件,只要系统中任何一对镜像盘中至少有一块磁盘可以使用,甚至可以在一半数量的硬盘出现问题时系统都可以正常运行,当一块硬盘失效时,系统会忽略该硬盘,转而使用剩余的镜像盘读写数据,具备很好的磁盘冗余能力,磁盘利用率为50%。

RAID10

RocketMQ_第18张图片

RAID1和RAID0的组合体。先将写入的数据分块,然后对每一块数据进行备份存存储。

RAID01

RocketMQ_第19张图片

RAID0和RAID1的组合体。先将写入的数据整个备份,然后在分块存储。

总结

RAID10在disk0和disk2、disk1和disk3两块磁盘故障下仍能正常工作,而RAID01在任意两块磁盘故障后无法正常工作,所以RAID10比RAID01容错率更高,在生产中一般选择RAID10

四、整合SpringBoot

导入依赖:

 
  

 
  
    org.apache.rocketmq
    rocketmq-spring-boot-starter
    2.0.3
 
  

application.yaml配置文件:

 
  

 
  
rocketmq:
  name-server: 服务器外网IP:9876
  producer:
    group: test # 生产者组名称
    accessKey: 用户名
    secretKey: 签名
  consumer:
    access-key: 用户名
    secretKey: 签名

1.普通消息

同步发送消息:发出一条消息后,在等待服务端的响应之后,才会发下一条消息,可靠性高,用于发送重要的通知消息、短消息通知等

异步发送消息:发出一条消息后,不用等待服务端返回响应,接着发送下一条消息,通过回调接口接收服务端响应,并处理响应结果,一般用于链路耗时较长,对响应时间较为敏感的业务场景

单向发送消息:只发送消息,不等待服务端返回响应且没有应答,耗时非常短,一般在微秒级别,适用于耗时非常短、可靠性要求不高的场景,例如日志收集

生产者:

 
  

 
  
public interface ProduceService {
    /**
     * 同步发送消息
     * @param topic 主题
     * @param content 消息内容
     * @return 发送消息结果
     */
    SendResult sendSyncMsg(String topic, String content);
    /**
     * 异步发送消息
     * @param topic 主题
     * @param content 消息内容
     */
    void sendAsyncMsg(String topic, String content);
    /**
     * 发送单向消息
     * @param topic 主题
     * @param content 消息内容
     */
    void sendOnewayMsg(String topic, String content);
}
 
  

 
  
@Slf4j
@Service
public class ProduceServiceImpl implements ProduceService {
    private final RocketMQTemplate rocketMQTemplate;
    public ProduceServiceImpl(RocketMQTemplate rocketMQTemplate) {
        this.rocketMQTemplate = rocketMQTemplate;
    }
    @Override
    public SendResult sendSyncMsg(String topic, String content) {
        // 1)MessageBuilder创建GenericMessage
        // Message message = MessageBuilder.withPayload(content).build();
        // 2)new 直接构造GenericMessage
        GenericMessage message = new GenericMessage<>(content);
        SendResult sendResult = rocketMQTemplate.syncSend(topic + ":" + "sync", message);
        log.info("result:{}", sendResult);
        return sendResult;
    }
    @Override
    public void sendAsyncMsg(String topic, String content) {
        GenericMessage message = new GenericMessage<>(content);
        rocketMQTemplate.asyncSend(topic + ":" + "async", message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("result:{}", sendResult);
            }
            @Override
            public void onException(Throwable e) {
                log.info("error:{}", e.getMessage());
            }
        });
    }
    @Override
    public void sendOnewayMsg(String topic, String content) {
        GenericMessage message = new GenericMessage<>(content);
        rocketMQTemplate.sendOneWay(topic + ":" + "oneway", message);
    }
}
 
  

 
  
@RequestMapping("/produce")
@RestController
public class ProduceController {
    private final ProduceService produceService;
    public ProduceController(ProduceService produceService) {
        this.produceService = produceService;
    }
    @RequestMapping("/sync/{topic}/{content}")
    public SendResult sendSyncMsg(@PathVariable("topic") String topic, @PathVariable("content") String content) {
        return produceService.sendSyncMsg(topic, content);
    }
    @RequestMapping("/async/{topic}/{content}")
    public void sendAsyncMsg(@PathVariable("topic") String topic, @PathVariable("content") String content) {
        produceService.sendAsyncMsg(topic, content);
    }
    @RequestMapping("/oneway/{topic}/{content}")
    public void sendOnewayMsg(@PathVariable("topic") String topic, @PathVariable("content") String content) {
        produceService.sendOnewayMsg(topic, content);
    }
}

消费者:

 
  

 
  
@Slf4j
@Component
// topic指定消费者订阅的主题, consumerGroup指定消费者组名
// 实现RocketMQPushConsumerLifecycleListener接口可以自定义消费者的相应参数值
@RocketMQMessageListener(topic = "TopicTest", consumerGroup = "TopicTest-consumer")
public class ConsumeListener implements RocketMQListener, RocketMQPushConsumerLifecycleListener {
    @Override
    public void onMessage(String message) {
        log.info("消息-\"{}\"-被消费...", message);
    }
    
    //配置Consumer相关参数
    @Override
    public void prepareStart(DefaultMQPushConsumer consumer) {
        //consumer.setMaxReconsumeTimes(-1);
    }
}

同步发送消息:

RocketMQ_第20张图片

RocketMQ_第21张图片

异步发送消息:

RocketMQ_第22张图片

单向发送消息:

RocketMQ_第23张图片

2.顺序消息

对于一个指定的Topic,消息严格按照先进先出(FIFO)的原则进行消息发布和消费,按照规则对消息进行分区同一个ShardingKey的消息会被分配到同一个队列中。只有同时满足了生产顺序性和消费顺序性才能达到上述的效果

生产顺序性:单一生产者串行地发送消息,并按序存储和持久化

消费顺序性:单一消费者一个队列按顺序异步消费消息

建议选择最细粒度的分区键进行拆分,例如将订单ID、用户ID作为分区键关键字

生产者:

 
  

 
  
@Override
public void sendOrderedMsg(String topic) {
    //表示一组消息的标识
    String id = "1234";
    for (int i = 0; i < 10; i++) {
        //设置消息的key值 RocketMQHeaders.KEYS -> KEYS
        Map headers = new HashMap<>();
        headers.put(RocketMQHeaders.KEYS, i + 1);
        GenericMessage message = new GenericMessage<>("第" + (i + 1) + "条消息", headers);
        //同步发送顺序消息
        //SendResult sendResult = rocketMQTemplate.syncSendOrderly(topic + ":order", message, id);
        //log.info("sendOrderedMsg result:{}", sendResult);
        //异步发送顺序消息
        //rocketMQTemplate.asyncSendOrderly(topic + ":order", message, id, new SendCallback() {
        //    @Override
        //    public void onSuccess(SendResult sendResult) {
        //        log.info("sendOrderedMsg result:{}", sendResult);
        //    }
        //
        //    @Override
        //    public void onException(Throwable e) {
        //        log.info("error:{}", e.getMessage());
        //    }
        //});
        //单向发送顺序消息
        rocketMQTemplate.sendOneWayOrderly(topic + ":order", message, id);
    }

消费者:

 
  

 
  
@Slf4j
@Component
//需要将consumeMode(默认为CONCURRENTLY)设置为ORDERLY 一个线程一个队列按顺序异步接受消息
@RocketMQMessageListener(topic = "TopicA", consumerGroup = "TopicA-ordered", consumeMode = ConsumeMode.ORDERLY)
public class OrderedConsumeListener implements RocketMQListener {
    @Override
    public void onMessage(String message) {
        log.info("消息-\"{}\"-被消费...", message);
    }
}

RocketMQ_第24张图片

RocketMQ_第25张图片

3.延时消息

将消息延迟一定时间后才投递到Consumer进行消费,例如电商系统下单一定时间内未支付会自动取消订单

RocketMQ默认支持18个延迟等级

RocketMQ_第26张图片

生产者:

 
  

 
  
@Override
public void sendDelayMsg(String topic, String content) {
    GenericMessage message = new GenericMessage<>(content);
    //使用延时等级2(延时5s) 异步发送消息
    rocketMQTemplate.asyncSend(topic + ":delay", message, new SendCallback() {
        @Override
        public void onSuccess(SendResult sendResult) {
            log.info("消息【{}】发送成功!", message.getPayload());
        }
        @Override
        public void onException(Throwable e) {
            log.info("消息【{}】发送失败!!!", message.getPayload());
        }
    }, 1500, 2);
}

消费者:

 
  

 
  
@Slf4j
@Component
@RocketMQMessageListener(topic = "TopicB", consumerGroup = "TopicTest-delay")
public class DelayConsumeListener implements RocketMQListener {
    @Override
    public void onMessage(String message) {
        log.info("消息【{}】被消费...", message);
    }
}

注意

如果将大量延时消息的定时时间设置为同一时刻,则到达该时刻后会有大量消息同时需要被处理,会造成系统压力过大,导致消息分发延迟,影响定时精度

4.事务消息

在普通消息基础上,支持二阶段的提交能力(2PC),将二阶段提交和本地事务绑定,保证全局的一致性

事务消息步骤:

  1. 生产者将半事务消息发送到Broker

  2. Broker将消息持久化成功之后,向生产者返回 Ack 确认消息已经发送成功,此时消息暂不能投递,为半事务消息

  3. 生产者开始执行本地事务逻辑

  4. 生产者根据本地事务执行结果向服务端提交二次确认结果

    • 二次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者

    • 二次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者

  5. 当在断网、生产者应用重启,服务端未收到发送者提交的二次确认结果或服务端收到的二次确认结果为Unknown未知状态时,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查

  6. 服务端仅会按照参数尝试指定次数,超过次数后事务会强制回滚

  7. 生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果

  8. 生产者根据检查得到的本地事务的最终状态再次提交二次确认

注意

  • 回查时如果原生产者已经崩溃,Broker则会联系同一生产者组的其他生产者实例回查本地事务执行情况

  • 事务消息不支持延时

  • 对于事务消息,需要进行幂等性检测,可能出现回滚后在提交

消息回查相关配置(broker.conf)

 
  

 
  
transactionTimeout=60 //生产者在60s内将最终状态发送给Broker,否则进行消息回查(默认为60s)
transactionCheckMax=15 //最多回查15次, 超过后将丢弃消息并记录到日志中(默认为15次)
transactionCheckInterval=60 //多次消息回查的时间间隔为60s(默认为60s)

事务监听器:

 
  

 
  
@Slf4j
@Component
//设置事务组名称 核心线程数 最大线程数
@RocketMQTransactionListener(txProducerGroup = "txGroup", corePoolSize = 2, maximumPoolSize = 5)
public class TransactionListener implements RocketMQLocalTransactionListener {
    /**
     * 半事务消息发送成功后, 执行本地事务的方法
     * @param msg 消息对象
     * @param arg 传递的业务参数
     * @return 执行本地事务后状态
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        if ("COMMIT_MESSAGE".equals(arg)) {
            return RocketMQLocalTransactionState.COMMIT;
        } else if ("ROLLBACK_MESSAGE".equals(arg)) {
            return RocketMQLocalTransactionState.ROLLBACK;
        } else if ("UNKNOWN".equals(arg)) {
            return RocketMQLocalTransactionState.UNKNOWN;
        }
        return RocketMQLocalTransactionState.UNKNOWN;
    }
    /**
     * 回查事务状态的方法
     * @param msg 消息对象
     * @return 回查到的本地事务状态
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        Object payload = msg.getPayload();
        int i = payload.hashCode() % 3;
        RocketMQLocalTransactionState[] states = RocketMQLocalTransactionState.values();
        return states[i];
    }
}

生产者:

 
  

 
  
@Override
public void sendTransactionMsg(String tag, String content) {
    GenericMessage message = new GenericMessage<>(content);
    //txProducerGroup, destination, message, arg =>生产者事务组名称、主题:标签、消息、业务参数
    TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction("txGroup", "TopicTransaction:" + tag, message, tag);
    log.info("tag:{} - result:{}", tag, result);
}

消费者:

 
  

 
  
@Slf4j
@Component
@RocketMQMessageListener(topic = "TopicTransaction", consumerGroup = "TopicTest-transaction")
public class TransactionConsumeListener implements RocketMQListener {
    @Override
    public void onMessage(String message) {
        log.info("消息-\"{}\"-被消费...", message);
    }
}

tag为COMMIT_MESSAGE的消息被消费,tag为ROLLBACK_MESSAGE的消息未被消费,tag为UNKNOWN的消息在消息回查后被消费

RocketMQ_第27张图片

5.批量消息

将一些消息聚成一批发送,增加吞吐率,并减少API和网络调用次数,但批量发送的消息必须具有相同的topic

生产者:

 
  

 
  
@Override
public SendResult sendBatchMsg() {
    List> messageList = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        GenericMessage m = new GenericMessage<>("第" + (i + 1) + "条消息");
        messageList.add(m);
    }
 
  
    //设置超时时间为5s
    SendResult sendResult = rocketMQTemplate.syncSend("TopicC:batch", messageList, 5000);
    log.info("result:{}", sendResult);
    return sendResult;
}

消费者:

 
  

 
  
@Slf4j
@Component
@RocketMQMessageListener(topic = "TopicC", consumerGroup = "TopicC-batch")
public class BatchConsumeListener implements RocketMQListener {
    @Override
    public void onMessage(Object message) {
        System.out.println(message instanceof List); //true
        log.info("消息-\"{}\"-被消费...", message);
    }
}

RocketMQ_第28张图片

RocketMQ_第29张图片

6.消息过滤

RocketMQMessageListener注解

 
  

 
  
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RocketMQMessageListener {
    //消费者组名
    String consumerGroup();
 
  
    //订阅的消息主题
    String topic();
 
  
    //消息过滤模式(默认TAG过滤)
    SelectorType selectorType() default SelectorType.TAG;
    //过滤表达式"*"
    String selectorExpression() default "*";
    //消费模式
    ConsumeMode consumeMode() default ConsumeMode.CONCURRENTLY;
 
  
    //消息模型
    MessageModel messageModel() default MessageModel.CLUSTERING;
    //消费者最大线程数
    int consumeThreadMax() default 64;
    //消费超时时间
    long consumeTimeout() default 30000L;
 
  
    //broker认证用户名
    String accessKey() default ACCESS_KEY_PLACEHOLDER;
    //broker认证签名
    String secretKey() default SECRET_KEY_PLACEHOLDER;
    //是否开启消息轨迹
    boolean enableMsgTrace() default true;
    //消息轨迹主题名称 ${rocketmq.consumer.customizedTraceTopic}
    String customizedTraceTopic() default TRACE_TOPIC_PLACEHOLDER;
    //nameServer地址 ${rocketmq.nameServer}
    String nameServer() default NAME_SERVER_PLACEHOLDER;
    //接入通道 ${rocketmq.accessChannel}
    String accessChannel() default ACCESS_CHANNEL_PLACEHOLDER;
}
 
  

 
  
public enum SelectorType {
    TAG, //主题过滤
    SQL92 //Sql过滤
}
 
  

 
  
public enum ConsumeMode {
    CONCURRENTLY, //并发接受消息
    ORDERLY //一个队列、一个线程顺序接受消息
}
 
  

 
  
public enum MessageModel {
    BROADCASTING("BROADCASTING"), //广播模式
    CLUSTERING("CLUSTERING"); //集群模式
    private final String modeCN;
    MessageModel(String modeCN) {
        this.modeCN = modeCN;
    }
    public String getModeCN() {
        return this.modeCN;
    }
}

消费者消费模式

  • 集群模式(CLUSTERING):一条消息只能被消费组内的任意一个消费者消费

  • 广播模式(BROADCASTING):每条消息推送给消费组所有的消费者,保证消息至少被每个消费者消费一次

开启Sql过滤需要配置broker.conf

 
  

 
  
enablePropertyFilter=true

Sql表达式支持常量:

  • 数值

  • 字符(必须用单引号包裹)

  • 布尔(TRUE、FALSE)

  • NULL(表示空)

Sql表达式支持运算符:

  • 数值比较(>、<、≥、≤、BETWEEN、=)

  • 字符(=、<>、IN)

  • 逻辑(AND、OR、NOT)

生产者:

 
  

 
  
@Override
public void sendMsgWithArgs() {
    for (int i = 0; i < 10; i++) {
        //设置m参数 用于Sql过滤
        HashMap map = new HashMap<>();
        map.put("m", i);
        GenericMessage m = new GenericMessage<>("第" + (i + 1) + "条消息", map);
        rocketMQTemplate.asyncSend("TopicD:sql", m, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("result:{}", sendResult);
            }
            @Override
            public void onException(Throwable e) {
                log.info("error:{}", e.getMessage());
            }
        });
    }
}

消费者:

@Slf4j
@Component
//开启Sql消息过滤 只消费 参数m在0~3之间的消息
@RocketMQMessageListener(topic = "TopicD", consumerGroup = "TopicD-sql", selectorType = SelectorType.SQL92, selectorExpression = "m BETWEEN 0 and 3")
public class ConsumeFilterBySQLListener implements RocketMQListener {

    @Override
    public void onMessage(String message) {
        log.info("消息-\"{}\"-被消费...", message);
    }
}

RocketMQ_第30张图片

注意

RocketMQHeadersMessageConst类中定义了一系列消息属性名称,都是系统保留字,自定义参数时需要避免使用

RocketMQ_第31张图片

RocketMQ_第32张图片

RocketMQ_第33张图片

7.Message对象

在使用RocketMQTemplate存在两个不同包下的Message对象:

//org.springframework.messaging包下
public interface Message {

   T getPayload(); //消息负载(数据)

   MessageHeaders getHeaders(); //消息头(可以设置消息的KEYS以及用于过滤的自定义参数)
}
//org.apache.rocketmq.common.message包下
public class Message implements Serializable {
    private static final long serialVersionUID = 8445773977080406428L;
    private String topic; //消息主题
    private int flag;
    private Map properties; //消息属性
    private byte[] body; //消息体
    private String transactionId; //事务id

    ...
}

源码解析

RocketMQ_第34张图片

rocketMQTemplate.syncSend()方法为例:

RocketMQ_第35张图片

 
  

 
  
//RocketMQUtil.convertToRocketMessage()方法
public static org.apache.rocketmq.common.message.Message convertToRocketMessage(
    ObjectMapper objectMapper, String charset,String destination,                       org.springframework.messaging.Message message) {
    
    //1.获取消息内容
    Object payloadObj = message.getPayload();
    byte[] payloads;
 
  
    //2.判断消息内容类型
    if (payloadObj instanceof String) {
        //如果是String类型直接转换为以UTF-8编码的字节数组
        payloads = ((String) payloadObj).getBytes(Charset.forName(charset));
    } else if (payloadObj instanceof byte[]) {
        //如果是byte[]类型直接转换
        payloads = (byte[]) message.getPayload();
    } else {
        try {
            //否则, 将先内容序列化为JSON字符串, 然后转换为以UTF-8编码的字节数组
            String jsonObj = objectMapper.writeValueAsString(payloadObj);
            payloads = jsonObj.getBytes(Charset.forName(charset));
        } catch (Exception e) {
            throw new RuntimeException("convert to RocketMQ message failed.", e);
        }
    }
    //3.获取消息主题和标签
    String[] tempArr = destination.split(":", 2);
    String topic = tempArr[0];
    String tags = "";
    if (tempArr.length > 1) {
        tags = tempArr[1];
    }
 
  
    //4.创建RocketMQ中的Message对象
    org.apache.rocketmq.common.message.Message rocketMsg = new org.apache.rocketmq.common.message.Message(topic, tags, payloads);
 
  
    //5.设置消息请求头
    MessageHeaders headers = message.getHeaders();
    if (Objects.nonNull(headers) && !headers.isEmpty()) {
        //设置消息KEYS属性
        Object keys = headers.get(RocketMQHeaders.KEYS);
        if (!StringUtils.isEmpty(keys)) { // if headers has 'KEYS', set rocketMQ message key
            rocketMsg.setKeys(keys.toString());
        }
        //设置消息FLAG属性
        Object flagObj = headers.getOrDefault("FLAG", "0");
        int flag = 0;
        try {
            flag = Integer.parseInt(flagObj.toString());
        } catch (NumberFormatException e) {
            // Ignore it
            log.info("flag must be integer, flagObj:{}", flagObj);
        }
        rocketMsg.setFlag(flag);
        //设置消息WAIT_STORE_MSG_OK属性
        Object waitStoreMsgOkObj = headers.getOrDefault("WAIT_STORE_MSG_OK", "true");
        boolean waitStoreMsgOK = Boolean.TRUE.equals(waitStoreMsgOkObj);
        rocketMsg.setWaitStoreMsgOK(waitStoreMsgOK);
 
  
        //设置消息自定义业务属性(用于消息过滤)
        headers.entrySet().stream()
            .filter(entry -> !Objects.equals(entry.getKey(), "FLAG")
                && !Objects.equals(entry.getKey(), "WAIT_STORE_MSG_OK")) // exclude "FLAG", "WAIT_STORE_MSG_OK"
            .forEach(entry -> {
                //不包含在MessageConst类中定义的属性 -> 自定义属性
                if (!MessageConst.STRING_HASH_SET.contains(entry.getKey())) {
                    rocketMsg.putUserProperty(entry.getKey(), String.valueOf(entry.getValue()));
                }
            }
        );
    }
    return rocketMsg;
}

在消费者消费时如果需要获取消息本身的属性,需要在发送时使用DefaultMQProducer对象

生产者:

 
  

 
  
@Override
public void sendOriginalMsg() {
    //获取DefaultMQProducer对象
    DefaultMQProducer producer = rocketMQTemplate.getProducer();
    //创建RocketMQ中的Message对象
    org.apache.rocketmq.common.message.Message message = new org.apache.rocketmq.common.message.Message();
    message.setTopic("TopicOrigin");
    message.setTags("original");
    message.setBody("hello...".getBytes(StandardCharsets.UTF_8));
    try {
        SendResult result = producer.send(message);
        log.info("result:{}", result);
    } catch (Exception e) {
        log.error("exception:{}", e.getMessage());
    }
}

消费者:

 
  

 
  
@Slf4j
@Component
@RocketMQMessageListener(topic = "TopicOrigin", consumerGroup = "TopicOrigin-consumer")
//泛型填入MessageExt类型
public class OriginalConsumeListener implements RocketMQListener { 
    @Override
    public void onMessage(MessageExt message) {
        String topic = message.getTopic();
        int flag = message.getFlag();
        String body = new String(message.getBody());
        log.info("topic:{}, flag:{}, body:{}", topic, flag, body);
        log.info("消息-\"{}\"-被消费...", message);
    }
}
 
  

 
  
/*
* org.apache.rocketmq.common.message包下
* MessageExt继承于Message 封装了更多的消息属性
*/
public class MessageExt extends Message {
    private static final long serialVersionUID = 5720810158625748049L;
    private int queueId;
    private int storeSize;
    private long queueOffset;
    private int sysFlag;
    private long bornTimestamp;
    private SocketAddress bornHost;
    private long storeTimestamp;
    private SocketAddress storeHost;
    private String msgId;
    private long commitLogOffset;
    private int bodyCRC;
    private int reconsumeTimes;
    private long preparedTransactionOffset;
    
    ...
}

RocketMQ_第36张图片

五、工作原理

1.生产消息过程

生产者将消息写入到Broker中的Queue,先从NameServer中获取Topic路由表和Broker列表信息,然后根据Queue选择策略,选出一个Queue发送消息

Queue选择算法

对于无序消息,Queue选择算法,也称为消息投递算法,分为:

  • 轮询算法(默认)

    保证每个Queue都可以均匀地获取到消息

  • 最小投递延迟算法

    先统计每次消息投递的时间延迟,然后将消息投递到时间延迟最小的Queue,如果延迟相同则采用轮询算法,可以有效地提高投递效率

2.消息持久化

RocketMQ中消息存储在本地store目录中,内容如下:

RocketMQ_第37张图片

  • abort:该文件在Broker启动后会自动创建,在正常关闭后会自动消失(若在Broker未启动时发现存在该文件,则说明上一次Broker是非正常关闭的)

  • checkpoint:存储commitlog、consumequeue、index文件的最后刷盘时间

  • commitlog:存放写入消息的commitlog文件

  • config:存放Broker在运行期间的配置信息

  • consumequeue:存放队列的文件

  • index:存放消息索引文件

  • lock:运行期间使用的全局锁

commitlog

commitlog文件,又称为mappedFile文件,每一个文件大小小于等于1G,文件名由20位十进制数构成,表示当前文件的第一条消息的起始偏移量(Commitlog Offset)

第一个文件名称为00000000000000000000(全零)

假如第一个文件大小为1073741820字节(1G = 1073741824)

则第二个文件名称为00000000001073741820

当前Broker中所有的消息都会按顺序落盘到该文件中,并没有按照Topic进行分类存放

commitlog文件是由一个个消息单元构成,每一个消息单元包括消息总长度MsgLen、消息物理位置PhysicalOffset、消息体Body、消息体长度BodyLength、消息主题Topic、消息生产者Bornhost、消息发送时间Borntimestamp、消息所在队列QueueId、消息所在队列偏移量QueueOffset等相关属性

RocketMQ_第38张图片

Comsumequeue

consumequeue目录下存放的是当前Broker中按照Topic划分的所有Queue

RocketMQ_第39张图片

默认每个Topic创建四个Queue,对应下标0~4

RocketMQ_第40张图片

 RocketMQ_第41张图片

生产者在发送消息到Broker,在消息落盘到Commitlog文件的同时,会存储一份到对应Topic的consumequeue文件中,consumequeue文件是Commitlog的索引文件,可以通过它定位到具体的消息

RocketMQ_第42张图片

consumequeue文件由一个个索引条目组成,最多可以存储30w个,每个索引条目包括消息在commitlog文件中的偏移量、消息长度、消息Tag组成,一共占20个字节。所以每一个consumequeue文件占30w*20字节大小

consumequeue文件名称也由20位十进制数构成,表示当前文件第一个索引条目的起始偏移量,因为consumequeue文件大小是固定的,所以后续文件名称也是固定的

store/config目录下consumerOffset文件存放消费进度(消费者集群模式下共享)

RocketMQ_第43张图片

RocketMQ_第44张图片

消息写入流程:

  1. Broker根据queueId获取到消息对应索引条目在consumequeue中的偏移量,即queueOffset

  2. 将queueId、queueOffset和消息封装成消息单元

  3. 将消息单元写入commitlog文件

  4. 形成索引条目

  5. 将索引条目发送到对应consumequeue中

消息拉取流程:

  1. Consumer获取到要消费消息的offset

  2. 向Broker发起拉取请求,包括拉取消息的queue、消息offset和tag标签

  3. Broker计算出在该consumequeue中的queueOffset(queueOffset = 消息offset * 20字节(索引条目的长度))

  4. 从该queueOffset开启向后查找第一个指定tag的索引条目

  5. 解析前8个字节(Commitlog offset)

  6. 从对应的commitlog中读取消息单元,并发送给Consumer

indexFile

实现对于包含key属性消息的快速查询,只有当发送到Broker的消息中包含key属性则会写入indexFile文件

RocketMQ_第45张图片

每个indexFile文件都是由indexHeader、slots槽位、indexes索引数据构成,每个indexFile文件中包含500w个slot,每个slot占4字节,每一个slot会挂载许多index索引单元

indexHeader固定为40字节,结构如下:

RocketMQ_第46张图片

  • beginTimestamp:该indexFile中第一条消息的存储时间

  • endTimestamp:该indexFile当前最后一条消息的存储时间

  • beginPhysicalOffset:该indexFile中第一条消息在commitlog中的offset偏移量

  • endPhysicalOffset:该indexFile中最后一条消息在commitlog中的offset偏移量

  • hashSlotCount:挂载了index的槽位slot数量

  • indexCount:该indexFile中包含索引的数量

索引index挂载到slot槽位:

RocketMQ_第47张图片

将消息key的hash值 % 500w即可得到slot槽位序号,slot存放的是当前最新的一个index索引,通过preIndexNo可以得到之前的所有index索引

index索引结构如下:

RocketMQ_第48张图片

  • keyhash:消息key的hash值

  • physicalOffset:当前key对应消息在commitlog中的offset偏移量

  • timeDiff:当前key对应消息存储时间和indexFile文件创建时间的差

  • preIndexNo:当前slot槽位下当前index索引的前一个索引的indexNo

通过索引查询流程:

RocketMQ_第49张图片

3.Rebalance

cluster集群模式下,将一个Topic下的所有queue在同一个ConsumerGroup中的consumer间重新分配的过程,即再平衡,能够提高消息的并行消费能力

RocketMQ_第50张图片

RocketMQ_第51张图片

Rebalance限制:由于一个queue只能分配给一个consumer,当某个ConsumerGroup下consumer数量大于queue数量时,多余的consumer将分配不到任何queue,此时也不会触发Rebalance

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

Rebalance危害

  • 消费暂停:在Rebalance期间,consumer需要暂停部分队列的消费,等到queue重新分配之后才能继续消费

  • 消费重复:在Rebalance后,consumer必须接着之前提交的消费进度继续消费,然而默认情况下consumer在消费完一条消息后会异步提交消费进度,这就导致和实际情况不一致,可能存在对消息的重复消费

  • 消费突刺:如果重复消息过多或者Rebalance暂停时间过长,会导致在Rebalance结束后瞬间有大量消息需要消费

4.Queue分配算法

同一个Topic下的queue只能由ConsumerGroup中的一个Consumer消费,具体queue的分配策略在Consumer创建时指定,常见的四种策略如下

平均分配策略

RocketMQ_第52张图片

计算平均值avg = Queue数量 / Consumer数量,如果能整除按顺序逐个分配给消费者,否则将多余的queue按照顺序逐个分配

环形平均算法

RocketMQ_第53张图片

在Queue队列组成的环形图上按照消费者顺序逐个分配

一致性Hash算法

RocketMQ_第54张图片

将每个queue的id进行hash、每个consumer的id进行hash然后放在hash环上,按照顺时针方向,距离queue最近的consumer就是将消费这个queue

同机房策略

根据queue部署机房位置和consumer位置,过滤出当前consumer同机房的queue,然后按照平均分配算法或者环形平均算法进行分配queue

总结

平均分配策略和环形分配策略分配效率更高,一致性Hash算法分配消息较低,而且很可能会导致分配不均匀,但是能够很大程度上减少Rebalance的影响(较少的queue会进行Rebalance)

5.订阅关系一致性

同一个消费者组下所有消费者实例所订阅的Topic、Tag必须完全一致。如果订阅关系(消费者组名-Topic-Tag)不一致,会导致消费消息紊乱,甚至消息丢失。

正确订阅

同一个ConsumerGroup下的Consumer订阅同一个Topic、同一个Tag

RocketMQ_第55张图片

同一个ConsumerGroup下的Consumer订阅同一个Topic、多个相同Tag

RocketMQ_第56张图片

同一个ConsumerGroup下的Consumer订阅多个相同Topic、多个相同Tag

RocketMQ_第57张图片

错误订阅

同一个ConsumerGroup下的Consumer订阅不同的Topic

RocketMQ_第58张图片

同一个ConsumerGroup下的Consumer订阅同一个Topic,但Tag不相同

RocketMQ_第59张图片

6.消费位点

记录对于Topic下所有queue,消费者组的消费进度。在集群模式下,消费位点是由客户端提给交服务端保存的,在广播模式下,消费位点是由客户端自己保存的

RocketMQ_第60张图片

每个队列都会记录自己的最小位点、最大位点,Consumer在消费完一批消息后,会将消费进度提交给Broker,Broker返回的ACK中包含queue的最小位点(minOffset)、最大位点(maxOffset)以及下次消费的起始位置(nextBeginOffset)

RocketMQ_第61张图片

  • CONSUME_FROM_LAST_OFFSET:从queue当前最后一条消息开始消费

  • CONSUME_FROM_FIRST_OFFSET:从queue第一条消息开始消费

  • CONSUME_FROM_TIMESTAMP:从指定的具体时间戳位置开始消费

7.消息幂等

由于消息可能出现重复,需要使得重复消费和消费一次的结果相同,并且对于系统没有影响,即消息幂等

同一条消息重复情况

  1. 发送时消息重复:当消息成功发送到broker并完成持久化,由于网络原因,Producer未能收到Broker的ACK,导致Producer认为消息发送失败而重新发送该消息

  2. 消费时消息重复:当Consumer已经消费完一条消息,由于网络原因,Broker未能收到消费成功响应,Broker会重新投递该消息

  3. Rebalance时消息重复

解决方案

幂等令牌:唯一业务标识的字符串

  1. 查询缓存是否存在该幂等令牌,判断本次操作是否为重复操作。如果缓存命中,则是重复操作;否则(可能是Key过期),执行下一步

  2. 通过幂等令牌作为主键,查询数据库是否存在相应数据,判断本次操作是否为重复操作。如果存在则是重复操作;否则,执行下一步

  3. 同一事务中完成三项操作:业务操作、将幂等令牌写入缓存、将幂等令牌作为主键的数据写入数据库

8.消息堆积和消费延迟

当Consumer的消费速度跟不上Producer的发送速度时,就会导致RocketMQ中的消息越来越多,即消息堆积,进而会导致消费延迟

消费者消费消息分为拉取消息和消费消息两个过程,由于拉取消息过程吞吐量很高,一般是在消费消息过程导致消息堆积,而消费者的消费消息的能力由消费耗时消费并发度决定,所以解决消息堆积问题需要先降低消费耗时再适当增加消费并发度

消费耗时

影响消费耗时的主要因素是代码逻辑,其中处理时间较长的包括CPU内部计算代码(复杂递归和循环)`外部I/O操作,其中外部I/O操作包括:

  1. 外部数据库读写,Mysql、Redis

  2. RPC远程服务调用(下游系统出现服务异常等情况)

消费并发度

对于顺序消息,消费并发度 = Topic分区queue的数量,而对于其他消息,消费并发度 = 单节点线程数 * 节点数量

单机线程数计算(理想情况下):

单节点最优线程数 = CPU核数 * (CPU内部逻辑计算耗时 + 外部I/O操作耗时) / CPU内部逻辑计算耗时

注意

实际生产环境中,建议先设置一个比理想线程数较小的值,进行压测查看系统运行情况,然后逐步增大线程数继续压测,直至找到最优的线程数为止

9.消息清理

消息在被消费后不会直接被清理掉,那样效率太低,而是以commitlog文件为单位进行清理

commitlog文件存在过期时间,默认为72小时(3天),broker.conf:

 
  

 
  
fileReservedTime=72

除了手动清理外,commitlog文件也会自动清理,无论文件中的消息是否被消费:

  • 文件过期,并且达到清理时间点(默认凌晨4点)

  • 文件过期,并且磁盘空间占用率达到过期清理警戒线(默认75%)

  • 磁盘空间占用率达到清理警戒线(默认85%),会从最老的文件开始清理

  • 磁盘空间占用率达到系统危险警戒线(默认90%),Broker将拒绝写入

 
  

 
  
deleteWhen=04 //磁盘文件空间充足情况下每天什么时候执行删除过期文件, 默认凌晨4点
diskMaxUsedSpaceRatio=75 //如果commitlog目录所在的分区使用比例大于该值, 则触发过期文件删除

10.延时消息详解

延时消息的延时时长不支持任意时长,只能指定定义好的延迟等级

RocketMQ默认支持18个延迟等级

RocketMQ_第62张图片

通过broker.conf设置:

 
  

 
  
//默认值
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
//自定义1天
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 1d

延时原理

RocketMQ_第63张图片

  1. Producer将延时消息发送到Broker,Broker将消息写入Commitlog文件中

  2. Broker同时会将消息分发到对应的consumequeue中,因为消息设置了延时等级,需要先修改消息的Topic为SCHEDULE_TOPIC_XXXX,如果queue不存在时,在该主题下创建延时等级相应的queueId目录(queueId = 延时等级 - 1),修改消息索引条目的消息Tag HashCode为投递时间(投递时间 = 消息发送到Broker的时间 + 延时等级时间) ,最后将消息索引写入相应的consumequeue中

    RocketMQ_第64张图片

  3. Broker内部存在一个延时消息服务类(ScheduleMessageService),内部使用定时器,如果从SCHEDULE_TOPIC_XXXX拉取到的消息到期,它才会进行消费

  4. 先读取Commitlog中原来写入的延时消息,将消息的延时等级设置为0(即一条普通消息),然后将消息再次投递到目标Topic,重新写入Commitlog文件

  5. Broker同时形成新的索引条目分发到目标Topic的consumequeue中

  6. 消费者进行消费

(SCHEDULE_TOPIC_XXXX中的队列只会创建具体使用过的延时等级相应的队列,不会一次性创建全部延时等级对应的队列)

11.消息重试

消息重试分为生产者的发送重试消费者的消费重试

对于发送重试而言,需要注意:

  • 如果采用同步或者异步方式发送,发送消息是失败会重试,而对于单向发送方式失败后不会重试

  • 如果发送的消息为顺序消息失败后不会重试

  • 发送消息重试机制可以保证消息不丢失,但是很可能会导致消息重复,这无法避免。只能在消费者消费时通过唯一标识对消息是否重复消费进行判断来解决

消息发送重试策略

  1. 同步发送失败策略:对于普通消息,默认采用轮询方式发送到对应的队列中,默认重试2次,在重试时尽量不会选择上次发送失败的Broker,如果只有一个Broker则不会选择上次发送失败的Queue。

     
         

     
         
    producer.setRetryTimesWhenSendFailed(2); //可以修改同步发送重试次数 默认为2次

    如果超过重试次数,则会抛出异常,说明该消息已经丢失了

  2. 异步发送失败策略:异步发送消息失败后,仅会在同一个Broker上进行重试,无法保证消息不丢失

     
         

     
         
    producer.setRetryTimesWhenSendAsyncFailed(2);//可以修改异步发送重试次数 默认为2次
  3. 消息刷盘失败策略:消息刷盘超时或Slave不可用(返回状态不是SEND_OK时),默认不会将消息尝试发送到另外的Broker,但可以设置修改配置使其支持

     
         

     
         
    producer.setRetryAnotherBrokerWhenNotStoreOK(true); //默认值为false

对于消息消费重试,不同的消息处理方式不同

顺序消息消费重试

当顺序消息消费失败后,为保证消息的顺序性,会自动不断地进行重试,直到消费成功。重试间隔时间默认为1s,重试期间会出现消息阻塞情况

 
  

 
  
consumer.setSuspendCurrentQueueTimeMillis(1000); //可以修改重试间隔时间 默认为1000 范围[10, 30000]

无序消息的消费重试

对于普通消息、延时消息和事务消息,当消息消费失败时,可以设置返回状态达到重试的结果。只有消费者模式为集群消费时支持重试,广播消费模式不支持重试

集群消费模式下,对于无序消息的消费重试,默认最多重试16次,每次重试时间间隔如下

重试次数 间隔时间 重试次数 间隔时间
1 10s 9 7m
2 30s 10 8m
3 1m 11 9m
4 2m 12 10m
5 3m 13 20m
6 4m 14 30m
7 5m 15 1h
8 6m 16 2h
 
  

 
  
consumer.setMaxReconsumeTimes(-1); //修改最大重试次数 默认为-1即16次

注意

  • 当设置的最大重试次数超过16次时,间隔时间均为2小时。并且只要修改了一个Consumer的最大重试次数就会应用到整个消费者组中的所有实例

  • 如果一条消息在重试次数超过最大重试次数后任然消费失败,则消息会被投递到死信队列

对于需要重试的消息,会将这些消息放入一个特殊Topic的队列中,之后再次消费,这个队列是重试队列,一般Topic名称命名为%RETRY%ConsumerGroup@ConsumerGroup

RocketMQ_第65张图片

重试消息的重试间隔时间和延时等级相似,因为它是通过延时消息实现的。

  1. 先将需要重试的消息保存在SCHEDULE_TOPIC_XXXX 延迟队列中

  2. 当间隔时间到期时,会将消息投递到 %RETRY%ConsumerGroup@ConsumerGroup重试队列中

消费普通消息重试(源码片段解读)

 
  

 
  
//并发消费状态
public enum ConsumeConcurrentlyStatus {
    //消费成功
    CONSUME_SUCCESS,
    //稍后重试
    RECONSUME_LATER;
}
 
  

 
  
//org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer类中的成员内部类
public class DefaultMessageListenerConcurrently implements MessageListenerConcurrently {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) {
        //批量消费消息
        for (MessageExt messageExt : msgs) {
            log.debug("received msg: {}", messageExt);
            try {
                long now = System.currentTimeMillis();
                rocketMQListener.onMessage(doConvertMessage(messageExt));
                long costTime = System.currentTimeMillis() - now;
                log.debug("consume {} cost: {} ms", messageExt.getMsgId(), costTime);
            } catch (Exception e) {
                //消费异常时 设置重试策略 返回RECONSUME_LATER
                log.warn("consume message failed. messageExt:{}", messageExt, e);
                context.setDelayLevelWhenNextConsume(delayLevelWhenNextConsume);
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
        }
        //消费成功返回CONSUME_SUCCESS
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
}
/**
* 消息重试策略
* -1 不重试直接放入DLQ(死信)队列
*  0 Broker控制重试间隔时间
* >0 客户端控制重试间隔时间
*/
private int delayLevelWhenNextConsume = 0;

消费顺序消息重试(源码片段解读)

 
  

 
  
public enum ConsumeOrderlyStatus {
    //消费成功状态
    SUCCESS,
    //过时
    @Deprecated
    ROLLBACK,
    @Deprecated
    COMMIT,
    //暂时阻塞队列 不断重试消费消息状态
    SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
 
  

 
  
//org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer类中的成员内部类
public class DefaultMessageListenerOrderly implements MessageListenerOrderly {
    @Override
    public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) {
        //顺序消费消息
        for (MessageExt messageExt : msgs) {
            log.debug("received msg: {}", messageExt);
            try {
                long now = System.currentTimeMillis();
                rocketMQListener.onMessage(doConvertMessage(messageExt));
                long costTime = System.currentTimeMillis() - now;
                log.info("consume {} cost: {} ms", messageExt.getMsgId(), costTime);
            } catch (Exception e) {
                //消费异常时 设置阻塞时间不断重试消费 返回SUSPEND_CURRENT_QUEUE_A_MOMENT
                log.warn("consume message failed. messageExt:{}", messageExt, e);
              context.setSuspendCurrentQueueTimeMillis(suspendCurrentQueueTimeMillis);
                return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
            }
        }
 
  
        //消费成功返回SUCCESS
        return ConsumeOrderlyStatus.SUCCESS;
    }
}

12.死信队列

当超过最大重试次数后,即消费者无法正常消费该消息时,会将消息投递到死信队列(DLQ),一般命名为%DLQ%ConsumerGroup@ConsumerGroup,其中的消息称为死信消息(DLM)。死信消息有效期和正常消息一样,默认3天后会被清理。

出现死信消息代表消费者无法正常消费,代码中出现了Bug,需要手动处理,将死信消息再次投递消费。

你可能感兴趣的:(消息队列,java-rocketmq,rocketmq)