入门教程


入门教程以 windows 系统为例,讲解如何在本地启动一个 RocketMQ ,然后通过控制台对其进行监控,最后编写客户端 demo 程序演示收发消息。

本地启动


下载

首先,前往RocketMQ下载地址进行下载。

解压后的目录结构如下:

rocketmq docker 安装教程 rocketmq入门教程_服务端

bin 目录下存放的是命令文件,这些命令文件可以控制 RocketMQ 的启停操作等;conf 目录下是配置文件;lib 目录下是必要的依赖文件。

bin 目录下的部分命令文件如下所示:

rocketmq docker 安装教程 rocketmq入门教程_服务端_02

这些命令文件通常有两个版本,一个是以 .cmd 后缀结尾的,它们用于在 windows 系统上使用;另一个是不带任何后缀的,它们用于在 linux 系统上使用。

配置环境变量

在 windows 系统上通过命令行启动 RocketMQ 需要配置环境变量,首先需要在环境变量中添加一个名为 ROCKETMQ_HOME 的变量,它的值是 RocketMQ 的解压路径。

rocketmq docker 安装教程 rocketmq入门教程_数据_03

然后,需要把 bin 目录的路径添加到 Path 变量中:

rocketmq docker 安装教程 rocketmq入门教程_数据_04

修改命令文件

由于 RocketMQ 的命令文件默认的把运行所需的内存设置的很大,会出现因本地内存不足而无法启动的情况,所以需要根据实际情况适当的调整内存配置。

对于 windows 系统来说,需要修改的文件是 runserver.cmd 和 runbroker.cmd ;对于 linux 系统来说,需要修改的文件是 runserver.sh 和 runbroker.sh 。

runserver.cmd 文件修改内容如下:

set "JAVA_OPT=%JAVA_OPT% -server -Xms256m -Xmx256m -Xmn128m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"

runbroker.cmd 文件修改内容如下:

set "JAVA_OPT=%JAVA_OPT% -server -Xms256m -Xmx256m -Xmn128m"

启动

完成上述步骤之后,即可使用命令行启动 RocketMQ 。

首先,启动 nameserver :

mqnameserv

然后,启动 broker :

mqbroker -n 127.0.0.1:9876

nameserver 默认占用的端口号是 9876,在 broker 启动时使用 -n 参事指定将自己注册到 127.0.0.1:9876 ,也即是注册到 nameserver 上。

关闭

RocketMQ 提供了专门的关闭脚本——mqshutdown:

# 关闭 NameServer
mqshutdown namesrv
# 关闭 Broker
mqshutdown broker

控制台


在 github 上,托管着一个名为 rocketmq-externals 的开源项目,这个项目中包含着许多针对 RocketMQ 的扩展程序,rocketmq-console 就是其中一个。

rocketmq-console 是一个控制台程序,它可以监控 RocketMQ 集群,并把信息展示在 web 页面上。

通过 git 将 rocketmq-externals 项目拉取到本地,然后进入到 rocketmq-console 目录下。

首先,修改配置文件 src/main/resources/application.properties ,指定 nameserver 的地址:

rocketmq.config.namesrvAddr=127.0.0.1:9876

然后,使用 maven 将代码打包:

mvn clean package -Dmaven.test.skip=true

打包后,jar 包文件会保存在 target 目录下。

最后,启动项目:

java -jar rocketmq-console-ng-1.0.0.jar

demo 程序


上述步骤都只是对服务端进行操作,接下来,将编写客户端 demo 程序来使用服务端。

引入依赖

首先,在项目中引入依赖:

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.4.0</version>
</dependency>

注意:客户端依赖的版本需要和服务端版本一致。

生产者

生产者是消息产生的地方,消息产生后,生产者会将其发送到 broker 。下面的示例代码中,创建了一条内容为 Hello RocketMQ 的消息:

package com.luzi.test;

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;

import java.nio.charset.StandardCharsets;

public class ProducerTestMain {

    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
        
        // 创建生成者对象并初始化组名
        DefaultMQProducer producer = new DefaultMQProducer("TestGroup");
        // 配置 namesrv 的地址
        producer.setNamesrvAddr("127.0.0.1:9876");
        // 配置消息发送超时时间
        producer.setSendMsgTimeout(60000);
        // 启动生产者
        producer.start();

        // 创建消息对象,三个参数依次指定topic、tag、消息内容
        Message message = new Message("TestTopic", "TestTag", "Hello RocketMQ".getBytes(StandardCharsets.UTF_8));
        // 使用生产者发送消息 
        SendResult sendResult = producer.send(message);
        // 打印消息发送的结果
        System.out.println(sendResult);

        // 关闭生产者
        producer.shutdown();
    }
}

消费者

消费者从 broker 获取消息并对消息进行处理。下面的示例代码中,消费者获取到消息之后,直接将其打印到控制台上:

package com.luzi.test;

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 org.apache.rocketmq.common.protocol.heartbeat.MessageModel;

import java.util.List;

public class ConsumerTestMain {

    public static void main(String[] args) throws MQClientException {

        // 实例化消息生产者,指定组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TestGroup");
        // 配置 namesrv 的地址
        consumer.setNamesrvAddr("127.0.0.1:9876");
        // 订阅 Topic
        consumer.subscribe("TestTopic", "*");
        // 配置消费模式:负载均衡
        consumer.setMessageModel(MessageModel.CLUSTERING);

        // 注册回调函数,用于处理接收到的消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages,
                                                            ConsumeConcurrentlyContext context) {
                for (MessageExt message : messages) {
                    System.out.println(message);
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        // 启动消费者
        consumer.start();
        System.out.println("Consumer Started.");
    }
}

服务端


RocketMQ 服务端主要包含 NameServer 和 Broker 两种组件。为了保证服务的稳定性和高可用性,NameServer 和 Broker 需要分别组成集群。

集群介绍


下图展示了一个 RocketMQ 集群中所需的四种组件,并使用箭头表示了它们之间的关系:

rocketmq docker 安装教程 rocketmq入门教程_服务端_05

  • Producer:集群中消息的生产者,是消息最初产生的地方。
  • Consumer:集群中消息的消费者,是消息最终被消费的地方。
  • Broker:在消息在集群中传输的过程中,负责接收来自生产者产生的消息,并暂时存储消息,最终将消息发送至消费者。
  • NameServer:负责管理 Broker 。集群中的 Broker 需要将自己的信息注册到 NameServer 上,Producer 和 Consumer 分别从 NameServer 上获取 Broker 的信息以完成消息的发送和接收。

启动 RocketMQ 的顺序是先启动 NameServer ,再启动 Broker ,这时候消息队列已经可以停供服务了,想发送消息就使用 Producer 来发送,想接收消息就使用 Consumer 来接收。很多应用程序既要发送,又要接收,可以启动多个 Producer 和 Consumer 来发送多种消息,同时接收多种消息。

为了消除单点故障,增加可靠性或增大吞吐量,可以在多台机器上部署多个 NameServer 和 Broker ,为每个 Broker 部署一个或多个 Slave 。

了解了四种角色以后 , 再介绍一下 Topic 和 Message Queue 这两个名词 。

一个分布式消息队列中间件部署好以后,可以给很多个业务提供服务,同一个业务也有不同类型的消息要投递,这些不同类型的消息以不同的 Topic 名称来区分。所以发送和接收消息前,先创建 Topic ,针对某个 Topic 发送和接收消息。有了 Topic 以后,还需要解决性能问题。

如果一个 Topic 要发送和接收的数据量非常大,需要能支持增加并行处理的机器来提高处理速度,这时候一个 Topic 可以根据需求设置一个或多个 Message Queue, Message Queue 类似分区或 Partition 。 Topic 有 了 多个 Message Queue 后,消息可以并行地向各个Message Queue 发送,消费者也可以并行地从多个 Message Queue 读取消息并消费。

集群特点

  • NameServer是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
  • Broker部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。
  • Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
  • Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。

集群模式

单Master模式

这种方式风险较大,一旦Broker重启或者宕机时,会导致整个服务不可用。不建议线上环境使用,可以用于本地测试。

多Master模式

一个集群无Slave,全是Master,例如2个Master或者3个Master,这种模式的优缺点如下:

  • 优点:配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高;
  • 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响。
多Master多Slave模式(异步)

每个Master配置一个Slave,有多对Master-Slave,HA采用异步复制方式,主备有短暂消息延迟(毫秒级),这种模式的优缺点如下:

  • 优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时Master宕机后,消费者仍然可以从Slave消费,而且此过程对应用透明,不需要人工干预,性能同多Master模式几乎一样;
  • 缺点:Master宕机,磁盘损坏情况下会丢失少量消息。
多Master多Slave模式(同步)

每个Master配置一个Slave,有多对Master-Slave,HA采用同步双写方式,即只有主备都写成功,才向应用返回成功,这种模式的优缺点如下:

  • 优点:数据与服务都无单点故障,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高;
  • 缺点:性能比异步复制模式略低(大约低10%左右),发送单个消息的RT会略高,且目前版本在主节点宕机后,备机不能自动切换为主机。

NameServer


NameServer 是整个消息队列中 的状态服务器,集群的各个组件通过它来了解全局的信息 。 同时午,各个角色的机器都要定期 向 NameServer 上报自己的状态,超时不上报的话, NameServer 会认为某个机器出故障不可用了,其他的组件会把这个机器从可用列表里移除 。

NamServer 可以部署多个,相互之间独立,其他角色同时向多个 NameServer 机器上报状态信息,从而达到热备份的目的。 NameServer 本身是无状态的,也就是说 NameServer 中的 Broker 、 Topic 等状态信息不会持久存储,都是由各个角色定时上报并存储到内存中的(NameServer 支持配置参数的持久化,一般用不到)。

Broker


Broker 是 RocketMQ 的核心,大部分“重量级”工作都是由 Broker 完成的,包括接收 Producer 发过来的消息、处理 Consumer 的消费消息请求、消息的持久化存储、消息的 HA 机制以及服务端过滤功能等 。

存储机制

分布式队列因为有高可靠性的要求,所以数据要通过磁盘进行持久化存储。

RocketMQ 利用“零拷贝”技术,提高了消息存盘和网络发送的速度。

RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成的,消息真正的物理存储文件是 CommitLog,ConsumeQueue 是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。 每个 Topic 下的每个 MessageQueue 都有一个对应的 ConsumeQueue 文件 。

CommitLog 以物理文件的方式存放,每台 Broker 上的 CommitLog 被本机器所有 ConsumeQueue 共享。在 CommitLog 中,一个消息的存储长度是不固定的,RocketMQ 采取一些机制,尽量向 CommitLog 中顺序写,但是随机读。ConsumeQueue 的内容也会被写到磁盘里作持久存储。

存储机制这样设计有以下几个好处:

  • CommitLog 顺序写,可以大大提高写入效率。
  • 虽然是随机读,但是利用操作系统的 pageCache 机制,可以批量的从磁盘读取,作为 cache 存到内存中,加速后续的读取速度。
  • 为了保证完全的顺序写,需要 ConsumeQueue 这个中间结构,因为 ConsumeQueue 里只存偏移量信息,所以尺寸是有限的,在实际情况中,大部分的 ConsumeQueue 能够被全部读入内存,所以这个中间结构的操作速断很快,可以认为是内存读取的速度。此外为了保证 CommitLog 和 ConsumeQueue 的一致性,CommitLog 里存储了 ConsumeQueues 、MessageKey、Tag 等所有信息,即使 ConsumeQueue 丢失,也可以通过 commitLog 完全恢复出来。

总结下来就是:RocketMQ 基于“顺序写”“随机读”的原则来设计,利用“零拷贝”技术,克服了磁盘操作的瓶颈 。

高可用性机制

RocketMQ 分布式集群是通过 Master 和 Slave 的配合达到高可用性的。Master 角色的 Broker 支持读和写, Slave 角色的 Broker 仅支持读,也就是 Producer 只能和 Master 角色的 Broker 连接 写人消息; Consumer 可以连接 Master 角色的 Broker ,也可以连接 Slave 角色的 Broker 来读取消息。

在 Consumer 的配置文件中,并不需要设置是从 Master 读还是从 Slave 读,当 Master 不可用或者繁忙的时候, Consumer 会被自动切换到从 S lave 读。有了自动切换 Consumer 这种机制,当一个 Master 角色的机器出现故障后,Consumer 仍然可以从 Slave 读取消息,不影响 Consumer 程序。 这就达到了消费端的高可用性。

为了达到发送端的高可用性,可以在创建 Topic 的时候,把 Topic 的多个 MessageQueue 创建在多个 Broker 组上(相同 Broker 名称,不同 brokerId 的机器组成一个 Broker 组),这样当一个 Broker 组的 Master 不可用后,其他 Maseter 任然可用,Producer 仍然可以发送消息。RocketMQ 目前还不支持把 Salve 自动转成 Master ,如果机器资源不足,需要把 Slave 转成 Master ,则要手动停止 Slave 角色的 Broker ,更改配置文件,用新的配置文件启动 Broker 。

刷盘和复制

RocketMQ 的消息是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。 RocketMQ 为了提高性能,会尽可能地保证磁盘的顺序写。 消息在通过 Producer 写人 RocketMQ 的时候,有两种写磁盘方式:

  • 异步刷盘方式:在返回写成功状态时 ,消息可能只是被写人了内存的PAGECACHE ,写操作的返回快,吞吐量大 ;当内存里的消息量积累到一定程度时 ,统一触发写磁盘动作,快速写人 。
  • 同步刷盘方式:在返回写成功状态时,消息已经被写人磁盘 。 具体流程是,消息、写入内存的 PAGECACHE 后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态 。

如果一个 Broker 组有 Master 和 Slave,消息需要从 Master 复制到 Slave 上,有同步和异步两种复制方式 。 同步复制方式是等 Master 和 Slave 均写成功后才反馈给客户端写成功状态;异步复制方式是只要 Master 写成功即可反馈给客户端写成功状态 。

这两种复制方式各有优劣,在异步复制方式下,系统拥有较低的延迟和较高的吞吐量,但是如果 Master 出了故障,有些数据因为没有被写人 Slave ,有可能会丢失;在同步复制方式下,如果 Master 出故障, Slave 上有全部的备份数据,容易恢复,但是同步复制会增大数据写人延迟,降低系统吞吐量。

实际应用中要结合业务场景,合理设置刷盘方式和主从复制方式,尤其是 SYNC FLUSH 方式,由于频繁地触发磁盘写动作, 会明显降低性能。 通常情况下,应该把 Master 和 Save 配置成 ASYNC FLUSH 的刷盘方式,主从之间配置成 SYNC MASTER 的复制方式,这样即使有一台机器出故障, 仍然能保证数据不丢,是个不错的选择 。

配置文件


使用命令行启动 Broker 时,可以使用 -c 参数指定配置文件:

sh mqbroker -c my-conf.properties

使用 -m 参数可以查看 Broker 的默认配置:

sh mqbroker -m

Broker 的配置文件实际上是一个键值对(.properties)文件,下面介绍可配置的属性:

  • namesrvAddr:指定 NameServer 的地址(域名或IP加端口号),多个地址之间使用分号分隔。如:192.168.100.131:9876;192.168.100.132
  • brokerClusterName:指定 Broker 所属集群的名字。
  • brokerName:指定 Broker 的名字。Master 和 Slave 通过使用相同的 brokerName 来表明相互关系,以说明某个 Salve 隶属于哪个 Master 。
  • brokerId:整数,值为 0 表示角色为 Master,大于 0 表示角色为 Slave。
  • brokerRole:指定 Broker 的角色。可配置的值有:

ASYNC_MASTER:异步复制 Master 。先返回发送成功的消息,再将消息从 Master 同步到 Slave 。

SYNC_MASTER:同步双写 Master 。当 Slave 和 Master 消息同步完成后,再返回发送成功的消息。

SLAVE:从 Broker。

  • flushDiskType:指定刷盘方式。可配置的值有:

ASYNC_FLUSH:异步刷盘。消息写入 page_cache 后就返回成功状态。

SYNC_FLUSH:同步刷盘。消息真正写入磁盘后再返回成功状态。

  • brokerIP1:指定 Broker 所处的IP地址(实际上 Broker 自己可以进行自动探测,但有时并不准确,需要手动配置)。
  • listenPort:指定 Broker 监听的端口号。如果一台机器上启动了多个 Broker ,则要设置不同的端口号,避免冲突。
  • defaultTopicQueueNums:整数,指定创建 Topic 时默认创建的队列数。
  • autoCreateTopicEnable:是否允许 Broker 自动创建 Topic 。建议线下开启,线上关闭。
  • autoCreateSubscriptionGroup:是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭。
  • deleteWhen:删除文件的时间点,默认凌晨4点。
  • fileReservedTime:整数,文件保留的时间,单位是小时,默认72个小时。
  • mapedFileSizeCommitLog:整数,每个 commitLog 文件大小的上限,以 B 为单位,默认1G。
  • mapedFileSizeConsumeQueue:整数,每个 ConsumeQueue 文件保存消息数量的上限,默认 30万 条。
  • storePathRootDir:指定 RocketMQ 文件存储的根路径。
  • storePathCommitLog:指定 commitLog 文件的存储路径。
  • storePathConsumeQueue:指定 ConsumeQueue 文件的存储路径。
  • storePathIndex:指定消息索引的存储路径。
  • storeCheckpoint:指定 checkPoint 文件的存储路径。
  • abortFile:指定 abort 文件的存储路径。

这些配置参数,在 Broker 启动的时候生效,如果启动后有更改,要重启Broker 。 现在使用 云服务或多 阿卡的机器比较普遍, Broker 自动探测获得的 ip 地址可能不符合要求,通过 brokerIPl =47 .98.41.234 这样的配置参数,可以设置 Broker 机器对外暴露的 ip 地址 。

客户端


生产者和消费者是消息队列的两个重要角色,生产者向消息队列写入数据,消费者从消息队列里读取数据, RocketMQ 的大部分用户只需要和生产者、消费者打交道。

客户端的主要内容也和生产者和消费者有关。

Message


在介绍生产者和消费者之前,我们需要先了解 Message 。Message 是生产者和消费者之间进行数据传输的载体。Message 将真正要传输的数据放在 body 字段种,并额外包含了很多便于对数据进行分类和处理的属性,下面分别介绍:

  • topic:消息的主题,根据这个属性可以把消息分类。
  • tags:消息的标签,这个属性用来过滤消息。
  • keys:消息的唯一标识。
  • delayTimeLevel:用于实现延时消息,指定消息延迟的时间。

注意:目前通过 delayTimeLevel 属性设置的延迟时间不支持任意值,仅支持预设值的时间长度 ( 1s/5s/10s/30s/1m/2m/3m/4m/5m/6m/7m/8m/9m/10m/20m/30m/1h/2h ) 。 比如 setDelayTimeLevel(3 ) 表示延迟 10s 。

消息的 Tag 和 Key

对一个应用来说,尽可能只用一个 Topic ,不同的消息子类型用 Tag 来标识(每条消息只能有一个 Tag ),服务器端基于 Tag 进行过滤,并不需要读取消息体的内容,所以效率很高。 发送消息设置了 Tag 以后,消费方在订阅消息时,才可以利用 Tag 在 Broker 端做消息过滤。

其次是消息的 Key。 对发送的消息设置好 Key ,以后可以根据这个 Key 来查找消息。 所以这个 Key 一般用消息在业务层面的唯一标识码来表示,这样后续查询消息异常,消息丢失等都很方便。 Broker 会创建专门的索引文件,来存储 Key 到消息的映射,由于是哈希索引,应尽量使 Key 唯一 ,避免潜在的哈希冲突 。

Tag 和 Key 的主要差别是使用场景不同, Tag 用在 Consumer 的代码中,用来进行服务端消息过滤, Key 主要用于通过命令行查询消息。

properties

在 Message 中定义了一个名为 properties 的 Map:

private Map<String, String> properties;

实际上,上面介绍的属性当中,一大部分都是以键值对的形式保存在这个 Map 中的。这包括:tags、keys、delayTimeLevel、buyerId、waitStoreMsgOK。

此外,Message 还提供了如下的两个方法供用户设置自定义的键值对:

public void putUserProperty(final String name, final String value) {...}
public String getUserProperty(final String name) {...}

用户自定义的属性主要用来过滤消息,这个在下文进行介绍。

Offset


实际运行中的系统,难免会遇到重新消费某条消息、跳过一段时间内的消息等情况。这些异常情况的处理,都和 Offset 有关。

首先来明确一下 Offset 的含义, RocketMQ 中, 一种类型的消息会放到一个 Topic 里,为了能够并行, 一般一个 Topic 会有多个 MessageQueue (也可以设置成一个), Offset 是指某个 Topic 下的一条消息在某个 MessageQueue 里的位置,通过 Offset 的值可以定位到这条消息,或者指示 Consumer 从这条消息开始向后继续处理 。

rocketmq docker 安装教程 rocketmq入门教程_apache_06

上图绘制了 Offset 之间的继承关系。Offset主要分为本地文件类型和 Broker 代存的类型两种。

OffsetStore 接口中定义的方法如下:

rocketmq docker 安装教程 rocketmq入门教程_数据_07

  • updateOffset:更新缓存中的 Offset 。
  • readOffset:读取 Offset,通过指定第二个参数,可以选择从缓存中读还是从硬盘(本地或者服务器)中读。
  • persist:将 Offset 进行持久化(对于 LocalFileOffsetStore 来说,持久化到本地;对于 RemoteBrokerOffsetStore 来说,持久化到服务器)。
  • persistAll:批量持久化。
  • load:载入 Offset ,当自行创建 OffsetStore 时可能会用到。

OffsetStore 使用 json 格式存储,简洁明了,下面是一个例子:

{
	"offsetTable":{{
			"brokerName":"broker-a",
			"queueId":2,
			"topic":"TestTopic"
		}:3,{
			"brokerName":"broker-a",
			"queueId":3,
			"topic":"TestTopic"
		}:0,{
			"brokerName":"broker-a",
			"queueId":0,
			"topic":"TestTopic"
		}:0,{
			"brokerName":"broker-a",
			"queueId":1,
			"topic":"TestTopic"
		}:0
	}
}

LocalFileOffsetStore 中使用常量 LOCAL_OFFSET_STORE_DIR 记录了本地文件的存储位置。

ClientConfig


如下图所示,RocketMQ 中内置的 Producer 和 Consumer 实现类都继承了 ClientConfig 类:

rocketmq docker 安装教程 rocketmq入门教程_apache_08

ClientConfig 是一个通用的客户端配置类,定义了通用的客户端属性,并为这些属性设置了默认值。例如:namesrvAddr 属性就是在这里定义的。

MQAdmin


如下图所示,RocketMQ 中内置的 Producer、Consumer 都继承了 MQAdmin 接口:

rocketmq docker 安装教程 rocketmq入门教程_服务端_09

MQAdmin 接口中定义的方法如下:

rocketmq docker 安装教程 rocketmq入门教程_apache_10

这些方法主要是针对 topic、offset 和 message 的操作。

消费者


根据使用者对读取操作的控制情况,消费者可分为两种类型 。 一个是 DefaultMQPushConsumer ,由系统控制读取操作,收到消息后自动调用回调函数来处理消息;另 一个是 DefaultMQPullConsumer ,读取操作中的大部分功能由使用者自主控制 。

下图绘制了 Consumer 之间的继承关系:

rocketmq docker 安装教程 rocketmq入门教程_数据_11

DefaultMQPushConsumer

使用 DefaultMQPushConsumer 主要是设置好各种参数和传入处理消息的函数。系统收到消息后自动调用回调函数来处理消息,自动保存 Offset,而且加入新的 DefaultMQPushConsumer 后会自动做负载均衡。

使用 DefaultMQPushConsumer 时无需关注怎样获取消息、如何保存 Offset 等繁琐的操作,只需要关注消息的处理逻辑即可。

参数配置

DefaultMQPushConsumer 需要设置三个参数: 一 是这个 Consumer 的 GroupName ,二是 NameServer 的地址和端口号,三是 Topic 的名称,下面将分别进行详细介绍。

GroupName

Consumer 的 GroupName 用 于把多个 Consumer 组织到一起 , 提高并发处理能力, GroupName 需要和消息模式 ( MessageModel )配合使用 。

RocketMQ 支持两种消 息模式 : Clustering 和 Broadcasting 。

  • 在 Clustering 模式下,同一个 ConsumerGroup ( GroupName 相同 ) 里的每个 Consumer 只消费所订阅消息的一部分内容, 同一个 ConsumerGroup 里所有的 Consumer 消费的内容合起来才是所订阅 Topic 内容的整体,从而达到负载均衡的目的 。
  • 在 Broadcasting 模式下,同一个 ConsumerGroup 里的每个 Consumer 都能消费到所订阅 Topic 的全部消息,也就是一个消息会被多次分发,被多个 Consumer 消费 。
NameServer

NameServer 的地址和端口号,可以填写多个 ,用分号隔开,达到消除单点故障的目的 , 比如 “ ip 1:port;ip2:port;ip3 :port ” 。

Topic

Topic 名称用来标识消息类型, 需要提前创建。 如果不需要消费某个 Topic 下的所有消息,可以通过指定消息的 Tag 进行消息过滤,比如:

consumer.subscribe("TestTopic", "tag1||tag2||tag3");

表示这个 Consumer 要消费 TestTopic 下带有 tag1 或 tag2 或 tag3 的消息(Tag 是在发送消息时设置的标签)。

在填写 Tag 参数的位置,用 null 或者 "*" 表示要消费这个 Topic 的所有消息:

consumer.subscribe("TestTopic", null);
consumer.subscribe("TestTopic", "*");
自动处理 Offset

对于 DefaultMQPushConsumer 来说,默认是 CLUSTERING 模式,也就是同一个 ConsumerGroup 里的多个消费者没人消费一部分,各自收到的消息内容不一样。这种情况下,由 Broker 端存储和控制 Offset 的值,使用 RemoteBrokerOffsetStore 结构。

在 DefaultMQPushConsumer 里的 BROADCASTING 模式下,每个 Consumer 都收到这个 Topic 的全部下消息,各个 Consumer 间相互没有干扰,RocketMQ 使用 LocalFileOffsetStore ,把 Offset 存到本地。

回调函数

DefaultMQPushConsumer 通过回调函数处理消息,每当获取到消息,都会创建一个新的线程进行处理。而这一部分的功能都定义在 MQPushConsumr 接口中:

rocketmq docker 安装教程 rocketmq入门教程_apache_12

MQPushConsumr 接口不仅定义了如何注册回调函数、如何订阅消息,也定义了线程的启动、停止、挂起、恢复等操作。

这里需要注意到,registerMessageListener 方法有两种重载形式。如果使用 MessageListenerConcurrently 作为监听器,那么 DefaultMQPushConsumer 将会并行的处理来自一个 MessageQueue 中的消息;如果 使用 MessageListenerOrderly 作为监听器,那么 DefaultMQPushConsumer 将会串行的处理来自一个MessageQueue 中的消息。

长轮询

虽然从名字上看,DefaultMQPushConsumer 使用的是 push 方式获取消息(即 broker 收到消息后立即推送给 Consumer ),但实际上,它使用的是长轮询的方式获取消息。

DefaultMQPushConsumer 底层使用的是长轮询的方式:Consumer 会定时的请求 Broker 获取新消息。Broker 接到新消息请求之后,如果队列中存在新消息,则立即返回;如果队列中没有新消息,则并不急于返回,而是通过一个循环不断查看状态,在每个循环中会等待一段时间(默认5秒),然后再次检查。当循环超时的时候,就返回空结果。在等待过程中,Broker 收到了新的消息后会直接返回请求结果。

rocketmq docker 安装教程 rocketmq入门教程_数据_13

长轮询的核心是, Broker 端 HOLD 住客户端过来的请求一小段时间,在这个时间内有新消息到达,就利用现有的连接立刻返回消息给 Consumer。 长轮询的主动权还是掌握在 Consumer 手中, Broker 即使有大量消息积压 ,也不会主动推送给 Consumer 。

长轮询方式的局限性是在 HOLD 住 Consumer 请求的时候需要占用资源,它适合用在消息队列这种客户端连接数可控的场景中 。

ProcessQueue

DefaultMQPushConsumer 有个线程池,消息处理逻辑在各个线程里同时执行。如果直接把消息提交到线程池里执行,将很难监控和控制,比如,如何得知当前消息堆积的数量?如何重复处理某些消息? 如何延迟处理某些消息?

RocketMQ 定义了 一个快照类 ProcessQueue 来解决这些问题,在PushConsumer 运行的时候, 每个 MessageQueue 都会有个对应的 ProcessQueue 对象,保存了这个 MessageQueue 消息处理状态的快照。

ProcessQueue 对象里主要的内容是一个 TreeMap 和 一个读写锁 。 TreeMap 里以 Message Queue 的 Offset 作为 Key ,以消息内容的引用为 Value ,保存了所有从 MessageQueue 获取到,但是还未被处理的消息; 读写锁控制着多个线程对 TreeMap 对象的并发访问 。

PushConsumer 会判断已获取但还未处理的消息个数、消息总大小、 Offset 的跨度,任何一个值超过设定的大小就隔一段时间再拉取消息,从而达到流量控制的目的 。 此外 ProcessQueue 还可以辅助实现顺序消费的逻辑。

启停

DefaultMQPushConsumer 的退出, 要调用 shutdown 函数, 以便释放资源、保存 Offset 等。 这个调用要加到 Consumer 所在应用的退出逻辑中。

PushConsumer 在启动的时候 ,会做各种配置检查,然后连接 NameServer 获取 Topic 信息,启动时如果遇到异常,比如无法连接 NameServer,程序仍然可以正常启动不报错(日志里有 WARN 信息)。 在单机环境下可以测试这种情况,启动 DefaultMQPushConsumer 时故意把 Name Server 地址填错,程序仍然可以正常启动,但是不会收到消息 。

这个特性和分布式系统的设计有关, RocketMQ 集群可以有多个 NameServer、Broker ,某个机器出异常后整体服务依然可用。所以 DefaultMQPushConsumer 被设计成当发现某个连接异常时不立刻退出,而是不断尝试重新连接。可以进行这样一个测试,在 DefaultMQPushConsumer 正常运行的时候,手动 kill 掉 Broker 或 NameS erver ,过一会儿再启动。会发现 DefaultMQPushConsumer 不会出错退出,在服务恢复后正常运行,在服务不可用的这段时间,仅仅会在日志里报异常信息。

如果需要在 DefaultMQPushConsumer 启动的时候,及时暴露配置问题,可以在 Consumer 启动之后调用如下的语句:

pushConsumer.fetchSubscribeMessageQueues("TopicName");

这时如果配置信息写得不准确,或者当前服务不可用,这个语句会报 MQC!ientException 异常。

DefaultMQPullConsumer

同 DefaultMQPushConsumer 一样,使用 DefaultMQPullConsumer 前需要先配置分组、消息模式、NameServer的地址等属性。不同的地方在于,DefaultMQPullConsumer 需要使用者手动的去获取消息,而不是像 DefaultMQPushConsumer 那样使用长轮询的方式自动的获取消息。

一般流程

下面先展示一段示例代码:

package com.luzi.test;

import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer;
import org.apache.rocketmq.client.consumer.PullResult;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

public class PullConsumerMain {

    private static  final Map<MessageQueue, Long> OFFSET_TABLE = new HashMap<>();

    public static void main(String[] args) throws MQClientException {
        // 创建一个 pullConsumer 对象,并指定它所属的组
        DefaultMQPullConsumer pullConsumer = new DefaultMQPullConsumer("PullConsumerGroup");
        // 配置 NameServer 的地址
        pullConsumer.setNamesrvAddr("127.0.0.1:9876");
        // 集群模式获取消息,可负载均衡
        pullConsumer.setMessageModel(MessageModel.CLUSTERING);
        // 启动 pullConsumer
        pullConsumer.start();
        // 从指定的 Topic 下获取 MessageQueue
        Set<MessageQueue> messageQueueSet = pullConsumer.fetchSubscribeMessageQueues("TestTopic");

        // 遍历所有的 MessageQueue
        for (MessageQueue messageQueue : messageQueueSet) {

            System.out.println("consume from the queue: " + messageQueue);
            
            // 跳转点
            SINGLE_MQ:
            while (true) {
                try {
                    // 拉取消息,四个参数分别指定 MessageQueue,Tag,Offset,最大消息数
                    // 这个方法会阻塞程序直到获取到消息
                    PullResult pullResult = pullConsumer.pullBlockIfNotFound(messageQueue, null, getMessageQueueOffset(messageQueue), 32);
                    System.out.println(pullResult);
                    // 记录 MessageQueue 中下一条消息偏移量
                    putMessageQueueOffset(messageQueue, pullResult.getNextBeginOffset());
                    // 判断服务端返回的状态
                    switch (pullResult.getPullStatus()) {
                        case FOUND: break;
                        case NO_MATCHED_MSG: break;
                        case NO_NEW_MSG: break SINGLE_MQ;
                        case OFFSET_ILLEGAL: break;
                        default: break;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        
        // 关闭 pullConsumer
        pullConsumer.shutdown();
    }

    // 获取 MessageQueue 的偏移量
    private static long getMessageQueueOffset(MessageQueue messageQueue) {
        Long offset = OFFSET_TABLE.get(messageQueue);
        return Objects.nonNull(offset) ? offset : 0L;
    }

    // 记录 MessageQueue 的偏移量
    private static void putMessageQueueOffset(MessageQueue messageQueue, long offset) {
        OFFSET_TABLE.put(messageQueue, offset);
    }

}

使用 DefaultMQPullConsumer 拉取消息需要做如下几件事情:

  • 创建 DefaultMQPullConsumer 对象。
  • 进行参数配置并启动。
  • 从指定的 Topic 下拉取 MessageQueue 。
  • 维护 MessageQueue 的偏移量(offset)。
  • 根据 MessageQueue 和偏移量请求服务端获取消息(可以指定 tag)。
  • 根据服务端返回的状态做不同的操作。
  • 业务流程处理完毕之后关闭。

MessageQueue 中并没有保存消息,它只记录了 topic、brokerName 和 queueId,它的作用就相当于是一个令牌,拿着这个令牌就可以获取到真正的消息。

Consumer 向服务端请求消息之后,会得到一个 PullRequest 对象,PullRequest 中包含了请求的状态、offset 信息以及消息本身。

请求的状态被定义再一个名为 PullStatus 的枚举当中:

  • FOUND:表示获取到消息。
  • NO_NEW_MSG:表示没有新的消息。
  • NO_MATCHED_MSG:表示没有相匹配的消息。
  • OFFSET_ILLEGAL:无效的偏移量,太大或者太小。
拉取消息

在 MQPullConsumer 接口中定义了如下一组用来拉取消息的重载方法:

PullResult pull(final MessageQueue mq, final String subExpression, final long offset, final int maxNums);
PullResult pull(final MessageQueue mq, final String subExpression, final long offset, final int maxNums, final long timeout);
PullResult pull(final MessageQueue mq, final MessageSelector selector, final long offset, final int maxNums);
PullResult pull(final MessageQueue mq, final MessageSelector selector, final long offset, final int maxNums, final long timeout);
void pull(final MessageQueue mq, final String subExpression, final long offset, final int maxNums, final PullCallback pullCallback);
void pull(final MessageQueue mq, final String subExpression, final long offset, final int maxNums, final PullCallback pullCallback, long timeout);
void pull(final MessageQueue mq, final MessageSelector selector, final long offset, final int maxNums, final PullCallback pullCallback);
void pull(final MessageQueue mq, final MessageSelector selector, final long offset, final int maxNums, final PullCallback pullCallback, long timeout);
PullResult pullBlockIfNotFound(final MessageQueue mq, final String subExpression, final long offset, final int maxNums);
void pullBlockIfNotFound(final MessageQueue mq, final String subExpression, final long offset, final int maxNums, final PullCallback pullCallback);

下面针对这些参数进行介绍:

  • mq:消息队列。
  • subExpression:通过表达式过滤 tag 标签。
  • selector:功能更强大的过滤器。
  • offset:消息偏移量。
  • maxNums:最大读取数量。
  • timeout:阻塞超时时间。
  • pullCallback:异步读取成功后的回调函数。
处理 Offset

在使用 DefaultMQPushConsumer 的时候,我们不用 关 心 OffsetStore 的事 ,但是如果 PullConsumer ,我们就要自己处理 OffsetStore 了。 如果不对 Offset 进行持久化存储,就可能因为程序的异常或重启而丢失 Offset,在实际应用中不推荐这样做。

下面地示例展示了如何使用 OffsetStore 持久化存储偏移量:

package com.luzi.test.offset;

import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer;
import org.apache.rocketmq.client.consumer.PullResult;
import org.apache.rocketmq.client.consumer.PullStatus;
import org.apache.rocketmq.client.consumer.store.OffsetStore;
import org.apache.rocketmq.client.consumer.store.ReadOffsetType;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Set;

public class OffsetConsumer {

    public static void main(String[] args) throws Exception {
        
        // 创建一个 pullConsumer 对象,并指定它所属的组
        DefaultMQPullConsumer pullConsumer = new DefaultMQPullConsumer("PullConsumerGroup");
        // 配置 NameServer 的地址
        pullConsumer.setNamesrvAddr("127.0.0.1:9876");
        // 集群模式获取消息,可负载均衡
        pullConsumer.setMessageModel(MessageModel.CLUSTERING);
        // 启动 pullConsumer
        pullConsumer.start();
        // 从指定的 Topic 下获取 MessageQueue
        Set<MessageQueue> messageQueueSet = pullConsumer.fetchSubscribeMessageQueues("TestTopic");

        while (true) {
            // 遍历所有的 MessageQueue
            for (MessageQueue messageQueue : messageQueueSet) {


                // 读取偏移量,优先从内存中读
                OffsetStore offsetStore = pullConsumer.getOffsetStore();
                long messageQueueOffset = offsetStore.readOffset(messageQueue, ReadOffsetType.MEMORY_FIRST_THEN_STORE);

                // 读取消息
                PullResult pullResult = pullConsumer.pull(messageQueue, "*", messageQueueOffset, 1);

                // 记录 MessageQueue 中下一条消息偏移量
                offsetStore.updateOffset(messageQueue, pullResult.getNextBeginOffset(), true);
                // 持久化到 NameServer
                offsetStore.persist(messageQueue);

                // 打印
                if (pullResult.getPullStatus() == PullStatus.FOUND) {
                    List<MessageExt> msgFoundList = pullResult.getMsgFoundList();
                    System.out.println("messageQueueOffset: " + messageQueueOffset);
                    System.out.println("consume from the queue: " + messageQueue);
                    System.out.println(pullResult);
                    for (MessageExt messageExt : msgFoundList) {
                        System.out.println(new String(messageExt.getBody(), StandardCharsets.UTF_8));
                    }
                    System.out.println();
                }

            }
        }
    }

}

DefaultMQPullConsumer 默认使用 RemoteBrokerOffsetStore 。

对于 PullConsumer 来说,使用者主动权很高,可以根据实际需要暂停、停止、启动消费过程。需要注意的是 Offset 的保存,要在程序的异常处理部分增加把 Offset 写入磁盘方面的处理,记准每一个 MessageQueue 的偏移量,才能保证消息消费的准确性。

生产者


RocketMQ 中内置的 Producer 的实现类有两个:DefaultMQProducer 和 TransactionMQProducer。其中,后者又是前者的子类。在非事务的场景当中,发送消息使用的都是 DefaultMQProducer ;在涉及到事务的场景当中,则需要使用 TransactionMQProducer 。

生产者向消息队列里写入消息,不同的业务场景需要生产者采用不同的写入策略。 比如同步发送、异步发送、 延迟发送、 发送事务消息等,下面具体介绍。

发送消息的一般流程

在发送消息之前,Producer 往往需要先配置一些属性:

  • groupName:分组名称。
  • instanceName:当一个 Jvm 需要启动多个 Producer 的时候,通过设置不同的 InstanceName 来区分,不设置的话系统使用默认名称 DEFAULT 。
  • namesrvAddr:NameServer 的地址。
  • retryTimesWhenSendFailed:发送失败重试次数,当网络出现异常的时候,这个次数影响消息的重复投递次数。想保证不丢消息,可以设置多重试几次。
  • sendMsgTimeout:发送消息超时时间。

完成属性配置之后,即可启动 Producer ,发送消息。业务流程结束之后,需要即时关闭 Producer 。下面展示一段简单的生产者示例代码:

package com.luzi.test;

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;

import java.nio.charset.StandardCharsets;

public class ProducerTestMain {

    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
        // 创建生成者对象并初始化组名
        DefaultMQProducer producer = new DefaultMQProducer("TestGroup");
        // 配置 namesrv 的地址
        producer.setNamesrvAddr("127.0.0.1:9876");
        // 配置消息发送超时时间
        producer.setSendMsgTimeout(60000);
        // 启动生产者
        producer.start();

        // 创建消息对象,三个参数依次指定topic、tag、消息内容
        Message message = new Message("TestTopic", "TestTag", "Hello RocketMQ".getBytes(StandardCharsets.UTF_8));
        // 使用生产者发送消息
        SendResult sendResult = producer.send(message);
        // 打印消息发送的结果
        System.out.println(sendResult);

        // 关闭生产者
        producer.shutdown();
    }
}

以不同的方式发送消息

Producer 用来发送消息的方法是一组定义在 MQProducer 接口中名为 send 的方法,这组方法有十数种重载形式,以适应各种不同的使用场景。

同步发送消息

如下的四种方法用于发送同步消息,根据需要,可以选择指定超时时间和 MessageQueue :

SendResult send(final Message msg);
SendResult send(final Message msg, final long timeout);
SendResult send(final Message msg, final MessageQueue mq);
SendResult send(final Message msg, final MessageQueue mq, final long timeout);
批量发送消息

这组用于批量发送消息的重载相较于上一组而言,只是把 Message 替换成了 Collection ,此外并无差别。

SendResult send(final Collection<Message> msgs);
SendResult send(final Collection<Message> msgs, final long timeout);
SendResult send(final Collection<Message> msgs, final MessageQueue mq);
SendResult send(final Collection<Message> msgs, final MessageQueue mq, final long timeout);
异步发送消息

异步发送消息需要设置一个回调函数,这个回调函数在 SendCallback 接口种定义:

void send(final Message msg, final SendCallback sendCallback);
void send(final Message msg, final SendCallback sendCallback, final long timeout);
void send(final Message msg, final MessageQueue mq, final SendCallback sendCallback);
void send(final Message msg, final MessageQueue mq, final SendCallback sendCallback, long timeout);

下面时 SendCallback 接口的定义:

public interface SendCallback {
    void onSuccess(final SendResult sendResult);
    void onException(final Throwable e);
}

实现 SendCallback 接口主要是实现两种逻辑:一个是消息发送成功时的处理逻辑;另一个是消息发送异常时的处理逻辑。

自定义消息发送规则

一个 Topic 会有多个 Message Queue ,如果使用 Producer 的 默认配置 ,这个 Producer 会轮流向各个 Message Queue 发送消息。 Consumer 在消费消息的时候,会根据负载均衡策略,消费被分配到的 Message Queue ,如果不经过特定的设置,某条消息被发往哪个 Message Queue ,被哪个 Consumer 消费是未知的。

在 MQProducer 接口中定义了一组接收 MessageQueueSelector 参数的方法:

SendResult send(final Message msg, final MessageQueueSelector selector, final Object arg);
SendResult send(final Message msg, final MessageQueueSelector selector, final Object arg, final long timeout);
void send(final Message msg, final MessageQueueSelector selector, final Object arg, final SendCallback sendCallback);
void send(final Message msg, final MessageQueueSelector selector, final Object arg, final SendCallback sendCallback, final long timeout)

这组方法借助于 MessageQueueSelector 可以实现不同的发送策略。

MessageQueueSelector 接口中只定义了一个 select 方法,这个方法用来选择一个合适的 MessageQueue:

public interface MessageQueueSelector {
    MessageQueue select(final List<MessageQueue> mqs, final Message msg, final Object arg);
}

RockerMQ 内置了三种选择策略:

rocketmq docker 安装教程 rocketmq入门教程_数据_14

如果需要自定义选择策略,只需要实现 MessageQueueSelector 接口即可。

发送延迟消息

RocketMQ 支持发送延迟消息, Broker 收到这类 消 息后 ,延迟一段时间再处理 , 使消息在规定的一段时间后生效 。

延迟消息的使用方法是在创建 Message 对象时,调用 setDelayTimeLevel 方法设置延迟时间, 然后再把这个消息发送出去。

public void setDelayTimeLevel(int level) {...}

目前延迟的时间不支持任意设置,仅支持预设值的时间长度 ( 1s/5s/1 Os/30s/I m/2m/3m/4m/5m/6m/
7m/8m/9m/1 Om/20m/30m/1 h/2h ) 。 比如 setDelayTimeLevel(3 ) 表示延迟 10s 。

对事务的支持

基本逻辑

RocketMQ 的 事务消息,是指发送消息事件和其他事件需要 同时成功或同时失败。 比如银行转账, A 银行的某账户要转一万元到 B 银行的某账户。 A 银行发送“B 银行账户增加一万元” 这个消息,要和“从 A 银行账户扣除一万元”这个操作同时成功或者同时失败。

RocketMQ 采用两阶段提交 的方式实现事务消息, TransactionMQProducer 处理上面情况的流程是,先发一个“准备从 B 银行账户增加一万元”的消息,发送成功后做从 A 银行账户扣除一万元的操作 ,根据操作结果是否成功,确定之前的“准备从 B 银行账户增加一万元”的消息是做 commit 还是 rollback ,具体流程如下:

  • 1 )发送方向 RocketMQ 发送“待确认”消息。
  • 2) RocketMQ 将收到的“待确认” 消息持久化成功后, 向发送方回复消息已经发送成功,此时第一阶段消息发送完成 。
  • 3 )发送方开始执行本地事件逻辑。
  • 4 )发送方根据本地事件执行结果向 RocketMQ 发送二次确认( Commit 或是 Rollback ) 消息 , RocketMQ 收到 Commit 状态则将第一阶段消息标记为可投递,订阅方将能够收到该消息;收到 Rollback 状态则删除第一阶段的消息,订阅方接收不到该消息。
  • 5 )如果出现异常情况,步骤 4 )提交的二次确认最终未到达 RocketMQ,服务器在经过固定时间段后将对“待确认”消息、发起回查请求。
  • 6 )发送方收到消息回查请求后(如果发送一阶段消息的 Producer 不能工作,回查请求将被发送到和 Producer 在同一个 Group 里的其他 Producer ),通过检查对应消息 的本地事件执行结果返回 Commit 或 Roolback 状态。
  • 7) RocketMQ 收到回查请求后,按照步骤 4 ) 的逻辑处理。

上面的逻辑似乎很好地实现了事务消息功能 ,它也是 RocketMQ 之前的版本实现事务消息的逻辑 。 但是因为 RocketMQ 依赖将数据顺序写到磁盘这个特征来提高性能,步骤 4 )却需要更改第一阶段消息的状态,这样会造成磁盘Catch 的脏页过多, 降低系统的性能 。 所以 RocketMQ 在 4.x 的版本中将这部分功能去除 。 系统中的一些上层 Class 都还在,用户可以根据实际需求实现自己的事务功能 。

具体实现

发送事务消息需要借助于 TransactionMQProducer 的 sendMessageInTransaction 方法:

public TransactionSendResult sendMessageInTransaction(final Message msg, final Object arg){...}

在发送事务消息之前,还需要设置 TransactionListener 事务监听器:

public void setTransactionListener(TransactionListener transactionListener){...}

TransactionListener 接口既能处理事务逻辑,又能供服务端回查:

public interface TransactionListener {

    LocalTransactionState executeLocalTransaction(final Message msg, final Object arg);

    LocalTransactionState checkLocalTransaction(final MessageExt msg);
}

这个接口种定义了两个方法:

  • executeLocalTransaction:这个方法用于实现本地的事务逻辑。它的除了接收消息之外,还接收一个用户自定义的参数。
  • checkLocalTransaction:这个方法在服务端回查时被调用。

这两个方法都返回一个 LocalTransactionState 类型的枚举值:

public enum LocalTransactionState {
    COMMIT_MESSAGE,
    ROLLBACK_MESSAGE,
    UNKNOW,
}
  • COMMIT_MESSAGE:通知服务端提交消息。
  • ROLLBACK_MESSAGE:通知服务端回滚消息。
  • UNKNOW:未知类型,服务端也会回滚消息。
示例代码

下面通过一段示例代码来更加深刻的认识 RocketMQ 对事务的支持:

package com.luzi.test.transaction;

import org.apache.commons.lang3.StringUtils;
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.client.producer.TransactionSendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;

import java.nio.charset.StandardCharsets;

public class TransactionProducerMain {

    public static final String COMMIT_TAG = "COMMIT_TAG";

    public static final String ROLLBACK_TAG = "ROLLBACK_TAG";

    public static final String UNKNOWN_TAG = "UNKNOWN_TAG";

    public static void main(String[] args) throws MQClientException {

        TransactionMQProducer producer = new TransactionMQProducer();
        producer.setProducerGroup("TransactionGroup");
        producer.setNamesrvAddr("127.0.0.1:9876");

        producer.setTransactionListener(new TransactionListener() {
            
            // 事务处理方法
            @Override
            public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {

                System.out.println("执行事务逻辑: " + new String(msg.getBody(), StandardCharsets.UTF_8));

                // 根据消息的标签判断是否提交
                if (StringUtils.equals(msg.getTags(), COMMIT_TAG)) {
                    return LocalTransactionState.COMMIT_MESSAGE;
                } else if (StringUtils.equals(msg.getTags(), ROLLBACK_TAG)) {
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                }

                return LocalTransactionState.UNKNOW;
            }

            // 回查方法
            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {

                System.out.println("执行回查逻辑: " + new String(msg.getBody(), StandardCharsets.UTF_8));
                return LocalTransactionState.COMMIT_MESSAGE;
            }
        });

        producer.start();

        String topic = "TransactionTopic";

        // 提交消息
        byte[] commitMessageBody = "commit message".getBytes(StandardCharsets.UTF_8);
        Message commitMessage = new Message(topic, COMMIT_TAG, commitMessageBody);
        TransactionSendResult commitResult = producer.sendMessageInTransaction(commitMessage, null);
        System.out.println(commitResult);

        // 回滚消息
        byte[] rollbackMessageBody = "rollback message".getBytes(StandardCharsets.UTF_8);
        Message rollbackMessage = new Message(topic, ROLLBACK_TAG, rollbackMessageBody);
        TransactionSendResult rollbackResult = producer.sendMessageInTransaction(rollbackMessage, null);
        System.out.println(rollbackResult);

        // 未知消息
        byte[] unknownMessageBody = "unknown message".getBytes(StandardCharsets.UTF_8);
        Message unknownMessage = new Message(topic, UNKNOWN_TAG, unknownMessageBody);
        TransactionSendResult unknownResult = producer.sendMessageInTransaction(unknownMessage, null);
        System.out.println(unknownResult);

        producer.shutdown();
    }

}

这段示例代码在事务处理方法种根据消的标签决定是否提交消息,经过对三种类型的消息进行测试,发现只有提交消息可以被消费者接收,这说明只有事务处理方法返回 LocalTransactionState.COMMIT_MESSAGE 时,消息才可以被提交。

可以在事务处理方法的起始处添加如下的延时语句:

Thread.sleep(60000);

再次运行程序,事务处理方法响应超时,服务端会调用回查方法。此时,三种类型的消息都可以被消费者接收。

消息过滤


在 RocketMQ 中,有三种方式可以过滤消息:一种是使用 tag 进行过滤;一种是使用 sql 进行过滤;最后一种是使用 FilterServer 进行过滤。

在介绍如何过滤之前,需要先介绍一个用于过滤消息的工具类——MessageSelector:

public class MessageSelector {

    /**
     * @see org.apache.rocketmq.common.filter.ExpressionType
     */
    private String type;

    /**
     * expression content.
     */
    private String expression;

    private MessageSelector(String type, String expression) {
        this.type = type;
        this.expression = expression;
    }

    /**
     * Use SLQ92 to select message.
     *
     * @param sql if null or empty, will be treated as select all message.
     */
    public static MessageSelector bySql(String sql) {
        return new MessageSelector(ExpressionType.SQL92, sql);
    }

    /**
     * Use tag to select message.
     *
     * @param tag if null or empty or "*", will be treated as select all message.
     */
    public static MessageSelector byTag(String tag) {
        return new MessageSelector(ExpressionType.TAG, tag);
    }

    public String getExpressionType() {
        return type;
    }

    public String getExpression() {
        return expression;
    }
}

通过 MessageSelector 可以指定以那种方式过滤消息,并指定表达式。

tag 过滤

tag 过滤的方式相对简单,在表达式中直接使用 tag 名称即可,可以使用或运算符“||”连接多个 tag ,实现同时监听多个 tag 的目的。如果要接收所有消息,表达式可以为 null、"" 或 "*" 。

sql 过滤

sql 过滤适用于更加复杂的场景,实现更加精细的过滤需求。

sql 过滤作用的对象是 Message 中的用户自定义属性:

public void putUserProperty(final String name, final String value) {...}
public String getUserProperty(final String name) {...}

RocketMQ 只定义可一些基本语法来支持这个特性:

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

RocketMQ 支持的常量类型如下:

  • 数值,如:123、3.14
  • 字符串,必须加单引号,如:'abc'
  • NULL
  • 布尔值,TRUE 或者 FALSE

如果生产者在创建消息的时候设置了如下属性:

message.putUserProperty("a", "10");

消费者可以通过如下的方式获取到这条消息:

pyshConsumer.subscribe("TestTopic", MessageSelector.bySql("a > 5"));

FilterServer 过滤

Filter Server 是一 种比 SQL 表达式更灵活的过滤方式,允许用户自 定 义Java 函数,根据 Java 函数 的逻辑对消息进行过滤。

要使用 Filter Server , 首先要在启动 Broker 前在配置文件里加上 filterServerNums = 3 这样的配置,Broker 在启动的时候 , 就会在本 机启动 3 个 Filter Server 进程 。 FilterServer 类 似 一 个 RocketMQ 的 Consumer 进程,它从本机 Broker 获取消息,然后根据用户上传过来的 Java 函数进行过滤,过滤后的消息
再传给远端的 Consumer。 这种方式会占用很多 Broker 机器的 CPU 资源,要根据实际情况谨慎使用。 上传的 java 代码也要经过检查,不能有申请大内存、创建线程等这样的操作,否则容易造成 Broker 服务器宕机。

要想使用 FilterServer 过滤,需要继承 MessageFilter 接口:

public interface MessageFilter {
    boolean match(final MessageExt msg, final FilterContext context);
}

根据 match 方法的返回值决定输入的 Message 是否发送到 Consumer。

下面是一个示例:

public class MessageFilterImpl implements MessageFilter {
    @Override
    public boolean match(MessageExt msg, FilterContext context) {
        String sequenceId = msg.getUserProperty("SequenceId");
        if (Objects.nonNull(sequenceId)) {
            int id = Integer.parseInt(sequenceId);
            if ((id % 3) == 0 && (id > 10)) {
                return true;
            }
        }
        return false;
    }
}

上面代码实现了过滤逻辑,它是根据消息的“SeqenceId”这个属性来过滤的,其实不一定要根据消息属性来过滤,也可以根据消息体的内容或其他特征过滤。

实现 MessageFilter 接口之后,还需要通过 DefaultMQPushConsumer 的 subscribe 方法将代码发送给 Broker ,才能使逻辑生效:

public void subscribe(String topic, String fullClassName, String filterClassSource){...}

下面是示例代码:

package com.luzi.test.filter;

import com.luzi.test.order.OrderConsumer;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.*;
import org.apache.rocketmq.common.MixAll;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.util.List;

public class MessageFilterConsumer {

    public static final Logger logger = LoggerFactory.getLogger(MessageFilterConsumer.class);

    public static final String NAMESRV_ADDR = "127.0.0.1:9876";

    public static final String CONSUMER_GROUP = "MessageFilterConsumerGroup";

    public static final String TOPIC = "MessageFilterTopic";

    public static final String TAG = "MessageFilterTag";

    public static void main(String[] args) throws Exception {

        // 实例化消息生产者,指定组名
        DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
        // 配置 namesrv 的地址
        pushConsumer.setNamesrvAddr(NAMESRV_ADDR);
        // 使用 MessageFilter 在服务端做消息过滤
        String filterCode = MixAll.file2String("/home/admin/MessageFilterImpl.java");
        pushConsumer.subscribe(TOPIC, "com.luzi.test.filter.MessageFilterImpl", filterCode);
        // 配置消费模式:负载均衡
        pushConsumer.setMessageModel(MessageModel.CLUSTERING);


        // 注册回调函数,用于处理接收到的消息
        pushConsumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                for (MessageExt message : msgs) {
                    String messageContent = new String(message.getBody(), StandardCharsets.UTF_8);
                    logger.info("messageContent={}", messageContent);
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        // 启动消费者
        pushConsumer.start();
        System.out.println("Consumer Started.");
    }

}

在使用 Filter Server 的 Consumer 例子中,主要是把实现过滤逻辑的类作为参数传到 Broker 端, Broker 端的 FilterServer 会解析这个类,然后根据 match 函数里的逻辑进行过滤。

不同场景下的使用


顺序消息


顺序消息是指消息的消费顺序和产生顺序相同。

RocketMQ 在默认情况下不保证顺序,比如创建一个 Topic ,默认八个写队列,八个读队列。这时候一条消息可能被写入任意一个队列里;在数据的读取过程中,可能有多个 Consumer ,每个 Consumer 也可能启动多个线程并行处理,所以消息被哪个 Consumer 消费,被消费的顺序和写入的顺序是否一致是不确定的。

顺序消息分为全局顺序消息和部分顺序消息,全局顺序消息指某个 Topic 下的所有消息都要保证顺序;部分顺序消息只要保证每一组消息被顺序消费即可。

全局顺序消息

要保证全局顺序消息,需要先把 Topic 的读写队列数设置为一,然后 Producer 和 Consumer 的并发设置也要是一。简单来说,为了保证整个 Topic 的全局消息有序,只能消除所有的并发处理,各部分都设置成单线程处理。这时高并发、高吞吐量的功能都用不上了。

部分顺序消息

在实际应用中,更多的场景只需要部分有序即可。在这种情况下,我们经过合适的配置,依然可以利用 RocketMQ 高并发、高吞吐量的能力。

要保证部分消息有序,需要发送端和消费端配合处理。在发送端,要做到把同一业务 ID 的消息发送到同一个 MessageQueue ;在消费过程中,要做到从同一个 Message Queue 读取的消息不被并发处理,这样才能达到部分有序。

发送端使用 MessageQueueSelector 来控制把消息发往哪个 MessageQueue ,示例如下:

package com.luzi.test.order;

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.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.List;

public class OrderProducer {

    public static final String NAMESRV_ADDR = "127.0.0.1:9876";

    public static final String PRODUCER_GROUP = "OrderProducerGroup";

    public static final String TOPIC = "OrderTopic";

    public static final String TAG = "OrderTag";

    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {

        // 创建生成者对象并初始化组名
        DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
        // 配置 namesrv 的地址
        producer.setNamesrvAddr(NAMESRV_ADDR);
        // 配置消息发送超时时间
        producer.setSendMsgTimeout(60000);
        // 启动生产者
        producer.start();

        // 构造消息
        String messagePattern = "message-{0}";
        for (int i = 0; i < 10; ++i) {
            byte[] messageContent = MessageFormat.format(messagePattern, String.valueOf(i)).getBytes(StandardCharsets.UTF_8);
            Message message = new Message(TOPIC, TAG, messageContent);

            // 发送消息
            // 消息分两组存放在不同的 MessageQueue 中
            SendResult sendResult = producer.send(message, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {

                    Integer id = (Integer) arg;
                    int index = id % 2;
                    return mqs.get(index);
                }
            }, i);

            System.out.println(sendResult);
        }

        // 关闭生产者
        producer.shutdown();
    }

}

消费端使用 MessageListener 串行的处理属于一个 MessageQueue 中的消息,示例如下:

package com.luzi.test.order;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.util.List;

public class OrderConsumer {

    public static final Logger logger = LoggerFactory.getLogger(OrderConsumer.class);

    public static final String NAMESRV_ADDR = "127.0.0.1:9876";

    public static final String CONSUMER_GROUP = "OrderConsumerGroup";

    public static final String TOPIC = "OrderTopic";

    public static final String TAG = "OrderTag";

    public static void main(String[] args) throws Exception {

        // 实例化消息生产者,指定组名
        DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
        // 配置 namesrv 的地址
        pushConsumer.setNamesrvAddr(NAMESRV_ADDR);
        // 订阅 Topic
        pushConsumer.subscribe(TOPIC, "*");
        // 配置消费模式:负载均衡
        pushConsumer.setMessageModel(MessageModel.CLUSTERING);

        // 注册回调函数,用于处理接收到的消息
        // 串行处理消息
        pushConsumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                for (MessageExt message : msgs) {
                    String messageContent = new String(message.getBody(), StandardCharsets.UTF_8);
                    logger.info("messageContent={}, queueId={}", messageContent, message.getQueueId());
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });

        // 启动消费者
        pushConsumer.start();
        System.out.println("Consumer Started.");
    }

}

在 MessageListenerOrderly 的实现中,为每个 ConsumerQueue 加锁,消费每个消息前,需要先获得这个消息对应的 ConsumerQueue 所对应的锁,这样保证了统一时间,同一个 ConsumerQueue 的消息不被并发消费,但不同 ConsumerQueue 的消息可以并发处理。

Consumer 的负载均衡

在吞吐量巨大的使用场景中,想要提高 Consumer 的处理速度,可以启动多个 Consumer 并发处理,这个时候就涉及如何在多个 Consumer 之间负载均衡的问题。

要做负载均衡,必须知道一些全局信息,也就是一个 ConsumerGroup 里到底有多少个 Consumer , 知道了全局信息,才可以根据某种算法来分配,比如简单地平均分到各个 Consumer。 在 RocketMQ 中,负载均衡或者消息分配是在 Consumer 端代码中完成的, Consumer 从 Broker 处获得全局信息,然后自己做负载均衡,只处理分给自己的那部分消息。

DefaultMQPushConsumer 的负载均衡

DefaultMQPushConsumer 的负载均衡过程不需要使用者操心,客户端程序会自动处理,每个 DefaultMQPushConsum巳r 启动后,会马上会触发一个 doRebalance 动作;而且在同一个 ConsumerGroup 里加入新的 DefaultMQPushConsumer 时,各个 Consumer 都会被触发 doRebalance 动作 。

如下图所示,具体的负载均衡算法有五种:

rocketmq docker 安装教程 rocketmq入门教程_数据_15

默认使用的是第一种 AllocateMessageQueueAveragely 。负载均衡的结果与 Topic 的 Message Queue
数量,以及 ConsumerGroup 里的 Consumer 的数 量 有关。 负载均衡的分配粒度只到 Message Queue ,把 Topic 下的所有 Message Queue 分配到不同的 Consumer 中,所以 Message Queue 和 Consumer 的数量关系,或者整除关系影响负载均衡结果。

以 AllocateMessageQueueAveragely 策略为例,如果创建 Topic 的时候,把Message Queue 数设为 3 , 当 Consumer 数量为 2 的时候,有一个 Consumer 需要处理 Topic 三分之二的消息,另一个处理三分之一的消息;当 Consumer 数量为 4 的时候,有一个 Consumer 无法收到消息,其他 3 个 Consumer 各处理 Topic 三分之一 的消息 。可见 Message Queue 数量设置过小不利于做负载均衡,通常情况下,应把一个 Topic 的 Message Queue 数设置为 16。

DefaultMQPullConsumer 的负载均衡

Pull Consumer 可以看到所有的 Message Queue , 而且从哪个 MessageQueue 读取消息,读消息时的 Offset 都由使用者控制,使用者可以实现任何特殊方式的负载均衡。

DefaultMQPullConsumer 有两个辅助方法可以帮助实现负载均衡,一个是 registerMessageQueueListener 方法:

public void registerMessageQueueListener(String topic, MessageQueueListener listener) {...}

这个方法用来注册 MessageQueueListener ,MessageQueueListener 接口的定义如下:

public interface MessageQueueListener {
    void messageQueueChanged(final String topic, final Set<MessageQueue> mqAll,
        final Set<MessageQueue> mqDivided);
}

MessageQueueListener 接口中只定义了一个方法——messageQueueChanged,这个方法共有三个参数,其中 mqAll 是 topic 下所有的 MessageQueue,mqDivided 是返回给当前 Consumer 的 MessageQueue。所以这个方法的作用就是根据一定的逻辑(自定义负载均衡),决定分配给当前 Consumer 哪些 MessageQueue 。

registerMessageQueueListener 方法在有新的 Consumer 加入或退出时被触发。

另一个辅助工具是 MQPullConsumerScheduleService 类,使用它类似使用 DefaultMQ PushConsumer ,但是它把 Pull 消息的 主动性留给了使用者,下面是一个示例:

package com.luzi.test.loadbalance;

import org.apache.rocketmq.client.consumer.*;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;

public class PullConsumerScheduleServiceTest {

    public static final String NAMESRV_ADDR = "127.0.0.1:9876";

    public static final String CONSUMER_GROUP = "ScheduleConsumerGroup";

    public static final String TOPIC = "ScheduleTopic";

    public static final String TAG = "ScheduleTag";

    public static void main(String[] args) throws MQClientException {

        // 配置属性
        MQPullConsumerScheduleService scheduleService = new MQPullConsumerScheduleService(CONSUMER_GROUP);
        scheduleService.getDefaultMQPullConsumer().setNamesrvAddr(NAMESRV_ADDR);
        scheduleService.setMessageModel(MessageModel.CLUSTERING);

        // 注册回调函数
        scheduleService.registerPullTaskCallback(TOPIC, new PullTaskCallback() {
            @Override
            public void doPullTask(MessageQueue mq, PullTaskContext context) {
                MQPullConsumer pullConsumer = context.getPullConsumer();
                try {
                    // 读取偏移量
                    long offset = pullConsumer.fetchConsumeOffset(mq, false);
                    offset = offset > 0 ? offset : 0;

                    // 消息处理逻辑
                    PullResult pullResult = pullConsumer.pull(mq, TAG, offset, 1);
                    if (Objects.equals(pullResult.getPullStatus(), PullStatus.FOUND)) {
                        List<MessageExt> msgFoundList = pullResult.getMsgFoundList();
                        for (MessageExt messageExt : msgFoundList) {
                            String messageBody = new String(messageExt.getBody(), StandardCharsets.UTF_8);
                            System.out.println("messageBody=" + messageBody);
                        }
                    }

                    // 更新偏移量
                    pullConsumer.updateConsumeOffset(mq, pullResult.getNextBeginOffset());
                    context.setPullNextDelayTimeMillis(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        });

        // 启动 Consumer
        scheduleService.start();
        System.out.println("Consumer Started.");
    }

}

然后我们看一看在 MQPullConsumerScheduleService 类的实现里,实现负载均衡的代码:

class MessageQueueListenerImpl implements MessageQueueListener {
    @Override
    public void messageQueueChanged(String topic, Set<MessageQueue> mqAll, Set<MessageQueue> mqDivided) {
        MessageModel messageModel =
            MQPullConsumerScheduleService.this.defaultMQPullConsumer.getMessageModel();
        switch (messageModel) {
            case BROADCASTING:
                MQPullConsumerScheduleService.this.putTask(topic, mqAll);
                break;
            case CLUSTERING:
                MQPullConsumerScheduleService.this.putTask(topic, mqDivided);
                break;
            default:
                break;
        }
    }
}

从源码中可以看出,用户通过更改 MessageQueueListenerimpl 的实现来做自己的负载均衡策略。