RocketMQ--简单入门

目录

windows安装

        下载RocketMQ

        启动nameserv&broker

        安装RocketMQ插件

RocketMQ基础入门

1.0.基本API

1.1.同步发送

1.1.1.引入依赖

1.1.2.消息生产者

1.1.3.消息消费者

1.1.4..启动Name Server

1.1.5..启动 Broker

1.1.6.接收消息

1.2.异步发送

1.3.单向发送

​1.4.拉模式

 1.5.顺序消息

 1.6.广播消息

 1.7.延迟消息

 1.8.批量消息

 1.9.过滤消息

1.9.1.Tag方式过滤

1.9.2.Sql方式过滤

1.10.事务消息

2.0.顺序消息

2.1.顺序类型

2.2.Rocket顺序消息

2.2.1.实现原理

2.2.2.如何保证集群有序

2.2.3.消息类型对比

2.2.4.注意事项

2.3.代码示例

2.4.如何保证消息顺序

3.0.消息投递策略

3.1.生产者投递策略

3.1.1.轮询算法投递

3.1.2.顺序投递策略

3.1.3.投递策略实现

3.2.消费者分配队列

3.2.1.消费者消费消息形式

3.2.2.平均分配算法

3.2.3.一致性hash分配算法

3.2.4.分配算法实现

4.0. RocketMQ消息保障

4.1.生产端保障

4.1.1.消息发送保障

1. 同步发送

2. 异步发送

4.1.2.消息发送总结

4.1.3.发送状态

4.2.4.MQ发送端重试保障

4.1.5.禁止自动创建topic

4.1.6.发送端规避

4.2.消费端保障

4.2.1.注意幂等性

4.2.2.消息消费模型

4.2.2.1.集群消费

4.2.2.2.广播消费

4.2.2.3.集群模式模拟广播

4.2.2.4.代码示例

4.2.3.消息消费模式

4.2.4.消息确认机制

4.2.5.消息重试机制

4.3.死信队列

5.0.常见问题

5.1.RocketMQ如何保证消息不丢失

5.2.RocketMQ的消息持久化机制

5.3.RocketMQ的事务消息原理


windows安装

下载RocketMQ

1.下载地址:https://rocketmq.apache.org/download/

 2. 下载Binary的包

3. 通过http的方式下载rocketmq-all-4.8.0-bin-release.zip

启动nameserv&broker

解压RocketMQ

 设置环境变量,变量名称ROCKETMQ_HOME,变量值为解压后rocketMQ的根目录。

进入到RocketMQ/bin目录启动nameserv服务,不要关闭启动窗口。

 执行命令:start mqnamesrv.cmd

D:\Server\rocketmq-all-5.1.3-bin-release\bin>start mqnamesrv.cmd

Java HotSpot(TM) 64-Bit Server VM warning: Using the DefNew young collector with the CMS collector is deprecated and will likely be removed in a future release

Java HotSpot(TM) 64-Bit Server VM warning: UseCMSCompactAtFullCollection is deprecated and will likely be removed in a future release.

The Name Server boot success. serializeType=JSON

进入到RocketMQ/bin目录启动broker服务,不要关闭启动窗口。

执行命令:start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true

D:\Server\rocketmq-all-5.1.3-bin-release\bin>start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true

The broker[Alienware-M17, 172.20.10.6:10911] boot success. serializeType=JSON and name server is 127.0.0.1:9876

注意:千万不要关闭启动窗口(一共三个启动窗口)

安装RocketMQ插件

下载:https://github.com/apache/rocketmq-externals

 Download ZIP把源码下载下来(rocketmq-externals-master.zip)

解压文件包rocketmq-externals-master.zip

 进入到\rocketmq-externals-master\rocketmq-console\src\main\resources目录下,修改启动配置。

        如果找不到rocketmq-console包,请下载低版本的rocketmq-externals,不要下载最新版本

server.address=0.0.0.0

server.port=8080 修改未被占用的端口

### SSL setting

#server.ssl.key-store=classpath:rmqcngkeystore.jks

#server.ssl.key-store-password=rocketmq

#server.ssl.keyStoreType=PKCS12

#server.ssl.keyAlias=rmqcngkey

#spring.application.index=true

spring.application.name=rocketmq-console

spring.http.encoding.charset=UTF-8

spring.http.encoding.enabled=true

spring.http.encoding.force=true

logging.level.root=INFO

logging.config=classpath:logback.xml

#if this value is empty,use env value rocketmq.config.namesrvAddr  NAMESRV_ADDR | now, you can set it in ops page.default localhost:9876

rocketmq.config.namesrvAddr=127.0.0.1:9876 添加nameserv地址

#if you use rocketmq version < 3.5.8, rocketmq.config.isVIPChannel should be false.default true

rocketmq.config.isVIPChannel=

#rocketmq-console's data path:dashboard/monitor

rocketmq.config.dataPath=/tmp/rocketmq-console/data

#set it false if you don't want use dashboard.default true

rocketmq.config.enableDashBoardCollect=true

#set the message track trace topic if you don't want use the default one

rocketmq.config.msgTrackTopicName=

rocketmq.config.ticketKey=ticket

#Must create userInfo file: ${rocketmq.config.dataPath}/users.properties if the login is required

rocketmq.config.loginRequired=false

#set the accessKey and secretKey if you used acl

#rocketmq.config.accessKey=

#rocketmq.config.secretKey=

进入到\rocketmq-externals-master\rocketmq-console目录下,执行命令:mvn clean package -Dmaven.test.skip=true,将工程打成jar包,打包后的jar包在\rocketmq-externals-master\rocketmq-console\target目录下。

D:\Server\rocketmq-externals-master\rocketmq-console>mvn clean package -Dmaven.test.skip=true

[INFO] Scanning for projects...

[INFO]

[INFO] ------------------------------------------------------------------------

[INFO] Building rocketmq-console-ng 2.0.0

[INFO] ------------------------------------------------------------------------

进入到\rocketmq-externals-master\rocketmq-console\target目录下启动jar包。

D:\Server\rocketmq-externals-master\rocketmq-console\target>java -jar rocketmq-console-ng-2.0.0.jar

12:21:34,729 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource

12:21:34,730 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource

  .   ____          _            __ _ _

 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \

( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \

 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )

  '  |____| .__|_| |_|_| |_\__, | / / / /

 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::        (v2.2.2.RELEASE)

打开页面:http://127.0.0.1:8080

 

1.0.基本API

RocketMQ目前支持 Java、C++、Go 三种语言访问,按惯例以Java语言为例看下如何用RocketMQ来收发消息的。

1.1.同步发送

1.1.1.引入依赖

添加RocketMQ客户端访问支持,具体版本和安装的RocketMQ版本一致即可。

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

1.1.2.消息生产者

public class Producer {
    public static void main(String[] args) throws Exception {
        //创建一个消息生产者,并设置一个消息生产者组
        DefaultMQProducer producer = new DefaultMQProducer("producer_group");
        //指定 NameServer 地址
        producer.setNamesrvAddr("localhost:9876");
        //初始化Producer,整个应用生命周期内只需要初始化一次
        producer.start();
        for (int i = 0; i < 100; i++) {
            //创建一条消息对象,指定其主题、标签和消息内容
            Message msg = new Message(
                    "Simple" /* 消息主题名 */,
                    "message_tag" /* 消息标签 */,
                    ("Hello Java demo RocketMQ " + i)
                            .getBytes(RemotingHelper.DEFAULT_CHARSET) /* 消息内容 */
            );
            //发送消息并返回结果
            SendResult sendResult = producer.send(msg);
            System.out.printf("%s%n", sendResult);
        }
        // 一旦生产者实例不再被使用则将其关闭,包括清理资源,关闭网络连接等
        producer.shutdown();
    }
}

示例中用DefaultMQProducer类来创建一个消息生产者,通常一个应用创建一个DefaultMQProducer对象,所以一般由应用来维护生产者对象,可以其设置为全局对象或者单例。该类构造函数入参producerGroup是消息生产者组的名字,无论生产者还是消费者都必须给出GroupName,并保证该名字的唯一性,ProducerGroup发送普通的消息时作用不大,后面介绍分布式事务消息时会用到。

接下来指定NameServer地址和调用start方法初始化,在整个应用生命周期内只需要调用一次start方法。

初始化完成后,调用send方法发送消息,示例中只是简单的构造了100条同样的消息发送,其实一个 Producer对象可以发送多个主题多个标签的消息,消息对象的标签可以为空send方法是同步调用,只要不抛异常就表示成功。

最后应用退出时调用shutdown方法清理资源、关闭网络连接,从服务器上注销自己,通常建议应用在 JBOSSTomcat 等容器的退出钩子里调用shutdown方法。

1.1.3.消息消费者

/**
 * 可靠要求高、数据量较少、实时响应
 */
public class Consumer {
    public static void main(String[] args) throws Exception {
        //创建一个消息消费者,并设置一个消息消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group");
        //指定 NameServer 地址
        consumer.setNamesrvAddr("localhost:9876");
        //设置 Consumer 第一次启动时从队列头部开始消费还是队列尾部开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        //订阅指定 Topic 下的所有消息
        consumer.subscribe("Simple", "*");
        //注册消息监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                //默认 list 里只有一条消息,可以通过设置参数来批量接收消息
                if (list != null) {
                    for (MessageExt ext : list) {
                        try {
                            System.out.println(new Date() + new String(ext.getBody(), "UTF-8"));
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        // 消费者对象在使用之前必须要调用 start 初始化
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

示例中用DefaultMQPushConsumer类来创建一个消息消费者,同生产者一样一个应用一般创建一个DefaultMQPushConsumer对象,该对象一般由应用来维护,可以其设置为全局对象或者单例。该类构造函数入参consumerGroup是消息消费者组的名字,需要保证该名字的唯一性。

接下来指定NameServer地址和设置消费者应用程序第一次启动时从队列头部开始消费还是队列尾部开始消费。

接着调用subscribe方法给消费者对象订阅指定主题下的消息,该方法第一个参数是主题名,第二个擦书是标签名,示例表示订阅了主题名topic_rocket_example下所有标签的消息。

最主要的是注册消息监听器才能消费消息,示例中用的是Consumer Push的方式,即设置监听器回调的方式消费消息,默认监听回调方法中List<MessageExt>里只有一条消息,可以通过设置参数来批量接收消息。

最后调用start方法初始化,在整个应用生命周期内只需要调用一次start方法。

1.1.4..启动Name Server

nohup sh bin/mqnamesrv &

tail -f ~/logs/rocketmqlogs/namesrv.log

RocketMQ核心的四大组件中Name ServerBroker都是由RocketMQ安装包提供的,所以要启动这两个应用才能提供消息服务。首先启动Name Server,先确保你的机器中已经安装了与RocketMQ相匹配的 JDK,并设置了环境变量JAVA_HOME ,然后在RocketMQ的安装目录下执行bin目录下的mqnamesrv,默认会将该命令的执行情况输出到当前目录的nohup.out 文件,最后跟踪日志文件查看Name Server的实际运行情况。

1.1.5..启动 Broker

nohup sh bin/mqbroker -n localhost:9876 &

tail -f ~/logs/rocketmqlogs/broker.log

同样也要确保你的机器中已经安装了与RocketMQ相匹配的JDK,并设置了环境变量JAVA_HOME,然后在RocketMQ的安装目录下执行bin目录下的mqbroker,默认会将该命令的执行情况输出到当前目录的nohup.out文件,最后跟踪日志文件查看Broker的实际运行情况。

1.1.6.接收消息

启动生产者与消费者,可以看到消费者同步消费了消息。

生产者:

消费者:

1.2.异步发送

  生产者:

/**
 * 异步发送 提高系统资源利用率 提高系统资源吞吐量
 * 消息存储在topic对应border中
 */
public class AsyncProducer {
    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("AsyncProducer");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        CountDownLatch countDownLatch = new CountDownLatch(100);
        for (int i = 0; i < 100; i++) {
            final int index = i;
            Message message = new Message("Simple", "message_tag", ("_AsyncProducer_" + i).getBytes(StandardCharsets.UTF_8));
            producer.send(message, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    countDownLatch.countDown();
                    System.out.println(index + "_消息发送成功_" + sendResult);
                }

                @Override
                public void onException(Throwable throwable) {
                    countDownLatch.countDown();
                    System.out.println(index + "_消息发送失败_" + throwable.getStackTrace());
                }
            });
        }
        countDownLatch.await(5, TimeUnit.SECONDS);
        producer.shutdown();
    }
}

 消费者:

/**
 * 可靠要求高、数据量较少、实时响应
 */
public class Consumer {
    public static void main(String[] args) throws Exception {
        //创建一个消息消费者,并设置一个消息消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group");
        //指定 NameServer 地址
        consumer.setNamesrvAddr("localhost:9876");
        //设置 Consumer 第一次启动时从队列头部开始消费还是队列尾部开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        //订阅指定 Topic 下的所有消息
        consumer.subscribe("Simple", "*");
        //注册消息监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                //默认 list 里只有一条消息,可以通过设置参数来批量接收消息
                if (list != null) {
                    for (MessageExt ext : list) {
                        try {
                            System.out.println(new Date() + new String(ext.getBody(), "UTF-8"));
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        // 消费者对象在使用之前必须要调用 start 初始化
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

 可以看到异步发送接收的消息是随机的。

1.3.单向发送

单项发送只负责发送,不管消息是否发送成功。发送效率最高,但是安全性最低。

生产者:

/**
 * 单项发送 只负责发送,不管消息是否发送成功
 * 比如日志打印系统
 * 发送效率最高,安全最低
 */
public class OneWayProducer {
    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("AsyncProducer");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        for (int i = 0; i < 100; i++) {
            Message message = new Message("Simple", "message_tag", ("_OneWayProducer_" + i).getBytes(StandardCharsets.UTF_8));
            producer.sendOneway(message);
            System.out.println(i + "_消息发送成功_");
        }
        producer.shutdown();
    }
}

 消费者:

/**
 * 可靠要求高、数据量较少、实时响应
 */
public class Consumer {
    public static void main(String[] args) throws Exception {
        //创建一个消息消费者,并设置一个消息消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group");
        //指定 NameServer 地址
        consumer.setNamesrvAddr("localhost:9876");
        //设置 Consumer 第一次启动时从队列头部开始消费还是队列尾部开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        //订阅指定 Topic 下的所有消息
        consumer.subscribe("Simple", "*");
        //注册消息监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                //默认 list 里只有一条消息,可以通过设置参数来批量接收消息
                if (list != null) {
                    for (MessageExt ext : list) {
                        try {
                            System.out.println(new Date() + new String(ext.getBody(), "UTF-8"));
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        // 消费者对象在使用之前必须要调用 start 初始化
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

1.4.拉模式

官方API

生产者:

同步发送:org.apache.rocketmq.example.simple.Producer

异步发送:org.apache.rocketmq.example.simple.AsyncProducer

单向发送:org.apache.rocketmq.example.simple.OnewayProducer

消费者:

拉模式:org.apache.rocketmq.example.simple.PullConsumer

推模式:org.apache.rocketmq.example.simple.PushConsumer

拉模式(随机获取一个queue):org.apache.rocketmq.example.simple.LitePullConsumerSubscribe.java

拉模式(指定获取一个queue):org.apache.rocketmq.example.simple.LitePullConsumerAssign.java

前面的consumer使用的都是推模式,这里使用consumer的拉模式主动拉取消息队列中的消息。

public class PullConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("PullConsumer");
        consumer.setNamesrvAddr("localhost:9876");
        Set<String> topics = new HashSet<>();
        topics.add("Simple");
        topics.add("TopicTest");
        consumer.setRegisterTopics(topics);
        consumer.start();
        while (true) {
            consumer.getRegisterTopics().forEach(topic -> {
                try {
                    Set<MessageQueue> messageQueues = consumer.fetchSubscribeMessageQueues(topic);
                    messageQueues.forEach(message -> {
                        try {
                            long offset = consumer.getOffsetStore().readOffset(message, ReadOffsetType.READ_FROM_MEMORY);
                            if (offset < 0) {
                                offset = consumer.getOffsetStore().readOffset(message, ReadOffsetType.READ_FROM_STORE);
                            }
                            if (offset < 0) {
                                offset = consumer.maxOffset(message);
                            }
                            if (offset < 0) {
                                offset = 0;
                            }
                            PullResult pullResult = consumer.pull(message, "*", offset, 48);
                            System.out.println("消息循环拉取成功_" + pullResult);
                            switch (pullResult.getPullStatus()) {
                                case FOUND:
                                    pullResult.getMsgFoundList().forEach(result -> {
                                        System.out.println("消息消费成功_" + result);
                                    });
                                    consumer.updateConsumeOffset(message, pullResult.getNextBeginOffset());
                                default:
                                    break;
                            }
                        } catch (MQClientException e) {
                            e.printStackTrace();
                        } catch (RemotingException e) {
                            e.printStackTrace();
                        } catch (MQBrokerException e) {
                            e.printStackTrace();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    });
                } catch (MQClientException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

 通常情况下,用推模式比较简单。需要注意DefaultMQPullConsumerImpl这个消费者类已标记为过期,但是还是可以使用的。替换的类是DefaultLitePullConsumerImpl。

• LitePullConsumerSubscribe:随机获取一个queue消息。

public class LitePullConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultLitePullConsumer consumer = new DefaultLitePullConsumer("LitePullConsumer");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("Simple", "*");
        consumer.start();
        while (true) {
            List<MessageExt> messageExts = consumer.poll();
            System.out.println("消息拉取成功");
            messageExts.forEach(message -> {
                System.out.println("消息消费成功_"+message);
            });
        }
    }
}

 • LitePullConsumerAssign:指定一个queue消息。

public class LitePullConsumerAssign {
    public static void main(String[] args) throws MQClientException {
        DefaultLitePullConsumer consumer = new DefaultLitePullConsumer("LitePullConsumer");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.start();
        Collection<MessageQueue> collection = consumer.fetchMessageQueues("Simple");
        ArrayList<MessageQueue> messageQueues = new ArrayList<>(collection);
        consumer.assign(messageQueues);
        consumer.seek(messageQueues.get(1), 10);
        System.out.println("consumer started");
        try {
            while (true) {
                List<MessageExt> messageExts = consumer.poll();
                System.out.println("消息拉取成功");
                messageExts.forEach(message -> {
                    System.out.println("消息消费成功_"+message);
                });
            }
        } finally {
            consumer.shutdown();
        }
    }
}

 1.5.顺序消息

官方API:

生产者:org.apache.rocketmq.example.order.Producer

消费者:org.apache.rocketmq.example.order.Consumer

顺序消息指生产者局部有序发送到一个queue,但多个queue之间是全局无序的。

顺序消息生产者样例:通过MessageQueueSelector将消息有序发送到同一个queue中。

/**
 * 顺序消息生产者
 */
public class OrderProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("OrderProducer");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        for (int j = 0; j < 5; j++) {
            for (int i = 0; i < 10; i++) {
                Message message = new Message("Simple", "message_tag",
                        ("order_" + j + "_step_" + i).getBytes(StandardCharsets.UTF_8));
                SendResult sendResult = producer.send(message, new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
                        Integer id = (Integer) o;
                        int index = id % list.size();
                        return list.get(index);
                    }
                }, j);
                System.out.printf("消息发送成功_", sendResult);
            }
        }
        producer.shutdown();
    }
}

 顺序消息消费者样例:通过MessageListenerOrderly消费者每次读取消息都只从一个queue中获取(通过加锁的方式实现)。

/**
 * 顺序消息消费者
 * Created by BaiLi
 */
public class OrderConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("OrderConsumer");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("Order","*");
        consumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
                list.forEach(message->{
                    System.out.println("QueueId:"+message.getQueueId() + "收到消息内容 "+new String(message.getBody()));
                });
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

 1.6.广播消息

广播消息并没有特定的消息消费者样例,这是因为这涉及到消费者的集群消费模式。

• MessageModel.BROADCASTING:广播消息。一条消息会发给所有订阅了对应主题的消费者,不管消费者是不是同一个消费者组。

• MessageModel.CLUSTERING:集群消息。每一条消息只会被同一个消费者组中的一个实例消费。

生产者:

/**
 * 异步发送 提高系统资源利用率 提高系统资源吞吐量
 * 消息存储在topic对应border中
 */
public class AsyncProducer {
    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("AsyncProducer");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        CountDownLatch countDownLatch = new CountDownLatch(100);
        for (int i = 0; i < 10; i++) {
            final int index = i;
            Message message = new Message("Simple", "message_tag", ("_AsyncProducer_" + i).getBytes(StandardCharsets.UTF_8));
            producer.send(message, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    countDownLatch.countDown();
                    System.out.println(index + "_消息发送成功_" + sendResult);
                }

                @Override
                public void onException(Throwable throwable) {
                    countDownLatch.countDown();
                    System.out.println(index + "_消息发送失败_" + throwable.getStackTrace());
                }
            });
        }
        countDownLatch.await(5, TimeUnit.SECONDS);
        producer.shutdown();
    }
}

 广播消费者:

/**
 * 广播消息
 */
public class BroadcastConsumer {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("Simple", "*");
        consumer.setMessageModel(MessageModel.BROADCASTING);
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                if (list != null) {
                    for (MessageExt message : list) {
                        try {
                            System.out.println("_消费着消费成功_" + new Date() + new String(message.getBody(), "UTF-8"));
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

启动两个消费者,消费消息,他们消费了同样的消息。

 集群消费者

/**
 * 集群消息
 */
public class BroadcastConsumer {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("Simple", "*");
        consumer.setMessageModel(MessageModel.CLUSTERING);
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                if (list != null) {
                    for (MessageExt message : list) {
                        try {
                            System.out.println("_消费着消费成功_" + new Date() + new String(message.getBody(), "UTF-8"));
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

启动两个消费者,消费消息,他们共同消费了生产者发送的消息。

/**
 * 集群消息
 */
public class BroadcastConsumer {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("Simple", "*");
        consumer.setMessageModel(MessageModel.CLUSTERING);
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                if (list != null) {
                    for (MessageExt message : list) {
                        try {
                            System.out.println("_消费着消费成功_" + new Date() + new String(message.getBody(), "UTF-8"));
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

消费者_1消费了6条消息。

 消费者_2消费了4条消息。

 1.7.延迟消息

官方API样例:

生产者:

预定日期发送:org.apache.rocketmq.example.schedule.ScheduledMessageProducer.java

指定时间发送:org.apache.rocketmq.example.schedule.TimerMessageProducer.java

消费者:

预定日期消费:org.apache.rocketmq.example.schedule.ScheduledMessageConsumer.java

  指定时间消费:org.apache.rocketmq.example.schedule.TimerMessageConsumer.java

延迟消息实现的效果就是在调用producer.send方法后,消息并不会立即发送出去,而是会等一段时间再发送出去。这是RocketMQ特有的一个功能。

• message.setDelayTimeLevel(3):预定日常定时发送。1到18分别对应messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h;可以在dashboard中broker配置查看。

• msg.setDelayTimeMs(10L):指定时间定时发送。默认支持最大延迟时间为3天,可以根据broker配置:timerMaxDelaySec修改。

预定时间生产者:

public class ScheduleProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("ScheduleProducer");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        for (int i = 0; i < 5; i++) {
            Message message = new Message("Schedule",
                    "message_tag",
                    ("_ScheduleProducer_" + i).getBytes(StandardCharsets.UTF_8)
            );
            //1到18分别对应messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
            message.setDelayTimeLevel(3);
            producer.send(message);
            System.out.printf(i + ".发送消息成功:%s%n", LocalTime.now());
        }
        producer.shutdown();
    }
}

 预定时间消费者:

public class ScheduleConsumer {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ScheduleConsumer");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("Schedule", "*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                if (list != null) {
                    for (MessageExt message : list) {
                        try {
                            System.out.println("_消息消费成功_" + LocalTime.now() + new String(message.getBody(), "UTF-8"));
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

可以看到生产者发送的消息延迟了30秒后发送到了消费者。

 指定时间生产者:

public class ScheduleProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("ScheduleProducer");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        for (int i = 0; i < 5; i++) {
            Message message = new Message("Schedule",
                    "message_tag",
                    ("_ScheduleProducer_" + i).getBytes(StandardCharsets.UTF_8)
            );
            message.setDelayTimeMs(10000L);
            producer.send(message);
            System.out.printf(i + ".发送消息成功:%s%n", LocalTime.now());
        }
        producer.shutdown();
    }
}

使用预定时间消费者接收消息,可以看到时间延迟了10秒后接收到了消息。 

 1.8.批量消息

官方生产者API:

org.apache.rocketmq.example.batch.SimpleBatchProducer

org.apache.rocketmq.example.batch.SplitBatchProducer

批量消息是指将多条消息合并成一个批量消息,一次发送出去。这样的好处是可以减少网络IO,提升吞吐量。

批量消息的使用限制:

• 消息大小不能超过4M,虽然源码注释不能超1M,但是实际使用不超过4M即可。平衡整体的性能,建议保持1M左右。

• 相同的Topic,

• 相同的waitStoreMsgOK

• 不能是延迟消息、事务消息等

批量消息生产者:

public class BatchProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("BatchProducer");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        ArrayList<Message> messages = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            messages.add(new Message("Simple", "message_tag", ("_BatchProducer_" + i).getBytes(StandardCharsets.UTF_8)));
        }
        SendResult send = producer.send(messages);
        System.out.printf("发送消息成功:%s%n", send);
        producer.shutdown();
    }
}

消息合并成一条发送了。

 消息消费者:

public class Consumer {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("Simple", "*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                if (list != null) {
                    for (MessageExt message : list) {
                        try {
                            System.out.println(new Date() + new String(message.getBody(), "UTF-8"));
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

消费者一条一条的消费了消息。

 如果消息大小超过了4M,需要对消息做切割后做批量发送,否则会出现报错。

public class BatchProducer {
    public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("BatchProducer");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        ArrayList<Message> messages = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            messages.add(new Message("Simple", "message_tag", ("_BatchProducer_" + i).getBytes(StandardCharsets.UTF_8)));
        }
        int i = 0;
        ListSplitter listSplitter = new ListSplitter(messages);
        while (listSplitter.hasNext()) {
            SendResult sendResult = producer.send(listSplitter.next());
            System.out.printf(i++ + "_消息发送成功_%s%n", sendResult);
        }
        producer.shutdown();
    }
}

class ListSplitter implements Iterator<List<Message>> {
    private static final int SIZE_LIMIT = 40 * 1000;
    private final List<Message> message;
    private int currentIndex;

    ListSplitter(List<Message> message) {
        this.message = message;
    }

    @Override
    public boolean hasNext() {
        return currentIndex < this.message.size();
    }

    @Override
    public List<Message> next() {
        int nextIndex = currentIndex;
        int totalSize = 0;
        for (; nextIndex < message.size(); nextIndex++) {
            Message message = this.message.get(nextIndex);
            int messageLength = message.getBody().length + message.getTopic().length();
            Map<String, String> properties = message.getProperties();
            Iterator<Map.Entry<String, String>> iterator = properties.entrySet().iterator();
            while (iterator.hasNext()) {
                messageLength += iterator.next().getKey().length() + iterator.next().getValue().length();
            }
            messageLength += messageLength + 20;
            if (messageLength > SIZE_LIMIT && nextIndex - currentIndex == 0) {
                nextIndex++;
                break;
            }
            if (messageLength + totalSize > SIZE_LIMIT) {
                break;
            } else {
                totalSize += messageLength;
            }
        }
        List<Message> messages = message.subList(currentIndex, nextIndex);
        currentIndex = nextIndex;
        return messages;
    }
}

发送10W条数据,切成了254次发送。

消费者接收到了10W条数据。

 1.9.过滤消息

在大多数情况下,可以使用MessageTag属性来简单快速过滤信息

官方API:

生产者:org.apache.rocketmq.example.filter.SqlFilterProducer

消费者:org.apache.rocketmq.example.filter.SqlFilterConsumer

1.9.1.Tag方式过滤

通过consumer.subscribe("TagFilterTest", "TagA || TagC")实现:

tag过滤生产者:

public class FilterTagProducer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("FilterTagProducer");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        String[] tags = new String[]{ "TagA", "TagB", "TagC"};
        for (int i = 0; i < 10; i++) {
            Message msg = new Message("Filter", tags[i % tags.length], ("FilterTagProducer_" + tags[i % tags.length]).getBytes(RemotingHelper.DEFAULT_CHARSET)
            );
            SendResult sendResult = producer.send(msg);
            System.out.printf(tags[i % tags.length] + "消息发送成功%s%n", sendResult);
        }
        producer.shutdown();
    }
}

生产消息:

 tag过滤消费者:

public class FilterTagConsumer {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("FilterTagConsumer");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("Filter", "TagA || TagC");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                if (list != null) {
                    for (MessageExt message : list) {
                        try {
                            System.out.println("_消息消费成功_" + LocalTime.now() + new String(message.getBody(), "UTF-8"));
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

消费了TagA 和TagC的消息:

 TagRocketMQ中特有的一个消息属性。

RocketMQ的最佳实践中就建议使用RocketMQ时,一个应用可以就用一个Topic,而应用中的不同业务就用Tag来区分。

Tag方式有一个很大的限制,就是一个消息只能有一个Tag,这在一些比较复杂的场景就有点不足了。 这时候可以使用SQL表达式来对消息进行过滤。

1.9.2.Sql方式过滤

通过MessageSelector.bySql(String sql)参数实现:

这里面的sql语句是按照SQL92标准来执行的。sql中可以使用的参数有默认的TAGS和一个在生产者中加入的自定义属性。

SQL过滤需要修改conf配置,在conf/broker.conf中添加:

enablePropertyFilter = true

filterSupportRetry = true

修改后重启borker服务:

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

SQL过滤生产者:

public class FilterTagProducer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("FilterTagProducer");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        String[] tags = new String[]{ "TagA", "TagB", "TagC"};
        for (int i = 0; i < 10; i++) {
            Message message = new Message("Filter", tags[i % tags.length],
                    ("FilterTagProducer_" + tags[i % tags.length]).getBytes(RemotingHelper.DEFAULT_CHARSET));
            message.putUserProperty("BeMyself",String.valueOf(i));
            SendResult sendResult = producer.send(message);
            System.out.printf(tags[i % tags.length] + "_BeMyself_" + i + "_消息发送成功%s%n", sendResult);
        }
        producer.shutdown();
    }
}

 SQL过滤消费者:

public class FilterTagConsumer {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("FilterTagConsumer");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("Filter", MessageSelector.bySql("TAGS is not null and TAGS in ('TagA', 'TagC')" +
                " and (BeMyself is not null and BeMyself between 0 and 3)"));
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                if (list != null) {
                    for (MessageExt message : list) {
                        try {
                            System.out.println("_消息消费成功_" + LocalTime.now() + new String(message.getBody(), "UTF-8"));
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

消费结果不仅仅根据tags做了过滤,还根据UserProperty做了过滤。

 SQL92语法:

RocketMQ只定义了一些基本语法来支持这个特性。我们可以很容易地扩展它。

• 数值比较,比如:>,>=,<,<=,BETWEEN,=;

• 字符比较,比如:=,<>,IN;

• IS NULL ,IS NOT NULL;

• 逻辑符号 AND,OR,NOT;

常量支持类型为:

• 数值,比如:123,3.1415;

• 字符,比如:'abc',必须用单引号包裹起来;

• NULL,特殊的常量

• 布尔值,TRUE 或 FALSE

使用注意:

• 只有推模式的消费者可以使用SQL过滤。拉模式是用不了的;

• 另外消息过滤是在Broker端进行的(consumer将过滤条件推送给broker),提升网络传输性能,但是broker服务会比较繁忙

1.10.事务消息

事务消息是在分布式系统中保证最终一致性两阶段提交的消息实现。他可以保证本地事务执行与消息发送两个操作的原子性,也就是这两个操作一起成功或者一起失败。

这个事务消息是RocketMQ提供的一个非常有特色的功能,需要着重理解。

broker事务回查次数15次后会标记事务已提交,并将状态存储到broker上面,直到事务消息被丢弃。 

事务消息机制的关键是在发送消息时会将消息转为一个half半消息,并存入RocketMQ内部的一个Topic(RMQ_SYS_TRANS_HALF_TOPIC),这个Topic对消费者是不可见的。再经过一系列事务检查通过后,再将消息转存到目标Topic,这样对消费者就可见了。

事务消息只保证消息发送者的本地事务与发消息这两个操作的原子性,因此,事务消息的示例只涉及到消息发送者,对于消息消费者来说,并没有什么特别的。

事务消息的关键是在TransactionMQProducer中指定了一个TransactionListener事务监听器,这个事务监听器就是事务消息的关键控制器

事务消息生产者:

public class TransactionProducer {
    public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, InterruptedException {
        TransactionMQProducer producer = new TransactionMQProducer("TransactionProducer");
        producer.setNamesrvAddr("localhost:9876");
        // 异步提交事务
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(200), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable runnable) {
                Thread thread = new Thread(runnable);
                thread.setName("BeMyself");
                return thread;
            }
        });
        producer.setExecutorService(executor);
        producer.setTransactionListener(new TransactionListener() {
            @Override
            public LocalTransactionState executeLocalTransaction(Message message, Object o) {
                String tags = message.getTags();
                if (StringUtils.contains("TagA", tags)) {
                    return LocalTransactionState.COMMIT_MESSAGE;
                } else if (StringUtils.contains("TagB", tags)) {
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                } else {
                    return LocalTransactionState.UNKNOW;
                }
            }

            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
                String tags = messageExt.getTags();
                if (StringUtils.contains("TagC", tags)) {
                    return LocalTransactionState.COMMIT_MESSAGE;
                } else if (StringUtils.contains("TagD", tags)) {
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                } else {
                    return LocalTransactionState.UNKNOW;
                }
            }
        });
        producer.start();
        String[] tags = new String[]{ "TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < 10; i++) {
            Message message = new Message("Transaction",
                    tags[i % tags.length],
                    ("TransactionProducer" + tags[i % tags.length]).getBytes(RemotingHelper.DEFAULT_CHARSET));
            TransactionSendResult transactionSendResult = producer.sendMessageInTransaction(message, null);
            System.out.println("消息发送成功_" + transactionSendResult);
            Thread.sleep(10);
        }
        Thread.sleep(10000);
        producer.shutdown();
    }
}

生产者发送消息:

 事务消息消费者:

public class TransactionConsumer {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TransactionConsumer");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("Transaction", "*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                if (list != null) {
                    for (MessageExt message : list) {
                        try {
                            System.out.println("_消息消费成功_" + LocalTime.now() + new String(message.getBody(), "UTF-8"));
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

消费者消费消息,TagA会先被消费,TagC在经过Broker回查后提交了commit后,消费者收到了消息。

 事务消息的使用限制:

• 事务消息不支持延迟消息批量消息

• 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为15次,但是用户可以通过Broker配置文件的transactionCheckMax参数来修改此限制。如果已经检查某条消息超过N次的话(N = transactionCheckMax)则Broker将丢弃此消息,并在默认情况下同时打印错误日志。可以通过重写AbstractTransactionCheckListener类来修改这个行为。

• 事务性消息可能不止一次被检查消费

2.0.顺序消息

2.1.顺序类型

无序消息:无序消息也指普通的消息Producer只管发送消息,Consumer只管接收消息,至于消息和消息之间的顺序并没有保证。

• Producer依次发送 orderId 为1、2、3的消息

• Consumer接到的消息顺序有可能是1、2、3,也有可能是2、1、3等情况,这就是普通消息。

全局顺序:

对于指定的一个Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。

Producer发送orderId 1,3,2 的消息, 那么Consumer也必须要按照 1,3,2 的顺序进行消费。

局部顺序:在实际开发有些场景中,我并不需要消息完全按照完全按的先进先出,而是某些消息保证先进先出就可以了。

 如一个打车涉及到不同地区北京,上海、广州、深圳。我不用管其它的订单,只保证同一个地区的订单ID能保证这个顺序就可以了。

2.2.Rocket顺序消息

RocketMQ可以严格的保证消息有序,但这个顺序,不是全局顺序,只是分区(queue)顺序,要全局顺序只能一个分区

之所以出现这个场景看起来不是顺序的,是因为发送消息的时候,消息发送默认是会采用轮询的方式发送到不queue(队列)。

2.2.1.实现原理

生产的message最终会存放在Queue中,如果一个Topic关联了4个Queue不指定消息往哪个队列里放,那么默认平均分配消息到4个queue

假设有10条订单消息,这10条消息会平均分配在这4个Queue上,那么每个Queue大概放2个左右。这里有一点很重的是:同一个queue,存储在里面的message 是按照先进先出的原则

我们让不同的地区用不同的queue。只要保证同一个地区的订单把他们放到同一个Queue那就保证消费者先进先出了。

这就保证局部顺序了,即同一订单按照先后顺序放到同一Queue那么取消息的时候就可以保证先进先取出。

2.2.2.如何保证集群有序

这里还有很关键的一点,在一个消费者集群的情况下,消费者1先去Queue拿消息,它拿到了北京订单1,它拿完后,消费者2queue拿到的是北京订单2。

拿的顺序是没毛病了,但关键是先拿到不代表先消费完它。会存在虽然你消费者1先拿到北京订单1,但由于网络等原因,消费者2比你真正的先消费消息。这是不是很尴尬了。

分布式锁:

Rocker采用的是分段锁,它不是锁整个Broker而是里面的单个Queue,因为只要锁单个Queue就可以保证局部顺序消费了。

所以最终的消费者这边的逻辑就是

• 消费者1去Queue北京订单1,它就锁住了整个Queue,只有它消费完成并返回成功后,这个锁才会释放。

• 然后下一个消费者去拿到北京订单2同样锁住当前Queue这样的一个过程来真正保证对同一个Queue能够真正意义上的顺序消费,而不仅仅是顺序取出。

2.2.3.消息类型对比

全局顺序与分区顺序对比

Topic消息类型

支持事务消息

支持定时/延时消息

性能

无序消息(普通、事务、定时/延时)

最高

分区顺序消息

全局顺序消息

一般

发送方式对比

Topic消息类型

支持可靠同步发送

支持可靠异步发送

支持Oneway发送

无序消息(普通、事务、定时/延时)

分区顺序消息

全局顺序消息

2.2.4.注意事项

1. 顺序消息暂不支持广播模式

2. 顺序消息不支持异步发送方式,否则将无法严格保证顺序。

3. 建议同一个Group ID只对应一种类型的Topic,即不同时用于顺序消息和无序消息的收发。

4. 对于全局顺序消息,建议创建broker个数 >=2。

2.3.代码示例

生产者:

public interface MQConstant {
    String ROCKETMQ_NAMESERVER_ADDR = "192.168.52.50:9876";
    String RABBITMQ_HOST = "127.0.0.1";
    Integer RABBITMQ_PORT = 5672;
    String RABBITMQ_USERNAME = "guest";
    String RABBITMQ_PASSWORD = "guest";
}

public class ProductOrder {
    private String orderId;
    private String type;
。。。 。。。
}

public class OrderProducer {
    private static final List<ProductOrder> orderList = new ArrayList<>();

    static {
        orderList.add(new ProductOrder("XXX001", "订单创建"));
        orderList.add(new ProductOrder("XXX001", "订单付款"));
        orderList.add(new ProductOrder("XXX001", "订单完成"));
        orderList.add(new ProductOrder("XXX002", "订单创建"));
        orderList.add(new ProductOrder("XXX002", "订单付款"));
        orderList.add(new ProductOrder("XXX002", "订单完成"));
        orderList.add(new ProductOrder("XXX003", "订单创建"));
        orderList.add(new ProductOrder("XXX003", "订单付款"));
        orderList.add(new ProductOrder("XXX003", "订单完成"));
    }

    public static void main(String[] args) throws Exception {
        //创建一个消息生产者,并设置一个消息生产者组
        DefaultMQProducer producer = new DefaultMQProducer("rocket_test_consumer_group");

        //指定 NameServer 地址
        producer.setNamesrvAddr(MQConstant.ROCKETMQ_NAMESERVER_ADDR);
        //初始化 Producer,整个应用生命周期内只需要初始化一次
        producer.start();

        for (int i = 0; i < orderList.size(); i++) {
            //获取当前order
            ProductOrder order = orderList.get(i);
            //创建一条消息对象,指定其主题、标签和消息内容
            Message message = new Message(
                    /* 消息主题名 */
                    "topicTest",
                    /* 消息标签 */
                    order.getOrderId(),
                    /* 消息内容 */
                    (order.toString()).getBytes(RemotingHelper.DEFAULT_CHARSET)
            );

            //发送消息并返回结果 使用hash选择策略
            SendResult sendResult = producer.send(message, new SelectMessageQueueByHash(), order.getOrderId());

            System.out.println("product: 发送状态:" + sendResult.getSendStatus() + ",存储queue:" + sendResult.getMessageQueue().getQueueId() + ",orderID:" + order.getOrderId() + ",type:" + order.getType());
        }

        // 一旦生产者实例不再被使用则将其关闭,包括清理资源,关闭网络连接等
        producer.shutdown();
    }
}

这里生产者使用的是根据附加参数Hash值,按照消息队列列表的大小取余数,得到消息队列的index,保证了同类消息的顺序一致性。

public class SelectMessageQueueByHash implements MessageQueueSelector {
    public SelectMessageQueueByHash() {
    }

    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        int value = arg.hashCode();
        if (value < 0) {
            value = Math.abs(value);
        }

        value %= mqs.size();
        return (MessageQueue)mqs.get(value);
    }
}

生产者顺序发送消息:

 消费者:

public class OrderConsumer {
    private static final Random random = new Random();

    public static void main(String[] args) throws Exception {
        //创建一个消息消费者,并设置一个消息消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocket_test_consumer_group");
        //指定 NameServer 地址
        consumer.setNamesrvAddr(MQConstant.ROCKETMQ_NAMESERVER_ADDR);
        //设置 Consumer 第一次启动时从队列头部开始消费还是队列尾部开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        //订阅指定 Topic 下的所有消息
        consumer.subscribe("topicTest", "*");

        //注册消费的监听 这里注意顺序消费为MessageListenerOrderly 之前并发为ConsumeConcurrentlyContext
        consumer.registerMessageListener(new MessageListenerOrderly() {
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext context) {
                //默认 list 里只有一条消息,可以通过设置参数来批量接收消息
                if (list != null) {
                    for (MessageExt ext : list) {
                        try {
                            try {
                                //模拟业务逻辑处理中...
                                TimeUnit.SECONDS.sleep(random.nextInt(10));
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                            //重试次数
                            int retryTimes = ext.getReconsumeTimes();
                            //获取接收到的消息
                            String message = new String(ext.getBody(), RemotingHelper.DEFAULT_CHARSET);
                            //获取队列ID
                            int queueId = context.getMessageQueue().getQueueId();
                            //打印消息
                            System.out.println("Consumer-线程名称=[" + Thread.currentThread().getId() + "],重试次数:[" + retryTimes + "],接收queueId:[" + queueId + "],接收时间:[" + new Date().getTime() + "],消息=[" + message + "]");

                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                //暂停当前队列
                int num = random.nextInt(10);
                if (num % 3 == 0) {
                    System.out.println("系统出现异常,阻塞当前队列...");
                    return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });

        // 消费者对象在使用之前必须要调用 start 初始化
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

消费者真正要达到消费顺序,需要分布式锁,所以这里需要将MessageListenerOrderly替换之前的MessageListenerConcurrently(并发),因为它里面实现了分布式锁。

public interface MessageListenerOrderly extends MessageListener {
    ConsumeOrderlyStatus consumeMessage(List<MessageExt> var1, ConsumeOrderlyContext var2);
}

消费者顺序消费消息,出现异常后消息会重试。

 2.4.如何保证消息顺序

RocketMQ架构本身是无法保证消息有序的,但是提供了相应的API保证消息有序消费。 RocketMQ API利用FIFO先进先出的特性,保证生产者消息有序进入同一队列,消费者在同一队列消费就能达到消息的有序消费。

• 使用MessageQueueSelector编写有序消息生产者

 有序消息生产者会按照一定的规则将消息发送到同一个队列中,从而保证同一个队列中的消息是有序的。RocketMQ 并不保证整个主题内所有队列的消息都是按照发送顺序排列的。

• 使用MessageListenerOrderly进行顺序消费与之对应的MessageListenerConcurrently并行消费(push模式)。/ ˈɔːdəli /

MessageListenerOrderlyRocketMQ专门提供的一种顺序消费的接口,它可以让消费者按照消息发送的顺序,一个一个地处理消息。这个接口支持按照消息的重试次数进行顺序消费、订单ID等作为消息键来实现顺序消费、批量消费等操作。

通过加锁的方式实现(有超时机制),一个队列同时只有一个消费者;并且存在一个定时任务,每隔一段时间就会延长锁的时间,直到整个消息队列全部消费结束。

• 消费端自己保证消息顺序消费(pull模式)。

• 消费者并发消费时设置消费线程为1。

RocketMQ的消费者可以开启多个消费线程同时消费同一个队列中的消息,如果要保证消息的顺序,需要将消费线程数设置为1。这样,在同一个队列中,每个消息只会被单个消费者线程消费,从而保证消息的顺序性

rokectMQ消息模型:

 上面我们介绍了顺序消息,它主要将相同的消息投递到一个队列中的,具体如何投递呢?

3.0.消息投递策略

上面我们介绍了顺序消息,但是RocketMQ还支持那些投递策略呢、RocketMQ的消息模型整体并不复杂,如下图所示:

 一个Topic(消息主题)可能对应多个实际的消息队列(MessgeQueue)

在底层实现上,为了提高MQ的可用性和灵活性,一个Topic在实际存储的过程中,采用了多队列的方式,具体形式如上图所示。每个消息队列在使用中应当保证先入先出(FIFO,First In First Out)的方式进行消费。

那么,基于这种模型,就会引申出两个问题:

• 生产者 在发送相同Topic的消息时,消息体应当被放置到哪一个消息队列(MessageQueue)中?

• 消费者 在消费消息时,应当从哪些消息队列中拉取消息?

3.1.生产者投递策略

生产者投递策略就是讲如何讲一个消息投递到不同的queue

3.1.1.轮询算法投递

默认投递方式:基于Queue队列轮询算法投递 

默认情况下,采用了最简单的轮询算法,这种算法有个很好的特性就是,保证每一个Queue队列的消息投递数量尽可能均匀

3.1.2.顺序投递策略

在有些场景下,需要保证同类型消息投递和消费的顺序性。

例如,假设现在有TOPIC topicTest ,该 Topic下有4个Queue队列 ,该Topic用于传递订单的状态变迁,假设订单有状态:未支付、已支付、发货中(处理中)、发货成功、发货失败。

在时序上,生产者从时序上可以生成如下几个消息:

订单T0000001:未支付 -->订单T0000001:已支付 -->订单T0000001:发货中(处理中) -->订单T0000001:发货失败

消息发送到MQ中之后,可能由于轮询投递的原因,消息在MQ的存储可能如下:

这种情况下,我们希望消费者消费消息的顺序和我们发送是一致的,然而,有上述MQ的投递和消费机制,我们无法保证顺序是正确的,对于顺序异常的消息,消费者即使有一定的状态容错,也不能完全处理好这么多种随机出现组合情况。

基于上述的情况, RockeMQ采用了这种实现方案:对于相同订单号的消息,通过一定的策略,将其放置在一个queue队列中 ,然后消费者再采用一定的策略(一个线程独立处理一个queue ,保证处理消息的顺序性),能够保证消费的顺序性。

 生产者在消息投递的过程中,使用了MessageQueueSelector作为队列选择的策略接口,只需要实现这个接口即可实现自定义投递策略。其定义如下:

public interface MessageQueueSelector {
    /**
     * 根据消息体和参数,从一批消息队列中挑选出一个合适的消息队列
     * @param mqs 待选择的MQ队列选择列表
     * @param msg 待发送的消息体
     * @param arg 附加参数
     * @return 选择后的队列
     */
    MessageQueue select(final List<MessageQueue> mqs, final Message msg, final Object arg);
}

3.1.3.投递策略实现

投递策略

策略实现类

说明

随机分配策略

SelectMessageQueueByRandom

使用了简单的随机数选择算法

基于Hash分配策略

SelectMessageQueueByHash

根据附加参数Hash值,按照消息队列列表的大小取余数,得到消息队列的index

基于机器机房位置分配策略

SelectMessageQueueByMachineRoom

开源的版本没有具体的实现,基本的目的应该是机器的就近原则分配

默认轮询策略

public class Producer {

    public static void main(String[] args) throws Exception {
        //创建一个消息生产者,并设置一个消息生产者组
        DefaultMQProducer producer = new DefaultMQProducer("rocket_test_consumer_group");

        //指定 NameServer 地址
        producer.setNamesrvAddr("192.168.52.50:9876");
        producer.setRetryTimesWhenSendFailed(3);
        producer.setRetryTimesWhenSendAsyncFailed(3);
        //初始化 Producer,整个应用生命周期内只需要初始化一次
        producer.start();

        for (int i = 0; i < 10; i++) {
            //创建一条消息对象,指定其主题、标签和消息内容
            Message msg = new Message(
                    /* 消息主题名 */
                    "topicTest",
                    /* 消息标签 */
                    "TagA",
                    /* 消息内容 */
                    ("Hello Java demo RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
            );

            //发送消息并返回结果
            SendResult sendResult = producer.send(msg);
            System.out.println("发送queueId:[" + sendResult.getMessageQueue().getQueueId() + "],偏移量offset:[" + sendResult.getQueueOffset() + "],发送状态:[" + sendResult.getSendStatus() + "]");
        }

        // 一旦生产者实例不再被使用则将其关闭,包括清理资源,关闭网络连接等
        producer.shutdown();
    }
}

 随机分配策略

//发送消息并返回结果
SendResult sendResult = producer.send(msg, new SelectMessageQueueByRandom(), null);

//随机分配

public class SelectMessageQueueByRandom implements MessageQueueSelector {
    private Random random = new Random(System.currentTimeMillis());

    public SelectMessageQueueByRandom() {
    }

    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        int value = this.random.nextInt(mqs.size());
        return (MessageQueue)mqs.get(value);
    }
}

 基于Hash分配策略在顺序消息已经演示过了。

//发送消息并返回结果
SendResult sendResult = producer.send(msg, new SelectMessageQueueByHash(), null);

3.2.消费者分配队列

3.2.1.消费者消费消息形式

RocketMQ对于消费者消费消息有两种形式:

• BROADCASTING :广播式消费,这种模式下,一个消息会被通知到每一个消费者

• CLUSTERING : 集群式消费,这种模式下,一个消息最多只会被投递到一个消费者上进行消费

模式如下:

对于使用了消费模式为MessageModel.CLUSTERING进行消费时,需要保证一个消息在整个集群中只需要被消费一次。实际上,在RoketMQ底层,消息指定分配给消费者的实现,是通过queue队列分配给消费者的方式完成的:也就是说,消息分配的单位是消息所在的queue队列queue队列指定给特定的消费者后,queue队列内的所有消息将会被指定到消费者进行消费。

RocketMQ定义了策略接口AllocateMessageQueueStrategy ,对于给定的消费者分组消息队列列表消费者列表当前消费者应当被分配到哪些queue队列 ,定义如下:

/**
 * 为消费者分配queue的策略算法接口
 */
public interface AllocateMessageQueueStrategy {
    /**
     * Allocating by consumer id
     *
     * @param consumerGroup 当前 consumer群组
     * @param currentCID 当前consumer id
     * @param mqAll 当前topic的所有queue实例引用
     * @param cidAll 当前 consumer群组下所有的consumer id set集合
     * @return 根据策略给当前consumer分配的queue列表
     */
    List<MessageQueue> allocate(
            final String consumerGroup,
            final String currentCID,
            final List<MessageQueue> mqAll,
            final List<String> cidAll
    );
    /**
     * 算法名称
     *
     * @return The strategy name
     */
    String getName();
}

相应地,RocketMQ提供了如下几种实现:

算法名称

含义

AllocateMessageQueueAveragely 

平均分配算法

AllocateMessageQueueAveragelyByCircle 

基于环形平均分配算法

AllocateMachineRoomNearby

基于机房邻近原则算法

AllocateMessageQueueByMachineRoom 

基于机房分配算法

AllocateMessageQueueConsistentHash 

基于一致性Hash算法

AllocateMessageQueueByConfig

基于配置分配算法

为了讲述清楚上述算法的基本原理,我们先假设一个例子,下面所有的算法将基于这个例子讲解。

假设当前同一个topic下有queue队列10个,消费者共有4个。

3.2.2.平均分配算法

这里所谓的平均分配算法,并不是指的严格意义上的完全平均,如上面的例子中,10个queue,而消费者只有4个,无法是整除关系,除了整除之外的多出来的queue,将依次根据消费者的顺序均摊。

按照例子来看,10/4=2 ,即表示每个 消费者 平均均摊2个queue;而 10%4=2 ,即除了均摊之外,多出来2个queue还没有分配,那么,根据消费者的顺序consumer-1、 consumer-2、 consumer-3、consumer-4 ,则多出来的2个queue将分别给consumer-1和consumer-2。

最终,分摊关系如下:

• consumer-1 :3个

• consumer-2 :3个

• consumer-3 :2个

• consumer-4 :2个

3.2.3.一致性hash分配算法

使用这种算法,会将consumer消费者作为Node节点构造成一个hash环,然后queue队列通过这个hash环来决定被分配给哪个consumer消费者。

其基本模式如下:

 一致性hash算法用于在分布式系统中,保证数据的一致性而提出的一种基于hash环实现的算法。

3.2.4.分配算法实现

在创建消费者的时候如果不指定分配算法,默认消费者使用AllocateMessageQueueAveragely平均分配策略。

//创建一个消息消费者,并设置一个消息消费者组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(null, "rocket_test_consumer_group");

public DefaultMQPushConsumer(final String namespace, final String consumerGroup) {
    this(namespace, consumerGroup, null, new AllocateMessageQueueAveragely());
}

public DefaultMQPushConsumer(final String namespace, final String consumerGroup, RPCHook rpcHook,
    AllocateMessageQueueStrategy allocateMessageQueueStrategy) {
    this.consumerGroup = consumerGroup;
    this.namespace = namespace;
    this.allocateMessageQueueStrategy = allocateMessageQueueStrategy;
    defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(this, rpcHook);
}

如果需要使用其他分配策略,在创建consumer的时候指定对应的分配策略即可。

//创建一个消息消费者,并设置一个消息消费者组,并指定使用一致性hash算法的分配策略
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(null,"rocket_test_consumer_group",null,new AllocateMessageQueueConsistentHash());
.....

4.0. RocketMQ消息保障

下面我们详细说下如何保障消息不丢失以及消息幂等性问题

4.1.生产端保障

生产端保障需要从以下几个方面来保障

1.使用可靠的消息发送方式

2.注意生产端重试

3.生产禁止自动创建topic

4.1.1.消息发送保障

1. 同步发送

发送者向MQ执行发送消息API时,同步等待,直到消息服务器返回发送结果,会在收到接收方发回响应之后才发下一个数据包的通讯方式,这种方式只有在消息完全发送完成之后才返回结果,此方式存在需要同步等待发送结果的时间代价。

 简单来说,同步发送就是指producer发送消息后,会在接收到broker响应后才继续发下一条消息的通信方式。

使用场景:

由于这种同步发送的方式确保了消息的可靠性,同时也能及时得到消息发送的结果,故而适合一些发送比较重要的消息场景,比如说重要的通知邮件、营销短信等等。在实际应用中,这种同步发送的方式还是用得比较多的。

注意事项:

这种方式具有内部重试机制,即在主动声明本次消息发送失败之前,内部实现将重试一定次数,默认为2次( DefaultMQProducergetRetryTimesWhenSendFailed )。 发送的结果存在同一个消息可能被多次发送给broker,这里需要应用的开发者自己在消费端处理幂等性问题。

默认重试次数2次,一共会发送三次

//创建一个消息生产者,并设置一个消息生产者组
DefaultMQProducer producer = new DefaultMQProducer("rocket_test_consumer_group");

//指定 NameServer 地址
producer.setNamesrvAddr("127.0.0.1:9876");
producer.setRetryTimesWhenSendFailed(2);

// 默认重试次数

private int retryTimesWhenSendFailed = 2;

public void setRetryTimesWhenSendFailed(int retryTimesWhenSendFailed) {
    this.retryTimesWhenSendFailed = retryTimesWhenSendFailed;
}

跟踪消息发送代码可以看到一共发送了3次,2次是重试。

producer.send(msg);

public SendResult send(
    Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    Validators.checkMessage(msg, this);
    msg.setTopic(withNamespace(msg.getTopic()));
    return this.defaultMQProducerImpl.send(msg);
}

public SendResult send(
    Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    return send(msg, this.defaultMQProducer.getSendMsgTimeout());
}

public SendResult send(Message msg,
    long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    return this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout);
}

private SendResult sendDefaultImpl(
    Message msg,
    final CommunicationMode communicationMode,
    final SendCallback sendCallback,
    final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    this.makeSureStateOK();
    Validators.checkMessage(msg, this.defaultMQProducer);
    final long invokeID = random.nextLong();
    long beginTimestampFirst = System.currentTimeMillis();
    long beginTimestampPrev = beginTimestampFirst;
    long endTimestamp = beginTimestampFirst;
    TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
    if (topicPublishInfo != null && topicPublishInfo.ok()) {
        boolean callTimeout = false;
        MessageQueue mq = null;
        Exception exception = null;
        SendResult sendResult = null;
        int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
        int times = 0;
        String[] brokersSent = new String[timesTotal];
        for (; times < timesTotal; times++) {
            String lastBrokerName = null == mq ? null : mq.getBrokerName();
            MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);

。。。 。。。

2. 异步发送

异步发送是指发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。MQ的异步发送,需要用户实现异步发送回调接口( SendCallback )。

 异步发送是指producer发出一条消息后,不需要等待broker响应,就接着发送下一条消息的通信方式。需要注意的是,不等待broker响应,并不意味着broker不响应,而是通过回调接口来接收broker的响应。所以要记住一点,异步发送同样可以对消息的响应结果进行处理。

使用场景:

由于异步发送不需要等待broker的响应,故在一些比较注重RT(响应时间)的场景就会比较适用。比如,在一些视频上传的场景,我们知道视频上传之后需要进行转码,如果使用同步发送的方式来通知启动转码服务,那么就需要等待转码完成才能发回转码结果的响应,由于转码时间往往较长,很容易造成响应超时。此时,如果使用的是异步发送通知转码服务,那么就可以等转码完成后,再通过回调接口来接收转码结果的响应了。

注意:RocketMQ内部只对同步模式做了重试,异步发送模式是没有自动重试的,需要自己手动实现。

异步发送重试次数设置,使用producer对象设置。

//创建一个消息生产者,并设置一个消息生产者组
DefaultMQProducer producer = new DefaultMQProducer("rocket_test_consumer_group");
//指定 NameServer 地址
producer.setNamesrvAddr("127.0.0.1:9876");

producer.setRetryTimesWhenSendAsyncFailed(3);

public void setRetryTimesWhenSendAsyncFailed(final int retryTimesWhenSendAsyncFailed) {
    this.retryTimesWhenSendAsyncFailed = retryTimesWhenSendAsyncFailed;
}

4.1.2.消息发送总结

1. 发送方式对比

发送方式

发送TPS

发送结果反馈

可靠性

适用场景

同步发送

一般

不丢失

重要的通知场景

异步发送

不丢失

比较注重RT(响应时间)的场景

单向发送

最快

可能丢失

可靠性要求并不高的场景

2. 使用场景对比

在实际使用场景中,利用何种发送方式,可以总结如下:

• 当发送的消息不重要时,采用one-way方式,以提高吞吐量;

• 当发送的消息很重要是,且对响应时间不敏感的时候采用sync方式;

• 当发送的消息很重要,且对响应时间非常敏感的时候采用async方式;

4.1.3.发送状态

发送消息时,将获得包含SendStatusSendResult。首先,我们假设MessageisWaitStoreMsgOK = true(默认为true),如果没有抛出异常,我们将始终获得SEND_OK,以下是每个状态的说明列表:

1.FLUSH_DISK_TIMEOUT

如果设置了FlushDiskType=SYNC_FLUSH (默认是 ASYNC_FLUSH),并且Broker没有在syncFlushTimeout (默认是 5 秒)设置的时间内完成刷盘,就会收到此状态码。

2.FLUSH_SLAVE_TIMEOUT 

如果设置为SYNC_MASTER ,并且slave Broker没有在syncFlushTimeout设定时间内完成同步,就会收到此状态码。

3. SLAVE_NOT_AVAILABLE

如果设置为SYNC_MASTER,并没有配置slave Broker,就会收到此状态码。

4.SEND_OK

这个状态可以简单理解为,没有发生上面列出的三个问题状态就是SEND_OK。需要注意的是,SEND_OK 并不意味着可靠,如果想严格确保没有消息丢失,需要开启 SYNC_MASTER or SYNC_FLUSH

5.注意事项

如果收到了FLUSH_DISK_TIMEOUT, FLUSH_SLAVE_TIMEOUT,意味着消息会丢失,有2个选择,一是无所谓,适用于消息不关紧要的场景,二是重发,但可能产生消息重复,这就需要consumer进行去重控制。如果收到了SLAVE_NOT_AVAILABLE就要赶紧通知管理员了。

4.2.4.MQ发送端重试保障

如果由于网络抖动等原因,Producer程序向Broker发送消息时没有成功,即发送端没有收到BrokerACK,导致最终Consumer无法消费消息,此时RocketMQ会自动进行重试。

DefaultMQProducer可以设置消息发送失败的最大重试次数,并可以结合发送的超时时间来进行重试的处理,具体API如下:

//设置消息发送失败时的最大重试次数
public void setRetryTimesWhenSendFailed(int retryTimesWhenSendFailed) {
    this.retryTimesWhenSendFailed = retryTimesWhenSendFailed;
}
//同步发送消息,并指定超时时间
public SendResult send(Message msg, long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    return this.defaultMQProducerImpl.send(msg, timeout);
}

重试问题:

超时重试针对网上说的超时异常会重试的说法都是错误的

是因为下面测试代码的超时时间设置为5毫秒,按照正常肯定会报超时异常,但设置1次重试和3000次的重试,虽然最终都会报下面异常,但输出错误时间报显然不应该是一个级别。但测试发现无论设置的多少次的重试次数,报异常的时间都差不多。

测试代码

public class RetryProducer {
    public static void main(String[] args) throws UnsupportedEncodingException, InterruptedException, RemotingException, MQClientException, MQBrokerException {
        // 创建一个消息生产者,并设置一个消息生产者组
        DefaultMQProducer producer = new DefaultMQProducer("rocket_test_consumer_group");
        // 指定 NameServer 地址
        producer.setNamesrvAddr("127.0.0.1:9876");
        // 设置重试次数(默认2次)
        producer.setRetryTimesWhenSendFailed(300000);
        // 初始化 Producer,整个应用生命周期内只需要初始化一次
        producer.start();
        Message msg = new Message(
                /* 消息主题名 */
                "topicTest",
                /* 消息标签 */
                "TagA",
                /* 消息内容 */
                ("Hello Java demo RocketMQ").getBytes(RemotingHelper.DEFAULT_CHARSET));
        // 发送消息并返回结果,设置超时时间 5ms 所以每次都会发送失败
        SendResult sendResult = producer.send(msg, 5);
        System.out.printf("%s%n", sendResult);
        // 一旦生产者实例不再被使用则将其关闭,包括清理资源,关闭网络连接等
        producer.shutdown();
    }
}

揭晓答案

针对这个疑惑,需要查看源码,发现只有同步发送才会重试,并且超时是不重试的

/**
 * 说明 抽取部分代码
 */
private SendResult sendDefaultImpl(Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout) {
    //1、获取当前时间
    long beginTimestampFirst = System.currentTimeMillis();
    long beginTimestampPrev;
    //2、去服务器看下有没有主题消息
    TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
    if (topicPublishInfo != null && topicPublishInfo.ok()) {
        boolean callTimeout = false;
        //3、通过这里可以很明显看出 如果不是同步发送消息 那么消息重试只有1次
        int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
        //4、根据设置的重试次数,循环再去获取服务器主题消息
        for (times = 0; times < timesTotal; times++) {
            MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
            beginTimestampPrev = System.currentTimeMillis();
            long costTime = beginTimestampPrev - beginTimestampFirst;
            //5、前后时间对比 如果前后时间差 大于 设置的等待时间那么直接跳出for循环了,这就说明连接超时是不进行多次连接重试的
            if (timeout < costTime) {
                callTimeout = true;
                break;
            }
            //6、如果超时直接报错
            if (callTimeout) {
                throw new RemotingTooMuchRequestException("sendDefaultImpl call timeout");
            }
        }
    }
}

重试总结:

通过这段源码很明显可以看出以下几点

1.如果是异步发送那么重试次数只有1

2.对于同步而言,超时异常也是不会再去重试

3.如果发生重试是在一个for循环里去重试,所以它是立即重试而不是隔一段时间去重试。

4.1.5.禁止自动创建topic

自动创建TOPIC流程:

autoCreateTopicEnable 设置为true 标识开启自动创建topic

1. 消息发送时如果根据topic没有获取到路由信息,则会根据默认的topic去获取,获取到路由信息后选择一个队列进行发送,发送时报文会带上默认的topic以及默认的队列数量。

2. 消息到达broker后,broker检测没有topic的路由信息,则查找默认topic的路由信息,查到表示开启了自动创建topic,则会根据消息内容中的默认的队列数量在本broker上创建topic,然后进行消息存储。

3. broker创建topic后并不会马上同步给name server,而是每30秒进行汇报一次,更新name server上的topic路由信息,producer会每30s进行拉取一次topic的路由信息,更新完成后就可以正常发送消息。更新之前一直都是按照默认的topic查找路由信息。

为什么不能开启自动创建:

上述 broker 中流程会有一个问题,就是在producer更新路由信息之前的这段时间,如果消息只发送到了broker-a,则broker-b上不会创建这个topic的路由信息,broker互相之间不通信。当producer更新之后,获取到的topic列表只有broker-a,就永远不会轮询到broker-b的队列(因为没有路由信息),所以我们生产通常关闭自动创建topic,而是采用手动创建的方式。

4.1.6.发送端规避

发送端规避

注意了,这里我们发现,有可能在实际的生产过程中,我们的RocketMQ有几台服务器构成的集群。其中有可能是一个主题TopicA中的4个队列分散在Broker1、Broker2、Broker3服务器上。

 如果这个时候Broker2挂了,我们知道,但是生产者不知道(因为生产者客户端每隔30S更新一次路由,但是Name ServerBroker之间的心跳检测间隔是10S,所以生产者最快也需要30S才能感知Broker2 挂了),所以发送到queue2的消息会失败,RocketMQ发现这次消息发送失败后,就会将Broker2排除在消息的选择范围,下次再次发送消息时就不会发送到 Broker2,这样做的目的就是为了提高发送消息的成功率。

问题梳理:

例如在发送之前sendWhichQueue该值为broker-a的q1,如果由于此时broker-a的突发流量异常大导致消息发送失败,会触发重试,按照轮循机制,下一个选择的队列为broker-a的q2队列,此次消息发送大概率还是会失败,即尽管会重试2次,但都是发送给同一个Broker处理,此过程会显得不那么靠谱,即大概率还是会失败,那这样重试的意义将大打折扣。

RocketMQ为了解决该问题,引入了故障规避机制,在消息重试的时候,会尽量规避上一次发送的Broker,回到上述示例,当消息发往broker-a q1队列时返回发送失败,那重试的时候,会先排除broker-a中所有队列,即这次会选择broker-b q1队列,增大消息发送的成功率。上述规避思路是默认生效的,即无需干预。

规避策略:

RocketMQ提供了两种规避策略,该参数由sendLatencyFaultEnable控制,用户可干预,表示是否开启延迟规避机制,默认为不开启。(DefaultMQProducer中设置这两个参数,sendLatencyFaultEnable设置为false(默认值),不开启,延迟规避策略只在重试时生效,例如在一次消息发送过程中如果遇到消息发送失败,规避broekr-a,但是在下一次消息发时,即再次调用DefaultMQProducersend方法发送消息时,还是会选择broker-a的消息进行发送,只有继续发送失败后,重试时才会再次规避broker-a。

sendLatencyFaultEnable设置为true:开启延迟规避机制,一旦消息发送失败会将broker-a "悲观"地认为在接下来的一段时间内该Broker不可用,在为未来某一段时间内所有的客户端不会向该Broker发送消息。这个延迟时间就是通过notAvailableDuration, latencyMax共同计算的,首先先计算本次消息发送失败所耗的时延,然后对应latencyMax中哪个区间,即计算在latencyMax的下标,然后返回notAvailableDuration同一个下标对应的延迟值。

注意事项:

如果所有的Broker都触发了故障规避,并且Broker只是那一瞬间压力大,那岂不是明明存在可用的Broker,但经过你这样规避,反倒是没有Broker可用来,那岂不是更糟糕了?针对这个问题,会退化到队列轮询机制,即不考虑故障规避这个因素,按自然顺序进行选择进行兜底。

4.2.消费端保障

4.2.1.注意幂等性

应用程序在使用RocketMQ进行消息消费时必须支持幂等消费,即同一个消息被消费多次和消费一次的结果一样。这一点在使用RoketMQ或者分析RocketMQ源代码之前再怎么强调也不为过。

“至少一次送达”的消息交付策略,和消息重复消费是一对共生的因果关系要做到不丢消息就无法避免消息重复消费。原因很简单,试想一下这样的场景:客户端接收到消息并完成了消费,在消费确认过程中发生了通讯错误。从Broker的角度是无法得知客户端是在接收消息过程中出错还是在消费确认过程中出错。为了确保不丢消息,重发消息是唯一的选择。

有了消息幂等消费约定的基础,RocketMQ就能够有针对性地采取一些性能优化措施,例如:并行消费、消费进度同步机制等,这也是RocketMQ性能优异的原因之一。

4.2.2.消息消费模型

从不同的维度划分,Consumer支持以下消费模式:

• 广播消费模式下,消息消费失败不会进行重试,消费进度保存在Consumer端;

• 集群消费模式下,消息消费失败有机会进行重试,消费进度集中保存在Broker端。

4.2.2.1.集群消费

使用相同Group ID的订阅者属于同一个集群,同一个集群下的订阅者消费逻辑必须完全一致(包括Tag的使用),这些订阅者在逻辑上可以认为是一个消费节点

注意事项:

• 消费端集群化部署,每条消息只需要被处理一次。

• 由于消费进度在服务端维护,可靠性更高。

• 集群消费模式下,每一条消息都只会被分发到一台机器上处理。如果需要被集群下的每一台机器都处理,请使用广播模式。

• 集群消费模式下,不保证每一次失败重投的消息路由到同一台机器上,因此处理消息时不应该做任何确定性假设。

4.2.2.2.广播消费

广播消费指的是:一条消息被多个consumer消费,即使这些consumer属于同一个 ConsumerGroup,消息也会被ConsumerGroup中的每个Consumer都消费一次,广播消费中 ConsumerGroup概念可以认为在消息划分方面无意义。

 注意事项:

• 广播消费模式下不支持顺序消息。

• 广播消费模式下不支持重置消费位点。

• 每条消息都需要被相同逻辑的多台机器处理。

• 消费进度在客户端维护,出现重复的概率稍大于集群模式。

• 广播模式下,消息队列RocketMQ保证每条消息至少被每台客户端消费一次,但是并不会对消费失败的消息进行失败重投,因此业务方需要关注消费失败的情况。

• 广播模式下,客户端每一次重启都会从最新消息消费。客户端在被停止期间发送至服务端的消息将会被自动跳过,请谨慎选择。

• 广播模式下,每条消息都会被大量的客户端重复处理,因此推荐尽可能使用集群模式。

• 目前仅Java客户端支持广播模式。

• 广播模式下服务端不维护消费进度,所以消息队列RocketMQ控制台不支持消息堆积查询、消息堆积报警和订阅关系查询功能。

4.2.2.3.集群模式模拟广播

如果业务需要使用广播模式,也可以创建多个Group ID,用于订阅同一个Topic

 注意事项:

• 每条消息都需要被多台机器处理,每台机器的逻辑可以相同也可以不一样。

• 消费进度在服务端维护,可靠性高于广播模式。

• 对于一个Group ID来说,可以部署一个消费端实例,也可以部署多个消费端实例。当部署多个消费端实例时,实例之间又组成了集群模式(共同分担消费消息)。假设Group ID 1部署了三个消费者实例 C1、C2、C3,那么这三个实例将共同分担服务器发送给Group ID 1 的消息。同时,实例之间订阅关系必须保持一致。

4.2.2.4.代码示例

1. 集群消费(生产者)

public class Producer {

    public static void main(String[] args) throws Exception {
        //创建一个消息生产者,并设置一个消息生产者组
        DefaultMQProducer producer = new DefaultMQProducer("rocket_test_consumer_group");

        //指定 NameServer 地址
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.setRetryTimesWhenSendFailed(3);
        producer.setRetryTimesWhenSendAsyncFailed(3);
        //初始化 Producer,整个应用生命周期内只需要初始化一次
        producer.start();
        
        //创建一条消息对象,指定其主题、标签和消息内容
        Message msg = new Message(
                /* 消息主题名 */
                "topicTest",
                /* 消息标签 */
                "TagA",
                /* 消息内容 */
                ("Hello Java demo RocketMQ ").getBytes(RemotingHelper.DEFAULT_CHARSET)
        );

        //发送消息并返回结果
        SendResult sendResult = producer.send(msg);
        System.out.println("发送queueId:[" + sendResult.getMessageQueue().getQueueId() + "],偏移量offset:[" + sendResult.getQueueOffset() + "],发送状态:[" + sendResult.getSendStatus() + "]");

        // 一旦生产者实例不再被使用则将其关闭,包括清理资源,关闭网络连接等
        producer.shutdown();
    }
}

发送一条消息

 集群消费(消费者)

/**
 * 集群消费
 */
public class ClusterConsumer {
    public static void main(String[] args) throws Exception {
        //创建一个消息消费者,并设置一个消息消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocket_test_consumer_group", null, new AllocateMessageQueueAveragelyByCircle());
        //指定 NameServer 地址
        consumer.setNamesrvAddr(MQConstant.ROCKETMQ_NAMESERVER_ADDR);
        //设置 Consumer 第一次启动时从队列头部开始消费还是队列尾部开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        //设置集群消费
        consumer.setMessageModel(MessageModel.CLUSTERING);
        //订阅指定 Topic 下的所有消息
        consumer.subscribe("topicTest", "*");

        //注册消息监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                //默认 list 里只有一条消息,可以通过设置参数来批量接收消息
                if (list != null) {
                    for (MessageExt ext : list) {
                        try {
                            String message = new String(ext.getBody(), RemotingHelper.DEFAULT_CHARSET);
                            System.out.println("Consumer-线程名称=[" + Thread.currentThread().getId() + "],接收时间:[" + new Date().getTime() + "],消息=[" + message + "],接收brokerName:[" + ext.getBrokerName() + "],接收queueId:[" + ext.getQueueId() + "],偏移量offset:[" + ext.getQueueOffset() + "]");
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        // 消费者对象在使用之前必须要调用 start 初始化
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

启动两个消费者,可以看到同时只有一个消费者在消费。

 消费者默认消费方式就是集群消费方式。

//设置集群消费
consumer.setMessageModel(MessageModel.CLUSTERING);

public enum MessageModel {

//广播
    BROADCASTING("BROADCASTING"),

//集群
    CLUSTERING("CLUSTERING");
。。。 。。。
}

2.如果设置为广播消费,则生产者发送一条消息,所有的消费者都会收到消息。

/**
 * 广播消费
 */
public class BroadcastConsumer {
    public static void main(String[] args) throws Exception {
        //创建一个消息消费者,并设置一个消息消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocket_test_consumer_group");
        //指定 NameServer 地址
        consumer.setNamesrvAddr("192.168.52.50:9876");
        //设置 Consumer 第一次启动时从队列头部开始消费还是队列尾部开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        //设置广播消费
        consumer.setMessageModel(MessageModel.BROADCASTING);
        //订阅指定 Topic 下的所有消息
        consumer.subscribe("topicTest", "*");

        //注册消息监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                //默认 list 里只有一条消息,可以通过设置参数来批量接收消息
                if (list != null) {
                    for (MessageExt ext : list) {
                        try {
                            System.out.println(new String(ext.getBody(), RemotingHelper.DEFAULT_CHARSET));
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        // 消费者对象在使用之前必须要调用 start 初始化
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

3.集群模式模拟广播

创建两个组:rocket_test_consumer_grouprocket_test_consumer_group_0

消息会发送到每个消费者组,类似广播消费。同组的消费者只会有一个消费者消费消息。

4.2.3.消息消费模式

RocketMQ消息消费本质上是基于的拉(pull)模式,consumer主动向消息服务器broker拉取消息。

• 推消息模式下,消费进度的递增是由RocketMQ内部自动维护的;

• 拉消息模式下,消费进度的变更需要上层应用自己负责维护,RocketMQ只提供消费进度保存和查询功能。  

推模式(PUSH):

我们上面使用的消费者都是PUSH模式,也是最常用的消费模式。

由消息中间件(MQ消息服务器代理)主动地将消息推送给消费者;采用Push方式,可以尽可能实时地将消息发送给消费者进行消费。但是,在消费者的处理消息的能力较弱的时候(比如,消费者端的业务系统处理一条消息的流程比较复杂,其中的调用链路比较多导致消费时间比较久。概括起来地说就是“慢消费问题”),而MQ不断地向消费者Push消息,消费者端的缓冲区可能会溢出,导致异常。

实现方式,代码上使用 DefaultMQPushConsumer

consumer把轮询过程封装了,并注册MessageListener监听器,取到消息后,唤醒MessageListenerconsumeMessage()来消费,对用户而言,感觉消息是被推送(push)过来的。主要用的也是这种方式。

推模型代码:

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

import java.io.UnsupportedEncodingException;
import java.util.List;

public class LitePullConsumer {

    public static void main(String[] args) throws MQClientException {
        // 创建DefaultLitePullConsumer实例
        DefaultLitePullConsumer consumer = new DefaultLitePullConsumer("rocket_test_consumer_group");
        //指定 NameServer 地址
        consumer.setNamesrvAddr( "127.0.0.1:9876");
        //设置 Consumer 第一次启动时从队列头部开始消费还是队列尾部开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        //订阅指定 Topic 下的所有消息
        consumer.subscribe("topicTest", "*");
        // 消费者对象在使用之前必须要调用 start 初始化
        consumer.start();

        try {
            //循环开始消费消息
            while (true) {
                //从consumer中获取消息
                List<MessageExt> list = consumer.poll();
                //消息处理
                if (list != null) {
                    for (MessageExt ext : list) {
                        try {
                            System.out.println(new String(ext.getBody(), RemotingHelper.DEFAULT_CHARSET));
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                //提交偏移量
                consumer.commitSync();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 一旦生产者实例不再被使用则将其关闭,包括清理资源,关闭网络连接等
            consumer.shutdown();
        }
    }
}

不需要对消息队列的做任何操作,直接拉取消费消息。

拉模式(PULL):

RocketMQPUSH模式是由PULL模式来实现的。

由消费者客户端主动向消息中间件(MQ消息服务器代理)拉取消息;采用Pull方式,如何设置Pull消息的频率需要重点去考虑,举个例子来说,可能1分钟内连续来了1000条消息,然后2小时内没有新消息产生(概括起来说就是“消息延迟与忙等待”)。如果每次Pull的时间间隔比较久,会增加消息的延迟,即消息到达消费者的时间加长,MQ中消息的堆积量变大;若每次Pull的时间间隔较短,但是在一段时间内MQ中并没有任何消息可以消费,那么会产生很多无效的Pull请求的RPC开销,影响MQ整体的网络性能,实时性不高,需要频繁的去服务器拉取数据。

拉模型代码:

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.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.common.RemotingHelper;

import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class DefaultPullConsumer {

    // 记录每个队列的消费进度
    private static final Map<MessageQueue, Long> OFFSE_TABLE = new HashMap<MessageQueue, Long>();

    public static void main(String[] args) throws MQClientException {
        //创建一个消息消费者,并设置一个消息消费者组
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("rocket_test_consumer_group");
        //指定 NameServer 地址
        consumer.setNamesrvAddr("127.0.0.1:9876");
        // 消费者对象在使用之前必须要调用 start 初始化
        consumer.start();

        // 获取Topic的所有队列
        Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("topicTest");

        //遍历所有队列
        for (MessageQueue mq : mqs) {
            System.out.printf("Consume from the queue: %s%n", mq);
            SINGLE_MQ:
            while (true) {
                try {
                    //拉取消息,arg1=消息队列,arg2=tag消息过滤,arg3=消息队列,arg4=一次最大拉去消息数量
                    PullResult pullResult =
                            consumer.pullBlockIfNotFound(mq, "*", getMessageQueueOffset(mq), 32);
                    //从consumer中获取消息
                    List<MessageExt> list = pullResult.getMsgFoundList();

                    //消息处理
                    if (list != null) {
                        for (MessageExt ext : list) {
                            try {
                                System.out.println(new String(ext.getBody(), RemotingHelper.DEFAULT_CHARSET));
                            } catch (UnsupportedEncodingException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                    //将消息放入hash表中,存储该队列的消费进度
                    putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
                    switch (pullResult.getPullStatus()) {
                        case FOUND:  // 找到消息,输出
                            System.out.println(pullResult.getMsgFoundList().get(0));
                            break;
                        case NO_MATCHED_MSG:  // 没有匹配tag的消息
                            System.out.println("无匹配消息");
                            break;
                        case NO_NEW_MSG:  // 该队列没有新消息,消费offset=最大offset
                            System.out.println("没有新消息");
                            break SINGLE_MQ;  // 跳出该队列遍历
                        case OFFSET_ILLEGAL:  // offset不合法
                            System.out.println("Offset不合法");
                            break;
                        default:
                            break;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        //关闭Consumer
        consumer.shutdown();
    }

    /**
     * 从Hash表中获取当前队列的消费offset
     *
     * @param mq 消息队列
     * @return long类型 offset
     */
    private static long getMessageQueueOffset(MessageQueue mq) {
        Long offset = OFFSE_TABLE.get(mq);
        if (offset != null)
            return offset;

        return 0;
    }

    /**
     * 将消费进度更新到Hash表
     *
     * @param mq     消息队列
     * @param offset offset
     */
    private static void putMessageQueueOffset(MessageQueue mq, long offset) {
        OFFSE_TABLE.put(mq, offset);
    }
}

可以看到拉模型非常的麻烦,需要自己维护消息队列的状态,流控(消息过多本地内存会溢出)、偏移量等等。

注意事项:

注意:RocketMQ 4.6.0版本后将弃用DefaultMQPullConsumerDefaultMQPullConsumer方式需要手动管理偏移量,官方已经被废弃,将在2022年进行删除。

 DefaultLitePullConsumer该类是官方推荐使用的手动拉取的实现类,偏移量提交由RocketMQ管理,不需要手动管理。

4.2.4.消息确认机制

consumer的每个实例是靠队列分配来决定如何消费消息的。那么消费进度具体是如何管理的,又是如何保证消息成功消费的?(RocketMQ有保证消息肯定消费成功的特性,失败则重试)

为了保证数据不被丢失,RocketMQ支持消息确认机制,即ack。发送者为了保证消息肯定消费成功,只有使用方明确表示消费成功,RocketMQ才会认为消息消费成功。中途断电,抛出异常等都不会认为成功——即都会重新投递。

确认消费:

业务实现消费回调的时候,当且仅当此回调函数返回

ConsumeConcurrentlyStatus.CONSUME_SUCCESS RocketMQ才会认为这批消息(默认1条)是消费完成的。 

consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        System.out.println(Thread.currentThread().getName() + " Receive New Messages: " + msgs);
        execute();//执行真正消费
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
})

消费异常:

如果这时候消息消费失败,例如数据库异常,余额不足扣款失败等一切业务认为消息需要重试的场景,只要返回ConsumeConcurrentlyStatus.RECONSUME_LATER RocketMQ就会认为这批消息消费失败了。

consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        System.out.println(Thread.currentThread().getName() + " Receive New Messages: " + msgs);
        execute();//执行真正消费
        return ConsumeConcurrentlyStatus.RECONSUME_LATER
    }
})

为了保证消息是肯定被至少消费成功一次,RocketMQ会把这批消息重发回Broker(topic不是原topic而是这个消费组的retry topic),在延迟的某个时间点(默认是10秒,业务可设置)后,再次投递到这个ConsumerGroup。而如果一直这样重复消费都持续失败到一定次数(默认16次),就会投递到DLQ死信队列。应用可以监控死信队列来做人工干预。

4.2.5.消息重试机制

顺序队列顺序消息的重试:

对于顺序消息,当消费者消费消息失败后,消息队列RocketMQ版会自动不断地进行消息重试(每次间隔时间为1秒),这时,应用会出现消息消费被阻塞的情况。因此,建议您使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。

集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确ConsumeOrderlyStatus 状态返回SUSPEND_CURRENT_QUEUE_A_MOMENT

public enum ConsumeOrderlyStatus {
    // 消费成功
    SUCCESS,
    // 回滚
    @Deprecated
    ROLLBACK,
    // 提交
    @Deprecated
    COMMIT,
    // 暂停当前队列消费
    SUSPEND_CURRENT_QUEUE_A_MOMENT;
}

无需重试或者有异常时不需要重试,则返回return ConsumeOrderlyStatus.SUCCESS;

生产者发送消息

public class Producer {

    public static void main(String[] args) throws Exception {
        //创建一个消息生产者,并设置一个消息生产者组
        DefaultMQProducer producer = new DefaultMQProducer("rocket_test_consumer_group");

        //指定 NameServer 地址
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.setRetryTimesWhenSendFailed(3);
        producer.setRetryTimesWhenSendAsyncFailed(3);
        //初始化 Producer,整个应用生命周期内只需要初始化一次
        producer.start();
        
        //创建一条消息对象,指定其主题、标签和消息内容
        Message msg = new Message(
                /* 消息主题名 */
                "topicTest",
                /* 消息标签 */
                "TagA",
                /* 消息内容 */
                ("Hello Java demo RocketMQ ").getBytes(RemotingHelper.DEFAULT_CHARSET)
        );

        //发送消息并返回结果
        SendResult sendResult = producer.send(msg);
        System.out.println("发送queueId:[" + sendResult.getMessageQueue().getQueueId() + "],偏移量offset:[" + sendResult.getQueueOffset() + "],发送状态:[" + sendResult.getSendStatus() + "]");

        // 一旦生产者实例不再被使用则将其关闭,包括清理资源,关闭网络连接等
        producer.shutdown();
    }
}

顺序消息重试,消费者监听MessageListenerOrderly接口。

public interface MessageListenerOrderly extends MessageListener {
    /**
     * It is not recommend to throw exception,rather than returning ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT
     * if consumption failure
     *
     * @param msgs msgs.size() >= 1<br> DefaultMQPushConsumer.consumeMessageBatchMaxSize=1,you can modify here
     * @return The consume status
     */
    ConsumeOrderlyStatus consumeMessage(final List<MessageExt> msgs,
        final ConsumeOrderlyContext context);
}

消费者消息重试机制(顺序消息重试),顺序消息消费加了一个分布式锁,后面有消息进来会阻塞住。一直到到重试消息被消费掉后才能正常消费。

import com.bemyself.common.MQConstant;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.*;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.remoting.common.RemotingHelper;

import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.List;

/**
 * 重试消费
 */
public class SequenceRetryConsumer {
    public static void main(String[] args) throws Exception {
        //创建一个消息消费者,并设置一个消息消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocket_test_consumer_group");
        //指定 NameServer 地址
        consumer.setNamesrvAddr(MQConstant.ROCKETMQ_NAMESERVER_ADDR);
        //设置 Consumer 第一次启动时从队列头部开始消费还是队列尾部开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        //订阅指定 Topic 下的所有消息
        consumer.subscribe("topicTest", "*");

        //注册消息监听器(顺序消息)
        consumer.registerMessageListener(new MessageListenerOrderly() {
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext context) {
                if (list != null) {
                    for (MessageExt ext : list) {
                        //获取消息重试次数
                        int retryTimes = ext.getReconsumeTimes();
                        try {
                            String message = new String(ext.getBody(), RemotingHelper.DEFAULT_CHARSET);
                            System.out.println("Consumer-线程名称=[" + Thread.currentThread().getId() + "],消息重试次数:[" + retryTimes + "],接收时间:[" + new Date().getTime() + "],消息=[" + message + "]");
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                System.out.println("出现异常...");
                // 消费成功
                //return ConsumeOrderlyStatus.SUCCESS;
                // 消费重试
                return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
            }
        });

        // 消费者对象在使用之前必须要调用 start 初始化
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

 无序队列无序消息的重试

无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。

重试次数

消息队列RocketMQ版默认允许每条消息最多重试16次,每次重试的间隔时间如下。

第几次重试

与上次重试的时间间隔

第几次重试

与上次重试的时间间隔

1

10秒

9

7分钟

2

30秒

10

8分钟

3

1分钟

11

9分钟

4

2分钟

12

10分钟

5

3分钟

13

20分钟

6

4分钟

14

30分钟

7

5分钟

15

1小时

8

6分钟

16

2小时

如果消息重试16次后仍然失败,消息将不再投递。如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的4小时46分钟之内进行16次重试,超过这个时间范围消息将不再重试投递。

无序消息重试时临时将消息保存在一个重试队列(%RETRY%消费者组名)中等待被重试消费。

 消费者和生产重试区别

消费者和生产者的重试还是有区别的,主要有两点

• 默认重试次数:Product默认是2次,而Consumer默认是16次。

• 重试时间间隔:Product是立刻重试,而Consumer是有一定时间间隔的。它按照1S,5S,10S,30S,1M,2M····2H进行重试。

注意:Product在异步情况重试失效,而对于Consumer在广播情况下重试失效。

重试配置方式

需要重试 

消费失败后,重试配置方式,集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置(三种方式任选一种):

• 方式1:返回RECONSUME_LATER(推荐) 

• 方式2:返回Null

• 方式3:抛出异常

无需重试

集群消费方式下,消息失败后期望消息不重试,需要捕获消费逻辑中可能抛出的异常,最终返回Action.CommitMessage,此后这条消息将不会再重试。

//注册消息监听器
consumer.registerMessageListener(new MessageListenerConcurrently() {
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
        //消息处理逻辑抛出异常,消息将重试。
        try {
            doConsumeMessage(list);
        } catch (Exception e) {
            //捕获消费逻辑中的所有异常,并返回Action.CommitMessage;
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
        //业务方正常消费
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});

无序消息重试,消费者监听MessageListenerConcurrently接口。

public interface MessageListenerConcurrently extends MessageListener {
    /**
     * It is not recommend to throw exception,rather than returning ConsumeConcurrentlyStatus.RECONSUME_LATER if
     * consumption failure
     *
     * @param msgs msgs.size() >= 1<br> DefaultMQPushConsumer.consumeMessageBatchMaxSize=1,you can modify here
     * @return The consume status
     */
    ConsumeConcurrentlyStatus consumeMessage(final List<MessageExt> msgs,
        final ConsumeConcurrentlyContext context);
}

消费者三种消息重试机制(无序消息重试),消息重试期间,后面有消息进来会正常被消费,不会受到消息重试的影响。

import com.bemyself.common.MQConstant;
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.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.remoting.common.RemotingHelper;

import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.List;

/**
 * 重试消费
 */
public class RetryConsumer {
    public static void main(String[] args) throws Exception {
        //创建一个消息消费者,并设置一个消息消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocket_test_consumer_group");
        //指定 NameServer 地址
        consumer.setNamesrvAddr(MQConstant.ROCKETMQ_NAMESERVER_ADDR);
        //设置 Consumer 第一次启动时从队列头部开始消费还是队列尾部开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        //订阅指定 Topic 下的所有消息
        consumer.subscribe("topicTest", "*");

        //注册消息监听器(无序消息)
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                if (list != null) {
                    for (MessageExt ext : list) {
                        //获取消息重试次数
                        int retryTimes = ext.getReconsumeTimes();
                        try {
                            String message = new String(ext.getBody(), RemotingHelper.DEFAULT_CHARSET);
                            System.out.println("Consumer-线程名称=[" + Thread.currentThread().getId() + "],消息重试次数:[" + retryTimes + "],接收时间:[" + new Date().getTime() + "],消息=[" + message + "]");
                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                System.out.println("出现异常...");

                //  业务方正常消费
                //return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                // 消息重试机制
                //int n = 1 / 0;
                // return null;
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
        });

        // 消费者对象在使用之前必须要调用 start 初始化
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

4.3.死信队列

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

死信消息特性:

• 不会再被消费者正常消费  

• 有效期与正常消息相同,均为3天,3天后会被自动删除。故死信消息应在产生的3天内及时处理

死信队列特性:

• 一个死信队列对应一个消费者组,而不是对应单个消费者实例

• 一个死信队列包含了对应的Group ID所产生的所有死信消息,不论该消息属于哪个Topic

• 若一个Group ID没有产生过死信消息,则RocketMQ不会为其创建相应的死信队列

正常情况下无法被消费的消息,系统会创建一个死信队列。死信队列的命名:%DLQ%消费者组名称

手动消费死信队列消息与正常指定消费时一样的,只需要指定死信队列的topic即可。

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.consumer.rebalance.AllocateMessageQueueConsistentHash;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.remoting.common.RemotingHelper;

import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.List;

/**
 * 普通消费
 */
public class Consumer {

    public static void main(String[] args) throws Exception {
        //创建一个消息消费者,并设置一个消息消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(null, "rocket_test_consumer_group", null, new AllocateMessageQueueConsistentHash());
        //指定 NameServer 地址
        consumer.setNamesrvAddr("127.0.0.1:9876");
        //设置 Consumer 第一次启动时从队列头部开始消费还是队列尾部开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        //订阅指定 Topic 下的所有消息
        consumer.subscribe("死信队列topic", "*");
        //注册消息监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
                //默认 list 里只有一条消息,可以通过设置参数来批量接收消息
                if (list != null) {
                    for (MessageExt ext : list) {
                        try {
                            String message = new String(ext.getBody(), RemotingHelper.DEFAULT_CHARSET);
                            //打印消息
                            System.out.println("接收queueId:[" + ext.getQueueId() + "],偏移量offset:[" + ext.getQueueOffset() + "],接收时间:[" + new Date().getTime() + "],消息=[" + message + "]");

                        } catch (UnsupportedEncodingException e) {
                            e.printStackTrace();
                        }
                    }
                }
                //出现异常会回到队列重新消费
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        // 消费者对象在使用之前必须要调用 start 初始化
        consumer.start();
        System.out.println("消息消费者已启动");
    }
}

5.0.常见问题

5.1.RocketMQ如何保证消息不丢失

我们将消息流程分为三大部分,每一部分都有可能会丢失数据。 

• 生产阶段:Producer通过网络将消息发送给Broker,这个发送可能会发生丢失。比如网络延迟不可达等。

• 存储阶段:Broker肯定是先把消息放到内存的,然后根据刷盘策略持久化到硬盘中。刚收到Producer的消息,放入内存,但是异常宕机了,导致消息丢失。

• 消费阶段:消费失败。比如先提交ack再消费,处理过程中出现异常,该消息就出现了丢失。

解决方案:

• 生产阶段:使用同步发送失败重试机制异步发送重写回调方法检查发送结果Ack确认机制。

• 存储阶段:同步刷盘机制;集群模式采用同步复制。

• 消费阶段:正常消费处理完成才提交ACK;如果处理异常返回重试标识。

除了上述,在生产阶段与消费者阶段部分消息还需要确保消息顺序消费

5.2.RocketMQ的消息持久化机制

RocketMQ的消息持久化机制是指将消息存储在磁盘上,以确保消息能够可靠地存储和检索。RocketMQ的消息持久化机制涉及到以下三个角色:CommitLog、ConsumeQueueIndexFile

• CommitLog:消息真正的存储文件,所有的消息都存在CommitLog文件中。

RocketMQ默认会将消息数据先存储到内存中的一个缓冲区,每当缓冲区中积累了一定量的消息或者一定时间后,就会将缓冲区中的消息批量写入到磁盘上的CommitLog文件中。消息在写入CommitLog文件后就可以被消费者消费了。

Commitlog文件的大小固定1G,写满之后生成新的文件,并且采用的是顺序写的方式。

• ConsumeQueue:消息消费逻辑队列,类似数据库的索引文件

RocketMQ中每个主题下的每个消息队列都会对应一个ConsumeQueueConsumeQueue存储了消息的offset以及该offset对应的消息在CommitLog文件中的位置信息,便于消费者快速定位并消费消息。

每个ConsumeQueue文件固定由30万个固定大小20byte的数据块组成;据块的内容包括:msgPhyOffset(8byte,消息在文件中的起始位置)+msgSize(4byte,消息在文件中占用的长度)+msgTagCode(8byte,消息的tagHash值)。

• IndexFile:消息索引文件,主要存储消息Keyoffset的对应关系,提升消息检索速度

如果生产者在发送消息时设置了消息Key,那么RocketMQ会将消息Key值和消息的物理偏移量(offset)存储在IndexFile文件中,这样当消费者需要根据消息Key查询消息时,就可以直接在IndexFile文件中查找对应的offset,然后通过ConsumeQueue文件快速定位并消费消息。

IndexFile文件大小固定400M,可以保存2000W个索引。

三个角色构成的消息存储结构如下:

 消息存储过程: 

5.3.RocketMQ的事务消息原理

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

发送消息的预处理阶段:在发送事务消息之前,RocketMQ会将消息的状态设置为“Preparing”,并将消息存储到消息存储库中。

执行本地事务:当预处理阶段完成后,消息发送者需要执行本地事务,并返回执行结果(commitrollback)。

消息的二次确认阶段:根据本地事务的执行结果,如果是commit,则RocketMQ将消息的状态设置为“Committing”;否则将消息的状态设置为“Rollback”。

完成事务:最后在消息的消费者消费该消息时,RocketMQ会根据消息的状态来决定是否提交该消息。如果消息的状态是“Committing”,则直接提交该消息;否则忽略该消息。

需要注意的是,如果在消息发送的过程中出现异常或者网络故障等问题,RocketMQ会触发消息回查机制。在回查过程中,RocketMQ会调用消息发送方提供的回查接口来确认事务的提交状态,从而解决消息投递的不确定性。

猜你喜欢

转载自blog.csdn.net/qq_43460743/article/details/131610295