RocketMQ

communication

# 一、RocketMQ基础

RocketMQ的官网地址: RocketMQ (opens new window)

RocketMQ GitHub地址:https://github.com/apache/rocketmq (opens new window)

RocketMQ 中文文档:https://rocketmq.apache.org/zh/docs (opens new window)

Apache Alibaba RocketMQ阿里巴巴开源的一个消息中间件,在阿里内部历经了双十一等很多高并发场景的考验,能够处理亿万级别的消息。2016年开源后捐赠给Apache,现在是Apache的一个顶级项目。

Java语言开发,底层采用Netty通信。

目前客户端支持Java, C++, Go。

消息中间件中有两个角色消息生产者消息消费者,RocketMQ 里同样有这两个概念。

  • 消息生产者负责创建消息并发送到 RocketMQ 服务器,RocketMQ 服务器会将消息持久化到磁盘;
  • 消息消费者从 RocketMQ 服务器拉取(推送)消息,提交给应用进行消费。

# RocketMQ概念

RocketMQ 官方概念介绍 (opens new window)

核心概念及配置参数说明,可查看RocketMQ 官方中文文档说明: best_practice.md (opens new window)

# 1、NameServer

名称服务器 NameServer,用于管理Broker。

RocketMQ 中,NameServer 被设计用来做简单的路由管理。其职责包括:

  • Brokers 定期向每个名称服务器注册路由数据。
  • 名称服务器为客户端(包括生产者,消费者和命令行客户端)提供最新的路由信息。

# 2、Broker

服务主机 Broker,用于暂存和传输消息。

集群模式下,Broker 角色分为Master和Slave。包括 ASYNC_MASTER(异步主机)、SYNC_MASTER(同步主机)以及SLAVE(从机)。

  • 如果对消息的可靠性要求比较严格,可以采用 SYNC_MASTERSLAVE的部署方式。
  • 如果对消息可靠性要求不高,可以采用ASYNC_MASTERSLAVE的部署方式。
  • 如果只是测试方便,则可以选择仅ASYNC_MASTER或仅SYNC_MASTER的部署方式。

# 3、Topic

主题 Topic,表示一类消息的集合,用于区分消息的种类,一级消息类型。

  • 每一个Topic包含若干条消息,每条消息只能属于一个Topic。
  • 同一个Topic下的消息,会分片保存在不同的Broker上。而每一个分片单位,叫做MessageQueue。

# 4、MessageQueue

消息队列 MessageQueue,Topic下的分区(分片),存储消息的载体(存储消息的物理地址)。

# 5、Tag

消息标签 Tag,⼆级消息类型,⽤来进⼀步区分某个Topic下的消息分类,实现消息的分流处理。

Topic与Tag都是业务上⽤来归类的标识,区分在于Topic是⼀级分类,⽽Tag可以理解为是⼆级分类。

一个应用尽可能用一个Topic,而消息子类型则可以用tags来标识。tags可以由应用自由设置,只有生产者在发送消息设置了tags,消费方在订阅消息时才可以利用tags通过broker做消息过滤:message.setTags("TagA")。

# 6、Message Key

消息关键字 Message Key,⼀般⽤于消息在业务层⾯的唯⼀标识。

对发送的消息设置好Key,以后可以根据这个Key来查找消息。⽐如消息异常,消息丢失,进⾏查找会很⽅便。

RocketMQ会创建专门的索引⽂件,⽤来存储Key与消息的映射。由于是Hash索引,应尽量使Key唯⼀,避免潜在的哈希冲突。

TagMessage Key的主要差别是使⽤场景不同:

  • Tag⽤在Consumer代码中,⽤于服务端消息过滤;
  • Message Key主要⽤于通过命令进⾏查找消息。RocketMQ并不能保证messageID唯⼀,在这种情况下,⽣产者在push消息的时候可以给每条消息设定唯⼀的key,消费者可以通过Message Key保证对消息幂等处理。

# 7、Producer Group

生产者组 Producer Group,生产者中将同一类Producer组成一个集合,这类生产者发送同一类消息,且发送逻辑一致。

目的:在事务模型时,可以保证容错。

# 8、Consumer Group

消费者组 Consumer Group,消费者把同一类的消费者Consumer组成一个集合,叫做消费者组。这类消费者须订阅同一个Topic,且消费逻辑一致。

目的:实现负载均衡和容错。

图例:

  • 一个消费者组订阅多个主题

  • 多个消费者组订阅一个主题

# RocketMQ端口

我们在安装rocketmq后,要开放的端口一般有4个:9876,10911,10912,10909

端口 源代码位置(模块) 描述 设置
9876 namesrv 用于NameServer对外服务,NettyServer的默认监听端口
10909 broker 用于主从同步,remotingServer服务的NettyServer的默认监听端口 可通过broker.conf文件中的fastListenPort参数进行修改
10911 broker 用于Broker对外服务,NettyServer的默认监听端口 可通过broker.conf文件中的listenPort参数进行修改
10912 store HAService服务组件使用 可通过broker.conf文件中的haListenPort参数进行修改

# RocketMQ生命周期

说明:

  • 一个消息生产者可以发送消息给一个或者多个Topic;

  • 一个消息消费者可以订阅一个或者多个Topic消息

  • 每个Broker下可存储多个Topic的消息;

  • 每个Topic的消息也可以分片存储于不同的Broker中。

  • 每个Topic有多个MessageQueue(默认4个)。

# RocketMQ集群架构

一个完整的RocketMQ集群中,有如下几个角色:

  • Producer:消息的发送者;
  • Consumer:消息接收者;
  • Broker:暂存和传输消息;
  • NameServer:管理Broker;
  • Topic:区分消息的种类;
  • Message Queue:相当于是Topic的分区;

# 异步通信模型

# RocketMQ发送方式

消息生产者Broker之间交互:

  • 同步发送
  • 异步发送
  • 单向发送
  • 顺序发送
  • 延迟发送
  • 批量发送

同步发送、异步发送需要Broker返回确认信息。

# RocketMQ消费模式

Broker消息消费者之间交互:

  • 集群消费Clustering
  • 广播消费Broadcasting

对同一个Topic下的MessageQueue,集群消费是只选其中一个进行消费;而广播消费会对每一个分片都进行消费。

# RocketMQ特点

RocketMQ 是一款分布式、队列模型的消息中间件和流平台,具有低延迟、高性能和可靠性、万亿级容量和灵活的可扩展性。具有以下特点:

  • 支持严格的消息顺序

  • 支持 Topic 与 Queue 两种模式

  • 亿级消息堆积能力

  • 比较友好的分布式特性

  • 同时支持 Push 与 Pull 方式消费消息

  • 历经多次天猫双十一海量消息考验

# RocketMQ使用场景

RocketMQ 在阿里集团被广泛应用在订单, 交易,充值,流计算,消息推送,日志流式处理,binlog 分发等场景。

# RocketMQ优势

目前主流的 MQ 主要是 RocketMQkafkaRabbitMQ

RocketMQ的主要优势有:

  • 支持事务型消息(消息发送和 DB 操作保持两方的最终一致性,RabbitMQ 和 Kafka 不支持)
  • 支持结合 RocketMQ 的多个系统之间数据最终一致性(多方事务,二方事务是前提)
  • 支持 18 个级别的延迟消息(RabbitMQ 和 Kafka 不支持)
  • 支持指定次数和时间间隔的失败消息重发(Kafka 不支持,RabbitMQ 需要手动确认)
  • 支持 Consumer 端 Tag 过滤,减少不必要的网络传输(RabbitMQ 和 Kafka 不支持)
  • 支持重复消费(RabbitMQ 不支持,Kafka 支持)

# MQ的作用

  • 异步:异步能提高系统的响应速度、吞吐量。

  • 系统解耦: 解决不同重要程度、不同能力级别系统之间依赖导致一死全死。

    • 减少服务之间的影响,提高系统整体的稳定性以及可扩展性。

    • 解耦后可以实现数据分发。生产者发送一个消息后,可以由一个或者多个消费者进行消

      费,并且消费者的增加或者减少对生产者没有影响。

  • 削峰填谷: 以稳定的系统资源应对突发的流量冲击。主要解决瞬时写压力大于应用服务能力导致消息丢失、系统奔溃等问题。

# RocketMQ社区 (opens new window)

# 二、RocketMQ消息模型

RocketMQ7种消息模型。普通模型、顺序模型、广播模型、延迟模型、批量模型、过滤模型、事务模型

# 1. 普通模型Normal

使用消息生产者发送消息,然后使用消费者来消费这些消息。

消息生产者有三种方式发送消息:同步发送、异步发送以及单向发送。

消费者消费消息有两种模式:一种是消费者主动去Broker上拉取消息的拉模式,另一种是消费者等待Broker把消息推送过来的推模式

# 1). 同步发送

消息生产者发送消息后,需要等待Broker返回确认信息,再继续进行下面的操作。

使用producer.send(msg)方法来发送消息。

同步发送官方样例:org.apache.rocketmq.example.simple.Producer (opens new window)

package org.apache.rocketmq.example.simple;

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import java.nio.charset.StandardCharsets;

public class Producer {
    public static final String PRODUCER_GROUP = "ProducerGroupName";
    public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
    public static final String TOPIC = "TopicTest";
    public static final String TAG = "TagA";

    public static void main(String[] args) throws MQClientException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
		producer.setNamesrvAddr(DEFAULT_NAMERVDDR);
        producer.start();
        for (int i = 0; i < 128; i++) {
            try {
                Message msg = new Message(TOPIC, TAG, "OrderID188", "Hello world".getBytes(StandardCharsets.UTF_8));
                SendResult sendResult = producer.send(msg);
                System.out.printf("%s%n", sendResult);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        producer.shutdown();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 2). 异步发送

消息生产者发送消息后,先继续进行下面的操作,Broker返回确认信息通过异步回调返回。

使用producer.send(msg, new SendCallback() {}方法来发送消息,SendCallback匿名内部类中有2个方法,onSuccess方法表示消息发送成功后的回调函数,onException方法表示消息发送失败后的回调函数。

异步发送官方样例:org.apache.rocketmq.example.simple.AsyncProducer (opens new window)

public class AsyncProducer {
    public static void main(
        String[] args) throws MQClientException, InterruptedException, UnsupportedEncodingException {

        DefaultMQProducer producer = new DefaultMQProducer("Jodie_Daily_test");
        producer.start();
        // suggest to on enableBackpressureForAsyncMode in heavy traffic, default is false
        producer.setEnableBackpressureForAsyncMode(true);
        producer.setRetryTimesWhenSendAsyncFailed(0);

        int messageCount = 100;
        final CountDownLatch countDownLatch = new CountDownLatch(messageCount);
        for (int i = 0; i < messageCount; i++) {
            try {
                final int index = i;
                Message msg = new Message("Jodie_topic_1023",
                    "TagA",
                    "OrderID188",
                    "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
                producer.send(msg, new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        countDownLatch.countDown();
                        System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
                    }

                    @Override
                    public void onException(Throwable e) {
                        countDownLatch.countDown();
                        System.out.printf("%-10d Exception %s %n", index, e);
                        e.printStackTrace();
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        countDownLatch.await(5, TimeUnit.SECONDS);
        producer.shutdown();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 3). 单向发送

单向发送没有返回值,也没有回调。就是只管把消息发出去就行了。

使用producer.sendOneWay方法来发送消息。

单向发送官方样例:org.apache.rocketmq.example.simple.OnewayProducer (opens new window)

public class OnewayProducer {
    public static void main(String[] args) throws Exception {
        //Instantiate with a producer group name.
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        // Specify name server addresses.
        producer.setNamesrvAddr("localhost:9876");
        //Launch the instance.
        producer.start();
        for (int i = 0; i < 100; i++) {
            //Create a message instance, specifying topic, tag and message body.
            Message msg = new Message("TopicTest" /* Topic */,
                    "TagA" /* Tag */,
                    ("Hello RocketMQ " +
                            i).getBytes(StandardCharsets.UTF_8) /* Message body */
            );
            //Call send message to deliver message to one of brokers.
            producer.sendOneway(msg);
        }
        //Wait for sending to complete
        Thread.sleep(5000);
        producer.shutdown();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 4). 拉模式

消费者主动去Broker上拉取消息。

拉模式官方样例:org.apache.rocketmq.example.simple.PullConsumer (opens new window)

try {
    long offset = this.consumeFromOffset(messageQueue);
    pullResult = consumer.pull(messageQueue, "*", offset, 32);
    switch (pullResult.getPullStatus()) {
        case FOUND:
            List<MessageExt> msgs = pullResult.getMsgFoundList();

            if (msgs != null && !msgs.isEmpty()) {
                this.doSomething(msgs);
                //update offset to broker
                consumer.updateConsumeOffset(messageQueue, pullResult.getNextBeginOffset());
                //print pull tps
                this.incPullTPS(topic, pullResult.getMsgFoundList().size());
            }
            break;
        case OFFSET_ILLEGAL:
            consumer.updateConsumeOffset(messageQueue, pullResult.getNextBeginOffset());
            break;
        case NO_NEW_MSG:
            Thread.sleep(1);
            consumer.updateConsumeOffset(messageQueue, pullResult.getNextBeginOffset());
            break;
        case NO_MATCHED_MSG:
            consumer.updateConsumeOffset(messageQueue, pullResult.getNextBeginOffset());
            break;
        default:
    }
} catch (Exception e) {
    e.printStackTrace();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 5). 推模式

消费者等待Broker把消息推送过来。

通常情况下,用推模式比较简单,实际上RocketMQ的推模式也是由拉模式封装出来的。

推模式官方样例:org.apache.rocketmq.example.simple.PushConsumer (opens new window)

public class PushConsumer {
    public static void main(String[] args) throws InterruptedException, MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_JODIE_1");
        consumer.subscribe("TopicTest", "*");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        //wrong time format 2017_0422_221800
        consumer.setConsumeTimestamp("20181109221800");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 2. 顺序模型Ordered

RocketMQ保证的是消息的局部有序,而不是全局有序。

顺序消息生产者官方样例:org.apache.rocketmq.example.order.Producer (opens new window)

public class Producer {
    public static void main(String[] args) throws MQClientException {
        try {
            DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
            producer.start();

            String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
            for (int i = 0; i < 100; i++) {
                int orderId = i % 10;
                Message msg =
                    new Message("TopicTestjjj", tags[i % tags.length], "KEY" + i,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                        Integer id = (Integer) arg;
                        int index = id % mqs.size();
                        return mqs.get(index);
                    }
                }, orderId);

                System.out.printf("%s%n", sendResult);
            }
            producer.shutdown();
        } catch (Exception e) {
            e.printStackTrace();
            throw new MQClientException(e.getMessage(), null);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

顺序消息消费者官方样例:org.apache.rocketmq.example.order.Consumer

public class Consumer {

    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.subscribe("TopicTest", "TagA || TagC || TagD");
        consumer.registerMessageListener(new MessageListenerOrderly() {
            AtomicLong consumeTimes = new AtomicLong(0);
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                this.consumeTimes.incrementAndGet();
                if ((this.consumeTimes.get() % 2) == 0) {
                    return ConsumeOrderlyStatus.SUCCESS;
                } else if ((this.consumeTimes.get() % 5) == 0) {
                    context.setSuspendCurrentQueueTimeMillis(3000);
                    return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                }

                return ConsumeOrderlyStatus.SUCCESS;
            }
        });

        consumer.start();
        System.out.printf("Consumer Started.%n");
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

实际上,RocketMQ也只保证了每个OrderID的所有消息有序(发到了同一个queue),而并不能保证所有消息都有序。

而若需要保证最终消费到的消息是有序的,需要从Producer、Broker、Consumer三个步骤都保证消息有序才行。

# 3. 广播模型Boradcasting

广播消息的消息生产者官方样例:org.apache.rocketmq.example.broadcast.PushConsumer (opens new window)

public class PushConsumer {

    public static final String CONSUMER_GROUP = "please_rename_unique_group_name_1";
    public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
    public static final String TOPIC = "TopicTest";
    public static final String SUB_EXPRESSION = "TagA || TagC || TagD";

    public static void main(String[] args) throws InterruptedException, MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);

        // Uncomment the following line while debugging, namesrvAddr should be set to your local address
//        consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR);

        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.setMessageModel(MessageModel.BROADCASTING);
        consumer.subscribe(TOPIC, SUB_EXPRESSION);
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        consumer.start();
        System.out.printf("Broadcast Consumer Started.%n");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

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

  • 集群模式下(MessageModel.CLUSTERING),每一条消息只会被同一个消费者组中的一个实例消费到(这跟kafka和rabbitMQ的集群模式是一样的)。
  • 广播模式则是把消息发给了所有订阅了对应主题的消费者,而不管消费者是不是同一个消费者组。

# 4. 延迟模型Delay

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

延迟时间的设置就是在Message消息对象上设置一个延迟级别message.setDelayTimeLevel(3)

开源版本的RocketMQ中,对延迟消息并不支持任意时间的延迟设定(商业版本中支持),而是只支持18个固定的延迟级别,1到18分别对应messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h。

public class ScheduledMessageProducer {
    public static void main(String[] args) throws Exception {
        // Instantiate a producer to send scheduled messages
        DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup");
        // Launch producer
        producer.start();
        int totalMessagesToSend = 100;
        for (int i = 0; i < totalMessagesToSend; i++) {
            Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());
            // This message will be delivered to consumer 10 seconds later.
            message.setDelayTimeLevel(3);
            // Send the message
            producer.send(message);
        }
        // Shutdown producer after use.
        producer.shutdown();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 5. 批量模型Batch

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

1). 单批次发送

批量消息的消息生产者官方样例:org.apache.rocketmq.example.batch.SimpleBatchProducer (opens new window)

public class SimpleBatchProducer {

    public static final String PRODUCER_GROUP = "BatchProducerGroupName";
    public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
    public static final String TOPIC = "BatchTest";
    public static final String TAG = "Tag";

    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
        // Uncomment the following line while debugging, namesrvAddr should be set to your local address
//        producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
        producer.start();

        //If you just send messages of no more than 1MiB at a time, it is easy to use batch
        //Messages of the same batch should have: same topic, same waitStoreMsgOK and no schedule support
        List<Message> messages = new ArrayList<>();
        messages.add(new Message(TOPIC, TAG, "OrderID001", "Hello world 0".getBytes(StandardCharsets.UTF_8)));
        messages.add(new Message(TOPIC, TAG, "OrderID002", "Hello world 1".getBytes(StandardCharsets.UTF_8)));
        messages.add(new Message(TOPIC, TAG, "OrderID003", "Hello world 2".getBytes(StandardCharsets.UTF_8)));

        SendResult sendResult = producer.send(messages);
        System.out.printf("%s", sendResult);
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 2). 多批次发送

如果批量消息大于1MB就不要用一个批次发送,而要拆分成多个批次消息发送。也就是说,一个批次消息的大小不要超过1MB。实际使用时,这个1MB的限制可以稍微扩大点,实际最大的限制是4194304字节(4MB)。

但是使用批量消息时,这个消息长度确实是必须考虑的一个问题。而且批量消息的使用是有一定限制的。

  • 批量消息不能是延迟消息、事务消息。
  • 这些消息应该有相同的Topic,相同的waitStoreMsgOK。

批量消息的消息生产者官方样例:org.apache.rocketmq.example.batch.SplitBatchProducer (opens new window)

# 6. 过滤模型Filter

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

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

# 1). Tag过滤

TAG是RocketMQ中特有的一个消息属性,可以使用Message的Tag属性来简单快速的过滤消息。

使用Tag过滤消息的消息生产者官方案例:org.apache.rocketmq.example.filter.TagFilterProducer (opens new window)

使用Tag过滤消息的消息消费者官方案例:org.apache.rocketmq.example.filter.TagFilterConsumer (opens new window)

public class TagFilterProducer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        producer.start();
        String[] tags = new String[] {"TagA", "TagB", "TagC"};
        for (int i = 0; i < 60; i++) {
            Message msg = new Message("TagFilterTest",
                tags[i % tags.length],
                "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));

            SendResult sendResult = producer.send(msg);
            System.out.printf("%s%n", sendResult);
        }
        producer.shutdown();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TagFilterConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
        consumer.subscribe("TagFilterTest", "TagA || TagC");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                ConsumeConcurrentlyContext context) {
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 2). SQL过滤

按照SQL92标准,可以使用SQL表达式来对消息进行过滤。

使用注意:只有推模式的消费者可以使用SQL过滤,拉模式是用不了的。

SQL过滤的消息生产者官方案例:org.apache.rocketmq.example.filter.SqlFilterProducer (opens new window)

SQL过滤的消息消费者官方案例:org.apache.rocketmq.example.filter.SqlFilterConsumer (opens new window)

public class SqlFilterProducer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        producer.start();
        String[] tags = new String[] {"TagA", "TagB", "TagC"};
        for (int i = 0; i < 10; i++) {
            Message msg = new Message("SqlFilterTest",
                tags[i % tags.length],
                ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
            );
            msg.putUserProperty("a", String.valueOf(i));
            SendResult sendResult = producer.send(msg);
            System.out.printf("%s%n", sendResult);
        }
        producer.shutdown();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SqlFilterConsumer {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
        // Don't forget to set enablePropertyFilter=true in broker
        consumer.subscribe("SqlFilterTest",
            MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" +
                "and (a is not null and a between 0 and 3)"));
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                ConsumeConcurrentlyContext context) {
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# SQL92语法

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

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

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

  • IS NULL 或者 IS NOT NULL;

  • 逻辑符号 AND,OR,NOT;

  • 常量支持类型为:

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

# 7. 事务模型Transaction

事务消息是RocketMQ提供的一个非常有特色的功能。

事务消息的实现机制:

事务消息只保证消息发送者的本地事务与发消息这两个操作的原子性,即事务消息只能保证生产者与Broker之间的一致性。

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

事务消息生产者的官方案例:org.apache.rocketmq.example.transaction.TransactionProducer (opens new window)

源码中的案例有点复杂,以下是一个更清晰明了的事务监听器示例。

public class TransactionListenerImpl implements TransactionListener {
 //在提交完事务消息后执行。
 //返回COMMIT_MESSAGE状态的消息会立即被消费者消费到。
 //返回ROLLBACK_MESSAGE状态的消息会被丢弃。
 //返回UNKNOWN状态的消息会由Broker过一段时间再来回查事务的状态。
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        String tags = msg.getTags();
        //TagA的消息会立即被消费者消费到
        if(StringUtils.contains(tags,"TagA")){
            return LocalTransactionState.COMMIT_MESSAGE;
        //TagB的消息会被丢弃
        }else if(StringUtils.contains(tags,"TagB")){
            return LocalTransactionState.ROLLBACK_MESSAGE;
        //其他消息会等待Broker进行事务状态回查。
        }else{
            return LocalTransactionState.UNKNOW;
        }
    }
 //在对UNKNOWN状态的消息进行状态回查时执行。返回的结果是一样的。
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
  String tags = msg.getTags();
        //TagC的消息过一段时间会被消费者消费到
        if(StringUtils.contains(tags,"TagC")){
            return LocalTransactionState.COMMIT_MESSAGE;
        //TagD的消息也会在状态回查时被丢弃掉
        }else if(StringUtils.contains(tags,"TagD")){
            return LocalTransactionState.ROLLBACK_MESSAGE;
        //剩下TagE的消息会在多次状态回查后最终丢弃
        }else{
            return LocalTransactionState.UNKNOW;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

使用注意:

  1. 事务消息不支持延迟消息和批量消息。
  2. 默认将单个消息的检查次数限制为15次。可以通过Broker配置文件的 transactionCheckMax 参数来修改此限制。

# 三、RocketMQ部署

# 3.1 Windows部署

# 1、安装RocketMQ服务端

  1. 官网下载二进制文件压缩包;

    官方下载地址 (opens new window)

  2. 解压至指定rocketmq目录;

  3. 配置环境变量;

    设置环境变量 ROCKETMQ_HOMENAMESRV_ADDR

    ROCKETMQ_HOME
    你的解压目录
    
    1
    2
    NAMESRV_ADDR
    localhost:9876
    
    1
    2
  4. 修改配置文件;

    配置参数说明,可查看RocketMQ 官方中文文档说明: best_practice.md (opens new window)

    修改配置文件broker.conf,添加如下配置

    brokerIP1=192.168.1.110
    
    # 在发送消息时,自动创建服务器不存在的Topic,默认创建的队列数
    defaultTopicQueueNums=4
    # 是否允许Broker 自动创建Topic,建议线下开启,线上关闭
    autoCreateTopicEnable=true
    # 是否允许Broker自动创建订阅组,建议线下开启,线上关闭
    autoCreateSubscriptionGroup=true
    
    # 删除文件时间点,默认是凌晨4点
    deleteWhen = 04
    # 文件保留时间,默认48小时
    fileReservedTime = 48
    # Broker 的角色
    brokerRole = ASYNC_MASTER
    # 刷盘方式
    flushDiskType = ASYNC_FLUSH
    
    # 存储路径
    storePathRootDir=D:/install_java/rocketmq/data/store
    # commitLog存储路径
    storePathCommitLog=D:/install_java/rocketmq/data/store/commitlog
    # 消费队列存储路径
    storePathConsumeQueue=D:/install_java/rocketmq/data/store/consumequeue
    # 消息索引存储路径
    storePathIndex=D:/install_java/rocketmq/data/store/index
    # checkpoint 文件存储路径
    storeCheckpoint=D:/install_java/rocketmq/data/store/checkpoint
    # abort 文件存储路径
    abortFile=D:/install_java/rocketmq/data/store/abort
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
  5. 启动nameserve;

    修改runserver.cmd文件中的JVM参数;

    set "JAVA_OPT=%JAVA_OPT% -server -Xms512m -Xmx512m -Xmn512m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
    
    1

    执行mqnamesrv.cmd文件;

    .\bin\mqnamesrv.cmd
    
    1
  6. 启动broker;

    修改runbroker.cmd中的JVM参数;

    set "JAVA_OPT=%JAVA_OPT% -server -Xms512m -Xmx512m"
    
    1

    执行mqbroker.cmd文件

    .\bin\mqbroker.cmd -n localhost:9876 -c .\conf\broker.conf
    
    1

    RocketMQ的数据默认存储在${user.home}/store目录下,可以通过-c指令指定配置文件路径。

# 2、启动控制台服务

  1. 下载控制台项目代码;

    rocketmq-console的gitee官方项目地址 (opens new window)

    或者rocketMQ的监控控制台 (opens new window)

  2. IDEA打开,编译运行;

  3. 访问http://localhost:8080

3、打包(可选)

进入项目根目录,执行打包命令。

mvn -Dmaven.test.skip=true clean package
1

# 扩展:部署到windows服务

1、创建bat文件

由于RocketMQ需要启动三个服务,分别是namesrvbrokerconsole;编写脚本,用于在windows环境启动使用MQ。

在RocketMQ安装目录下进行创建如下三个bat文件。

mq-namesrv.bat

@echo off
d:
cd D:\install_java\rocketmq-all-4.9.4-bin-release
start .\bin\mqnamesrv.cmd
cmd
1
2
3
4
5

mq-broker.bat

@echo off
d:
cd D:\install_java\rocketmq-all-4.9.4-bin-release
start .\bin\mqbroker.cmd -c .\conf\broker.conf
cmd
1
2
3
4
5

mq-console.bat

@echo off
e:
cd E:\project\openSource\rocketmq\rocketmq-console\target
java -jar rocketmq-console-ng-2.0.0.jar
cmd
1
2
3
4
5

说明:修改为自己的安装路径。

2、下载 nssm软件

下载链接:

链接:https://pan.baidu.com/s/1oH90DowxG9vbflq3eY-AWw 
提取码:60dv
1
2

下载下来之后是一个压缩包进行解压,找到对应你自己系统的32/64位nssm。

3、install服务

在cmd窗口中找到解压路径下的nssm.exe

依次执行 nssm install <服务名>

nssm install rocketmq_namesrv
nssm install rocketmq_broker
nssm install rocketmq_console
1
2
3

依次在回车出现nssm的配置界面,填写你的bat文件地址,点击 install service ,就建立成功。

然后在计算机服务中就可以看到刚刚建立的server了,右键点击启动,就可以启动该服务了。

4、设置启动类型

在服务中右键点击打开服务,在启动类型中进行选择;

选择配置如下:

rocketmq_namesrv	自动
rocketmq_broker		自动(延迟启动)
rocketmq_console	自动(延迟启动)
1
2
3

说明:默认情况下,“自动(延迟启动)” 实际上是在最后一次“自动”服务启动后2分钟后才启动。即默认会在系统启动后2分钟后才启动。

# 3.2 Docker部署

注意: 启动 RocketMQ Server + Broker + Console 至少需要 2G 内存。

需要开通端口:

  • 9876 namServer
  • 10909 Broker对外服务的监听端口
  • 10911
  • 8080 控制台

# broker.conf说明

# 所属集群名字
brokerClusterName=DefaultCluster

# broker 名字,注意此处不同的配置文件填写的不一样,如果在 broker-a.properties 使用: broker-a,
# 在 broker-b.properties 使用: broker-b
brokerName=broker-a

# 0 表示 Master,> 0 表示 Slave
brokerId=0

# nameServer地址,分号分割
# namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876

# 启动IP,如果 docker 报
# com.alibaba.rocketmq.remoting.exception.RemotingConnectException: connect to <192.168.0.120:10909> failed
# 解决方式1 加上一句 producer.setVipChannelEnabled(false);,解决方式2 brokerIP1 设置宿主机IP,不要使用docker 内部IP
brokerIP1=192.168.141.123

# 在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
defaultTopicQueueNums=4

# 是否允许 Broker 自动创建 Topic,建议线下开启,线上关闭 !!!这里仔细看是 false,false,false
autoCreateTopicEnable=true

# 是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup=true

# Broker 对外服务的监听端口
listenPort=10911

# 删除文件时间点,默认凌晨4点
deleteWhen=04

# 文件保留时间,默认48小时
fileReservedTime=120

# commitLog 每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824

# ConsumeQueue 每个文件默认存 30W 条,根据业务情况调整
mapedFileSizeConsumeQueue=300000

# destroyMapedFileIntervalForcibly=120000
# redeleteHangedFileInterval=120000
# 检测物理文件磁盘空间
diskMaxUsedSpaceRatio=88
# 存储路径
# storePathRootDir=/home/ztztdata/rocketmq-all-4.1.0-incubating/store
# commitLog 存储路径
# storePathCommitLog=/home/ztztdata/rocketmq-all-4.1.0-incubating/store/commitlog
# 消费队列存储
# storePathConsumeQueue=/home/ztztdata/rocketmq-all-4.1.0-incubating/store/consumequeue
# 消息索引存储路径
# storePathIndex=/home/ztztdata/rocketmq-all-4.1.0-incubating/store/index
# checkpoint 文件存储路径
# storeCheckpoint=/home/ztztdata/rocketmq-all-4.1.0-incubating/store/checkpoint
# abort 文件存储路径
# abortFile=/home/ztztdata/rocketmq-all-4.1.0-incubating/store/abort
# 限制的消息大小
maxMessageSize=65536

# flushCommitLogLeastPages=4
# flushConsumeQueueLeastPages=2
# flushCommitLogThoroughInterval=10000
# flushConsumeQueueThoroughInterval=60000

# Broker 的角色
# - ASYNC_MASTER 异步复制Master
# - SYNC_MASTER 同步双写Master
# - SLAVE
brokerRole=ASYNC_MASTER

# 刷盘方式
# - ASYNC_FLUSH 异步刷盘
# - SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH

# 发消息线程池数量
# sendMessageThreadPoolNums=128
# 拉消息线程池数量
# pullMessageThreadPoolNums=128
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

# 单机部署foxiswho/rocketmq 4.8

foxiswho/rocketmq官方docker镜像地址:https://hub.docker.com/r/foxiswho/rocketmq (opens new window)

broker.conf

RocketMQ Broker 需要一个配置文件,按照上面的 Compose 配置,我们需要在 ./data/brokerconf/ 目录下创建一个名为 broker.conf 的配置文件,内容如下:

brokerClusterName=DefaultCluster
brokerName=broker-a
brokerId=0
# 修改为你宿主机的 IP
brokerIP1=192.168.106.121
# 在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
defaultTopicQueueNums=4
autoCreateTopicEnable=true
autoCreateSubscriptionGroup=true
# Broker 对外服务的监听端口
listenPort=10911
# 删除文件时间点,默认凌晨4点
deleteWhen=04
# 文件保留时间,默认48小时
fileReservedTime=120
mapedFileSizeCommitLog=1073741824
mapedFileSizeConsumeQueue=300000
diskMaxUsedSpaceRatio=88

# 存储路径
storePathRootDir=/home/rocketmq/store
# commitLog 存储路径
storePathCommitLog=/home/rocketmq/store/commitlog
# 消费队列存储
storePathConsumeQueue=/home/rocketmq/store/consumequeue
# 消息索引存储路径
storePathIndex=/home/rocketmq/store/index
# checkpoint 文件存储路径
storeCheckpoint=/home/rocketmq/store/checkpoint
# abort 文件存储路径
abortFile=/home/rocketmq/store/abort

maxMessageSize=65536
brokerRole=ASYNC_MASTER
flushDiskType=ASYNC_FLUSH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

创建data目录并授权

创建目录

sudo mkdir -p /usr/local/docker/rocketmq4.8/data/namesrv/logs
sudo mkdir -p /usr/local/docker/rocketmq4.8/data/broker/{logs,store} 
sudo mkdir -p /usr/local/docker/rocketmq4.8/data/conf
1
2
3

上传brocker.conf文件至./data/conf目录;

授权,映射本地目录权限一定要设置为 777 权限,否则启动不成功。

sudo chmod -R 777 /usr/local/docker/rocketmq4.8/data/
1

docker-compose.yml

cd /usr/local/docker/rocketmq4.8
vi docker-compose.yml
1
2

docker-compose.yml文件内容:

version: '3.5'
services:
  rmqnamesrv:
    restart: unless-stopped
    image: foxiswho/rocketmq:4.8.0
    container_name: rmqnamesrv4.8
    ports:
      - 9876:9876
    volumes:
      - ./data/namesrv/logs:/home/rocketmq/logs
    environment:
        JAVA_OPT_EXT: "-Xms512M -Xmx512M -Xmn128m"
    command: mqnamesrv
    networks:
        rmq:
          aliases:
            - rmqnamesrv

  rmqbroker:
    restart: unless-stopped
    image: foxiswho/rocketmq:4.8.0
    container_name: rmqbroker4.8
    ports:
      - 10911:10911
      - 10912:10912
      - 10909:10909
    volumes:
      - ./data/broker/logs:/home/rocketmq/logs
      - ./data/broker/store:/home/rocketmq/store
      - ./data/conf:/home/rocketmq/conf
    environment:
        NAMESRV_ADDR: "rmqnamesrv:9876"
        JAVA_OPT_EXT: "-Xms512M -Xmx512M -Xmn128m"
    command: mqbroker -c /home/rocketmq/conf/broker.conf
    depends_on:
      - rmqnamesrv
    networks:
      rmq:
        aliases:
          - rmqbroker

  rmqconsole:
    restart: unless-stopped
    image: styletang/rocketmq-console-ng
    container_name: rmqconsole4.8
    ports:
      - 8086:8080
    environment:
        JAVA_OPTS: "-Drocketmq.namesrv.addr=rmqnamesrv:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false"
    depends_on:
      - rmqnamesrv
    networks:
      rmq:
        aliases:
          - rmqconsole

networks:
  rmq:
    name: rmq
    driver: bridge
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

# 四、RocketMQ高可用集群

# 4.1 高可用集群方案

基于DLedger Controller的高可用集群方案,实现快速构建自动主从切换的 RocketMQ 集群。

其架构如上图所示,主要增加支持自动主从切换的Controller组件,其可以独立部署也可以内嵌在NameServer中。

若需要保证Controller具备容错能力,Controller部署需要三副本及以上(遵循Raft的多数派协议)。

详细设计思路请参考 设计思想 (opens new window)

详细的新集群部署和旧集群升级指南请参考 部署指南 (opens new window)

# 4.2 DLedger-Docker部署

TODO

# 五、RocketMQ高级特性

# 日志的打印

# 1). 生产者端日志打印

消息发送成功或者失败要打印消息日志,务必要打印SendResult和key字段。

send消息方法只要不抛异常,就代表发送成功。

发送成功会有多个状态,在sendResult里定义。以下对每个状态进行说明:

  • SEND_OK

消息发送成功。要注意的是消息发送成功也不意味着它是可靠的。要确保不会丢失任何消息,还应启用同步Master服务器或同步刷盘,即SYNC_MASTER或SYNC_FLUSH。

  • FLUSH_DISK_TIMEOUT

消息发送成功但是服务器刷盘超时。此时消息已经进入服务器队列(内存),只有服务器宕机,消息才会丢失。消息存储配置参数中可以设置刷盘方式和同步刷盘时间长度,如果Broker服务器设置了刷盘方式为同步刷盘,即FlushDiskType=SYNC_FLUSH(默认为异步刷盘方式),当Broker服务器未在同步刷盘时间内(默认为5s)完成刷盘,则将返回该状态——刷盘超时。

  • FLUSH_SLAVE_TIMEOUT

消息发送成功,但是服务器同步到Slave时超时。此时消息已经进入服务器队列,只有服务器宕机,消息才会丢失。如果Broker服务器的角色是同步Master,即SYNC_MASTER(默认是异步Master即ASYNC_MASTER),并且从Broker服务器未在同步刷盘时间(默认为5秒)内完成与主服务器的同步,则将返回该状态——数据同步到Slave服务器超时。

  • SLAVE_NOT_AVAILABLE

消息发送成功,但是此时Slave不可用。如果Broker服务器的角色是同步Master,即SYNC_MASTER(默认是异步Master服务器即ASYNC_MASTER),但没有配置slave Broker服务器,则将返回该状态——无Slave服务器可用。

# 2). 消费者端日志打印

如果消息量较少,建议在消费入口方法打印消息,消费耗时等,方便后续排查问题。

public ConsumeConcurrentlyStatus consumeMessage(
        List<MessageExt> msgs,
        ConsumeConcurrentlyContext context) {
    log.info("RECEIVE_MSG_BEGIN: " + msgs.toString());
    // 正常消费过程
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}   
1
2
3
4
5
6
7

如果能打印每条消息消费耗时,那么在排查消费慢等线上问题时,会更方便。

# 消息幂等性

RocketMQ无法避免消息重复(Exactly-Once),所以如果业务对消费重复非常敏感,务必要在业务层面进行去重处理。可以借助关系数据库进行去重。

首先需要确定消息的唯一键,可以是msgId,也可以是消息内容中的唯一标识字段,例如订单Id等。

在消费之前判断唯一键是否在关系数据库中存在。如果不存在则插入,并消费,否则跳过。(实际过程要考虑原子性问题,判断是否存在可以尝试插入,如果报主键冲突,则插入失败,直接跳过)

msgId一定是全局唯一标识符,但是实际使用中,可能会存在相同的消息有两个不同msgId的情况(消费者主动重发、因客户端重投机制导致的重复等),这种情况就需要使业务字段进行重复消费。

# 消息一致性

# 消息发送

# 消息接收

消息中间件做异步解耦时的一个典型问题,就是如果下游服务处理消息事件失败,如何保证整个调用链路的完整性。

Apache RocketMQ 作为金融级的可靠业务消息中间件,在消息投递处理机制的设计上天然支持可靠传输策略,通过完整的确认和重试机制保证每条消息都按照业务的预期被处理。

# 消息持久化存储

TODO

# 负载均衡策略

TODO

# 消息发送重试Retry

RocketMQ官方文档——消息发送重试 (opens new window)

RocketMQ 客户端连接服务端,生产者发起消息发送请求时,可能会因为网络故障、服务异常等原因导致调用失败。生产者发送消息失败处理方式——消息重试机制。

# 重试触发条件

触发消息发送重试机制的条件如下:

  • 客户端消息发送请求调用失败或请求超时
  • 网络异常造成连接失败或请求超时。
  • 服务端节点处于重启或下线等状态造成连接失败。
  • 服务端运行慢造成请求超时。
  • 服务端返回失败错误码
    • 系统逻辑错误:因运行逻辑不正确造成的错误。
    • 系统流控错误:因容量超限造成的流控错误。

# 消息发送重试机制

Producer的send方法本身支持内部重试,重试逻辑如下:

  • 至多重试2次。
  • 如果同步模式发送失败,则轮转到下一个Broker,如果异步模式发送失败,则只会在当前Broker进行重试。这个方法的总耗时时间不超过sendMsgTimeout设置的值,默认10s。
  • 如果本身向broker发送消息产生超时异常,就不会再重试。

以上策略也是在一定程度上保证了消息可以发送成功。如果业务对消息可靠性要求比较高,建议应用增加相应的重试逻辑:比如调用send同步方法发送失败时,则尝试将消息存储到DB,然后由后台线程定时重试,确保消息一定到达Broker。

上述DB重试方式为什么没有集成到MQ客户端内部做,而是要求应用自己去完成,主要基于以下几点考虑:

  1. 首先,MQ的客户端设计为无状态模式,方便任意的水平扩展,且对机器资源的消耗仅仅是cpu、内存、网络。

  2. 其次,如果MQ客户端内部集成一个KV存储模块,那么数据只有同步落盘才能较可靠,而同步落盘本身性能开销较大,所以通常会采用异步落盘,又由于应用关闭过程不受MQ运维人员控制,可能经常会发生 kill -9 这样暴力方式关闭,造成数据没有及时落盘而丢失。

  3. 第三,Producer所在机器的可靠性较低,一般为虚拟机,不适合存储重要数据。

综上,建议重试过程交由应用来控制。

# 消息消费重试

RocketMQ官方文档——消费重试 (opens new window)

消费者出现异常,消费某条消息失败时, Apache RocketMQ 会根据消费重试策略重新投递该消息进行故障恢复。

了解 Apache RocketMQ 的消息确认机制以及消费重试策略可以帮助您分析如下问题:

  • 如何保证业务完整处理消息:了解消费重试策略,可以在设计实现消费者逻辑时保证每条消息处理的完整性,避免部分消息出现异常时被忽略,导致业务状态不一致。
  • 系统异常时处理中的消息状态如何恢复:帮助您了解当系统出现异常(宕机故障)等场景时,处理中的消息状态如何恢复,是否会出现状态不一致。

# 死信队列DLQ

TODO

# 消息丢失

# 消息顺序

# 消息积压

# 消息重复消费

# 消息轨迹

Producer 端要想使用消息轨迹,需要多配置两个配置项:

## application.properties
rocketmq.name-server=127.0.0.1:9876
rocketmq.producer.group=my-group

rocketmq.producer.enable-msg-trace=true
rocketmq.producer.customized-trace-topic=my-trace-topic
1
2
3
4
5
6

Consumer 端消息轨迹的功能需要在 @RocketMQMessageListener 中进行配置对应的属性:

@Service
@RocketMQMessageListener(
    topic = "test-topic-1", 
    consumerGroup = "my-consumer_test-topic-1",
    enableMsgTrace = true,
    customizedTraceTopic = "my-trace-topic"
)
public class MyConsumer implements RocketMQListener<String> {
    ...
}
1
2
3
4
5
6
7
8
9
10

注意:

默认情况下 Producer 和 Consumer 的消息轨迹功能是开启的且 trace-topic 为 RMQ_SYS_TRACE_TOPIC Consumer 端的消息轨迹 trace-topic 可以在配置文件中配置 rocketmq.consumer.customized-trace-topic 配置项,不需要为在每个 @RocketMQMessageListener 配置。

阿里云消息轨迹正常显示需要设置accessChannel配置为CLOUD。

# ACL功能

权限控制(ACL)主要为RocketMQ提供Topic资源级别的用户访问控制。

用户在使用RocketMQ权限控制时,可以在Client客户端通过 RPCHook注入AccessKey和SecretKey签名;同时,将对应的权限控制属性(包括Topic访问权限、IP白名单和AccessKey和SecretKey签名等)设置在$ROCKETMQ_HOME/conf/plain_acl.yml的配置文件中。

Broker端对AccessKey所拥有的权限进行校验,校验不过,抛出异常。

# Broker配置

而Broker端具体的配置信息可以参见源码包下docs/cn/acl/user_guide.md

主要是在broker.conf中打开acl的标志:aclEnable=true。然后就可以用plain_acl.yml来进行权限配置了。并且这个配置文件是热加载的,也就是说要修改配置时,只要修改配置文件就可以了,不用重启Broker服务。

我们来简单分析下源码中的plan_acl.yml的配置:

#全局白名单,不受ACL控制
#通常需要将主从架构中的所有节点加进来
globalWhiteRemoteAddresses:
- 10.10.103.*
- 192.168.0.*

accounts:
#第一个账户
- accessKey: RocketMQ
  secretKey: 12345678
  whiteRemoteAddress:
  admin: false 
  defaultTopicPerm: DENY #默认Topic访问策略是拒绝
  defaultGroupPerm: SUB #默认Group访问策略是只允许订阅
  topicPerms:
  - topicA=DENY #topicA拒绝
  - topicB=PUB|SUB #topicB允许发布和订阅消息
  - topicC=SUB #topicC只允许订阅
  groupPerms:
  # the group should convert to retry topic
  - groupA=DENY
  - groupB=PUB|SUB
  - groupC=SUB
#第二个账户,只要是来自192.168.1.*的IP,就可以访问所有资源
- accessKey: rocketmq2
  secretKey: 12345678
  whiteRemoteAddress: 192.168.1.*
  # if it is admin, it could access all resources
  admin: true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 客户端代码

ACL客户端官方样例:org.apache.rocketmq.example.simple.AclClient

注意,如果要在自己的客户端中使用RocketMQ的ACL功能,还需要引入一个单独的依赖包

<dependency>
	<groupId>org.apache.rocketmq</groupId>
	<artifactId>rocketmq-acl</artifactId>
	<version>4.7.1</version>
</dependency>
1
2
3
4
5

Producer 端要想使用 ACL 功能,需要多配置两个配置项:

## application.properties
rocketmq.name-server=127.0.0.1:9876
rocketmq.producer.group=my-group

rocketmq.producer.access-key=AK
rocketmq.producer.secret-key=SK
1
2
3
4
5
6

Consumer 端 ACL 功能需要在 @RocketMQMessageListener 中进行配置

@Service
@RocketMQMessageListener(
    topic = "test-topic-1", 
    consumerGroup = "my-consumer_test-topic-1",
    accessKey = "AK",
    secretKey = "SK"
)
public class MyConsumer implements RocketMQListener<String> {
    ...
}
1
2
3
4
5
6
7
8
9
10

注意:

可以不用为每个 @RocketMQMessageListener 注解配置 AK/SK,在配置文件中配置 rocketmq.consumer.access-keyrocketmq.consumer.secret-key 配置项,这两个配置项的值就是默认值。

# 六、RocketMQ使用

# 6.1 RocketMQ原生包使用

官方样例:org.apache.rocketmq.example (opens new window)

# 6.2 SpringBoot集成RocketMQ

SpringBoot集成RocketMQ,引入依赖包——rocketmq-spring-boot-starter

SpringBoot集成RocketMQ的starter依赖是由Spring社区提供的。

# 环境要求

  • JDK 1.8 and above
  • Maven 3.0 and above
  • Spring Boot 2.0 and above

# 代码开发

  1. rocketmq-spring-boot-starter的更新进度一般都会略慢于RocketMQ的版本更新。
  2. apache有一个官方的rocketmq-spring示例,地址:https://github.com/apache/rocketmq-spring.git (opens new window)
  • pom.xml
<!-- ********  RocketMQ搭建 Begin  ********** -->
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>
<!-- ********  RocketMQ搭建 End  ********** -->
1
2
3
4
5
6
7
  • 配置文件
#NameServer地址
rocketmq.name-server=192.168.1.111:9876
#默认的消息生产者组
rocketmq.producer.group=myProducerGroup
1
2
3
4
  • 生产者
@Component
public class SpringProducer {

    @Resource
    private RocketMQTemplate rocketMQTemplate;
    
 	//发送普通消息的示例
    public void sendMessage(String topic,String msg){
        this.rocketMQTemplate.convertAndSend(topic,msg);
    }
    
 	//发送事务消息的示例
    public void sendMessageInTransaction(String topic,String msg) throws InterruptedException {
        String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < 10; i++) {
            Message<String> message = MessageBuilder.withPayload(msg).build();
            String destination =topic+":"+tags[i % tags.length];
            SendResult sendResult = rocketMQTemplate.sendMessageInTransaction(destination, message,destination);
            System.out.printf("%s%n", sendResult);

            Thread.sleep(10);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  • 消费者

@RocketMQMessageListener注解的属性含义:

  1. nameServer:指定rocketmq地址;
  2. consumerGroup:消费者组,与yaml文件中须保存一致;
  3. topic:主题,与yaml文件中须保存一致;
  4. selectorType:
  5. selectorExpression:
  6. consumeMode:
  7. messageModel:消息模式,有集群模式、广播模式;
@Component
@RocketMQMessageListener(consumerGroup = "MyConsumerGroup", topic = "TestTopic")
public class SpringConsumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println("Received message : "+ message);
    }
}
1
2
3
4
5
6
7
8

# 扩展一:自定义RocketMQTemplate

当同一服务,需要引入多个RocketMQ服务端时,使用扩展类实现,指定具体的nameServer。

@ExtRocketMQTemplateConfiguration(
        nameServer = "192.168.1.121:9876",
        group = "myProducerGroup"
)
public class ExtMQTemplate extends RocketMQTemplate {

}
1
2
3
4
5
6
7

使用自定义配置类

    @Resource(name = "extMQTemplate")
    private RocketMQTemplate sysCenterMQTemplate;
1
2

# 扩展二:tag标识

生产者

RocketMQ包中的Message对象(org.apache.rocketmq.common.message.Message)里的tag属性,在SpringBoot依赖中的Message对象(org.springframework.messaging.Message)中就没有。Tag属性被移到了发送目标中,与Topic一起以topic:tag的方式指定。

import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;

    @Resource
    private RocketMQTemplate rocketMQTemplate;

	/**
     * 数据同步——用户
     */
    public void syncData2User(SysUser entity) {
        String topic = "userTopic";
        // tag属性
        String tagValue = "A1";
        // 设置主题为 topic:tags,用于进行消息过滤,实现消息的分流处理。
        topic = topic + ":" + tagValue;

        Message<T> message = MessageBuilder
                .withPayload(entity)
                // 设置Key,用于消费端进行接口幂等性校验时使用。由于是哈希索引,请务必保证key尽可能唯一,这样可以避免潜在的哈希冲突。
                .setHeader(RocketMQHeaders.KEYS, entity.getId())
                .build();
        
        // 单向模式发送。
        rocketMQTemplate.convertAndSend(topic, message);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

消费者

在消费者中,@RocketMQMessageListener注解的selectorExpression属性用于标识tag值,当发送消息的tag值与该属性值匹配时,才会进行接收。

@Component
@RocketMQMessageListener(
        nameServer = "192.168.1.111:9876",
        topic = "userTopic",
        consumerGroup = "myConsumerGroup",
        selectorExpression = "A1"
)
public class SysUserConsumerReceive implements RocketMQListener<SysUser> {
    @Override
    public void onMessage(SysUser message) {
        System.out.println("Received message : "+ message.getId());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 扩展三:自定义header

SpringBoot依赖中的Message对象(org.springframework.messaging.Message)和RocketMQ原生包中的Message对象(org.apache.rocketmq.common.message.Message)是两个不同的对象,在使用的时候非常容易弄错。

生产者

SpringBoot依赖中的MessageBuilder对象,setHeader方法设置自定义参数。

import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;

    @Resource
    private RocketMQTemplate rocketMQTemplate;

	/**
     * 异步发送JSON数据——使用自定义Header属性过滤消息
     *
     * @param dataJson   消息JSON数据
     * @param topic      主题(formats: `topicName` / `topicName:tags`)
     * @param dataId     KEY-当前数据主键ID
     * @param methodType 自定义参数-操作类型(ADD、UPDATE、DELETE)
     */
    public void asyncSendJson(String dataJson, String topic, String dataId, String methodType) {
        Message<String> message = MessageBuilder
                .withPayload(dataJson)
                .setHeader(RocketMQHeaders.KEYS, dataId)
                // 设置自定义参数,用于进行消息过滤,实现消息的分流处理。
                .setHeader("METHOD", methodType)
                .build();
        
        // 异步发送。
        rocketMQTemplate.asyncSend(topic, message, new SendCallback() {
            @Override
            public void onSuccess(SendResult result) {
                logger.info("MQ发送成功! --- {}", result.toString());
            }

            @Override
            public void onException(Throwable e) {
                logger.error("【MQ发送异常】\r\n异常记录:", e);
            }
        }, 5 * 1000);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

消费者

我们通过RocketMQ包中的Message对象,getProperty方法获取header属性值,getBody方法获取消息体。

import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;

@Component
@RocketMQMessageListener(
        nameServer = "192.168.1.111:9876",
        topic = "userTopic",
        consumerGroup = "myConsumerGroup"
)
public class SysUserConsumerReceive implements RocketMQListener<Message> {

    @Override
    public void onMessage(Message message) {
        // 获取消息header中自定义属性
        String headerMethod = message.getProperty("METHOD");
        String context = new String(message.getBody());
        if (StringUtils.isEmpty(context)) {
            throw new BusinessException("MQ消息为空或NULL值");
        }
        switch (Objects.requireNonNull(headerMethod)) {
            case "ADD":
                // 业务处理1
                break;
            case "UPDATE":
                // 业务处理2
                break;
            default:
                break;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 6.3 Spring Cloud集成RocketMQ

Spring Cloud集成RocketMQ,引入依赖包——spring-cloud-starter-stream-rocketmq

SpringCloudStreamSpring社区提供的一个统一的消息驱动框架,目的是想要以一个统一的编程模型来对接所有的MQ消息中间件产品。

SpringCloudStream官方目前只封装了kafkaRabbitMQ的具体依赖,而RocketMQ的依赖是由阿里巴巴自己来维护的。

# 代码开发

  • pom.xml
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
    <version>2.2.3.RELEASE</version>
</dependency>
1
2
3
4
5
  • 配置文件
#ScStream通用的配置以spring.cloud.stream开头
spring.cloud.stream.bindings.input.destination=TestTopic
spring.cloud.stream.bindings.input.group=scGroup
spring.cloud.stream.bindings.output.destination=TestTopic

#rocketMQ的个性化配置以spring.cloud.stream.rocketmq开头
#spring.cloud.stream.rocketmq.binder.name-server=192.168.232.128:9876;192.168.232.129:9876;192.168.232.130:9876
spring.cloud.stream.rocketmq.binder.name-server=192.168.232.128:9876
1
2
3
4
5
6
7
8

SpringCloudStream中,一个binding对应一个消息通道。

其中配置的input,是在Sink.class中定义的,对应一个消息消费者。此处input为自定义名称。

output,是在Source.class中定义的,对应一个消息生产者。此处output为自定义名称。

  • 配置类

在启动类中添加如下注解:

@EnableBinding({Source.class, Sink.class})
1

@EnableBinding,这是SpringCloudStream引入的Binder配置

  • 生产者
@Component
public class ScProducer {
    @Resource
    private Source source;
    
    public void sendMessage(String msg){
        Map<String, Object> headers = new HashMap<>();
        headers.put(MessageConst.PROPERTY_TAGS, "testTag");
        MessageHeaders messageHeaders = new MessageHeaders(headers);
        Message<String> message = MessageBuilder.createMessage(msg,
        messageHeaders);
        this.source.output().send(message);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 消费者
@Component
public class ScConsumer {
    @StreamListener(Sink.INPUT)
    public void onMessage(String messsage){
    	System.out.println("received message:"+messsage+" from binding:"+ Sink.INPUT);
	}
}
1
2
3
4
5
6
7

# 七、FAQ

# RocketMQ消息消费重试机制

顺序消费状态

  1. ConsumeOrderlyStatus.COMMIT
  2. ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT

消费者对于消息的处理,如果设置了自动提交,返回ConsumeOrderlyStatus.COMMIT是会提交offset;但是返回ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT 会校验是否重试。

# 顺序消息的重试

对于顺序消息,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时,应用会出现消息消费被阻塞的情况。

因此,在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。

# 无序消息的重试

对于无序消息(普通、延时、事务消息),当消费者消费消息失败时,可以通过设置返回状态达到消息重试的结果。

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

# 1)重试次数

消息队列 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 次重试,超过这个时间范围消息将不再重试投递。

注意: 一条消息无论重试多少次,这些重试消息的 Message ID 不会改变。

# 2)配置方式

消费失败后,重试配置方式

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

  • 返回 Action.ReconsumeLater (推荐)
  • 返回 Null
  • 抛出异常
public class MessageListenerImpl implements MessageListener {
   @Override
   public Action consume(Message message, ConsumeContext context) {
       //处理消息
       doConsumeMessage(message);
       if(exception){
       //方式1:返回 Action.ReconsumeLater,消息将重试
       return Action.ReconsumeLater;
       //方式2:返回 null,消息将重试
       return null;
       //方式3:直接抛出异常, 消息将重试
       throw new RuntimeException("Consumer Message exceotion");
     }
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

消费失败后,不重试配置方式

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

public class MessageListenerImpl implements MessageListener {
    @Override
    public Action consume(Message message, ConsumeContext context) {
        try {
            doConsumeMessage(message);
        } catch (Throwable e) {
            //捕获消费逻辑中的所有异常,并返回 Action.CommitMessage;
            return Action.CommitMessage;
        }
        //消息处理正常,直接返回 Action.CommitMessage;
        return Action.CommitMessage;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14

注意:在整合SpringBoot后,默认是当消费者处理抛出异常时就会自动重试,不需要手动编码进行处理。

自定义消息最大重试次数

消息队列 RocketMQ 允许 Consumer 启动的时候设置最大重试次数,重试时间间隔将按照如下策略:

  • 最大重试次数小于等于 16 次,则重试时间间隔同上表描述。
  • 最大重试次数大于 16 次,超过 16 次的重试时间间隔均为每次 2 小时。

原生方式:

Properties properties = new Properties();
//配置对应 Group ID 的最大消息重试次数为 20 次
properties.put(PropertyKeyConst.MaxReconsumeTimes,"20");
Consumer consumer =ONSFactory.createConsumer(properties);
1
2
3
4

整合SpringBoot方式:

// 通过设置监听器的 maxReconsumeTimes 属性设置,比如设置为20
@Component
@RocketMQMessageListener(maxReconsumeTimes = 20,topic = "xxx", consumerGroup = "xxx")
public class TestConsumer implements RocketMQListener<String> {}
注意:
1
2
3
4
5
  • 消息最大重试次数的设置对相同 Group ID 下的所有 Consumer 实例有效。
  • 如果只对相同 Group ID 下两个 Consumer 实例中的其中一个设置了 MaxReconsumeTimes,那么该配置对两个 Consumer 实例均生效。
  • 配置采用覆盖的方式生效,即最后启动的 Consumer 实例会覆盖之前的启动实例的配置

获取消息重试次数

消费者收到消息后,可按照如下方式获取消息的重试次数:

原生方式:

public class MessageListenerImpl implements MessageListener {
    @Override
    public Action consume(Message message, ConsumeContext context) {
        //获取消息的重试次数
        System.out.println(message.getReconsumeTimes());
        return Action.CommitMessage;
    }
}

1
2
3
4
5
6
7
8
9

整合SpringBoot方式:

// 通过实现RocketMQListener<MessageExt>接口,丛MessageExt实体中获取
@Component
@RocketMQMessageListener(topic = "xxx", consumerGroup = "xxx")
public class ExConsumer implements RocketMQListener<MessageExt> {

    @Override
    public void onMessage(MessageExt message) {
        // 重试次数
        message.getReconsumeTimes();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12

# Broker启动报错delayOffset

问题描述:

由于电脑断电重启Windows环境下,启动Broker报错load delayOffset.json failed

查看日志发写,\store\config下的delayOffset.json文件内容为空(NUL)。

解决:

将其修改为:

{
	"offsetTable":{}
}
1
2
3

# Windows Broker闪退问题

记录Windows RocketMQ 4.9.0 服务能启动 Broker闪退问题。

解决:

  1. 找到日志目录

    在根目录 conf 文件夹下的 logback_broker.xml 文件中,找到日志所在目录(第 22 行)。

  2. 打开当前用户目录

    打开当前登录账户所在C盘目录。

    Win+R打开运行中 ,输入英文点 . ,回车即可进入相应目录。

  3. 分析日志信息

    在文件 broker_default.log 中,并没有找到异常信息;

    然后查看 broker.log;

# 八、常用MQ比较