1

This article focuses on the practical application of RocketMQ, and the theoretical part will be discussed in another article. Not much to say here, let's go directly to the actual combat.

1. Configuration

Usually, the development directly depends on rocketmq-spring-boot-starter . The starter contains all the required dependencies, such as:

  • rocketmq-client : It encapsulates the client's application and also includes netty's communication services.
  • rocketmq-acl : Access Control Service.

starter also provides many ready-made packages, such as: RocketMQTemplate.java , RocketMQListener.java , RocketMQUtil.java etc., which are often used in application development.

It is recommended to use the above rocketmq-spring-boot-starter directly. I have seen some companies encapsulate a service to replace the official starter for internal compatibility. But this service except adding some custom programs, other classes and methods are copied starter .
When the subsequent rocketmq-spring-boot-starter upgraded, or bugs are fixed, or functions are expanded, it is difficult to upgrade the company's internal services unless it is copied from scratch. When there is no corresponding volume within the company, it is recommended not to learn from big factories to package basic services by themselves, otherwise it will be easy to ride a tiger.

pom depends on
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>

At the time of writing, the latest version of the starter is 2.2.0 , and the corresponding rocketmq-client and rocketmq-acl versions are 4.8.0 .

It is very important to rocketmq-spring-boot-starter version, because 061f2c6eeb7452 has been iterating rapidly, and many classes and methods will change in the new version, such as the tag, message transaction, etc. mentioned below, which is why it is not recommended to encapsulate the starter within the company.

application
# rocketmq 配置项,对应 RocketMQProperties 配置类
rocketmq:
  name-server: 127.0.0.1:9876 # RocketMQ Namesrv
  # Producer 配置项
  producer:
    group: koala-dev-event-centre-group # 生产者分组
    send-message-timeout: 3000 # 发送消息超时时间,单位:毫秒。默认为 3000 。
    compress-message-body-threshold: 4096 # 消息压缩阀值,当消息体的大小超过该阀值后,进行消息压缩。默认为 4 * 1024B
    max-message-size: 4194304 # 消息体的最大允许大小。。默认为 4 * 1024 * 1024B
    retry-times-when-send-failed: 2 # 同步发送消息时,失败重试次数。默认为 2 次。
    retry-times-when-send-async-failed: 2 # 异步发送消息时,失败重试次数。默认为 2 次。
    retry-next-server: false # 发送消息给 Broker 时,如果发送失败,是否重试另外一台 Broker 。默认为 false
    access-key: # Access Key ,可阅读 https://github.com/apache/rocketmq/blob/master/docs/cn/acl/user_guide.md 文档
    secret-key: # Secret Key
    enable-msg-trace: true # 是否开启消息轨迹功能。默认为 true 开启。可阅读 https://github.com/apache/rocketmq/blob/master/docs/cn/msg_trace/user_guide.md 文档
    customized-trace-topic: RMQ_SYS_TRACE_TOPIC # 自定义消息轨迹的 Topic 。默认为 RMQ_SYS_TRACE_TOPIC 。
  # Consumer 配置项
  consumer:
    listeners: # 配置某个消费分组,是否监听指定 Topic 。结构为 Map<消费者分组, <Topic, Boolean>> 。默认情况下,不配置表示监听。
      erbadagang-consumer-group:
        topic1: false # 关闭 test-consumer-group 对 topic1 的监听消费

Rocketmq has many configurations. In addition to the basic server configuration, there are also acl, producer, consumer, etc. But usually there will be multiple consumers in a service, it is recommended to implement in code. And if there is only one producer, it can be configured.

2. Ordinary message sending

For the source code of the message sent by rocketmq, it is recommended to check org.apache.rocketmq.client.producer.DefaultMQProducer . Most of the properties in the class correspond to the parameters in the configuration file.

2.1. Three types of message sending

Only ordinary message sending methods are discussed here, which are different from message sending methods such as sequence, transaction, delay/timing, etc. There are currently three types:

  • Synchronization (sync): Synchronous sending means that after the producer sends a message, it will wait synchronously, and will continue to send the next message after receiving the response from the broker.
  • Asynchronous (async): Asynchronous sending means that after the producer sends a message, it does not need to wait for the broker's response before sending the next message. Asynchronous sending can also process the response result of the message, and it is necessary to implement the asynchronous sending callback interface when sending the message.
  • one-way (oneWay): is a one-way communication method, that is to say, the producer is only responsible for sending messages, not waiting for the broker to send back the response result, and no callback function is triggered, which means that the producer only sends requests and not Wait for the response result.
Three Sending Methods
sending methodsend TPSsend result responsereliabilityscenes to be used
SynchronizegenerallyhavehighImportant Notification Scenarios
asynchronousquickhavehighScenarios that pay more attention to RT (response time)
one-wayfastestnoneLowScenarios with low reliability requirements
async thread pool configuration for asynchronous execution
this.defaultAsyncSenderExecutor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors(), 60000L, TimeUnit.MILLISECONDS, this.asyncSenderThreadPoolQueue, new ThreadFactory() {
            private AtomicInteger threadIndex = new AtomicInteger(0);

            public Thread newThread(Runnable r) {
                return new Thread(r, "AsyncSenderExecutor_" + this.threadIndex.incrementAndGet());
            }
        });
Three send message codes
    private String convertDestination(String topic, String tag) {
        return StringUtils.isBlank(tag) ? topic : StringUtils.join(topic, ":", tag);
    }

    /**
     * 同步发送
     */
    public SendResult syncSend(String topic, String tag, String content) {
        String destination = this.convertDestination(topic, tag);
        return rocketMQTemplate.syncSend(destination, content);
    }

    /**
     * 异步发送
     */
    public void asyncSend(String topic, String tag, String content, SendCallback sendCallback) {
        String destination = this.convertDestination(topic, tag);
        rocketMQTemplate.asyncSend(destination, content, sendCallback);
    }

    /**
     * 单向发送
     */
    public void sendOneWay(String topic, String tag, String content) {
        String destination = this.convertDestination(topic, tag);
        rocketMQTemplate.sendOneWay(destination, content);
    }

2.2. Bulk sending

Batch message sending is to package multiple messages of the same topic together and send them to the message server, reducing the number of network calls and improving network transmission efficiency.

Of course, it is not that the more messages sent in the same batch, the better the performance. The judgment is based on the length of a single message. If the content of a single message is relatively long, sending multiple messages in a package will affect the response of other threads to send messages. time, and a single batch length can not exceed the total message DefaultMQProducer # maxMessageSize, i.e. the configuration file rocketmq.producer.max-message-size .

code
    /**
     * 同步-批量发送
     */
    public SendResult syncBatchSend(String topic, String tag, List<String> contentList) {
        String destination = this.convertDestination(topic, tag);
        List<Message<String>> messageList = contentList.stream()
                .map(content -> MessageBuilder.withPayload(content).build())
                .collect(Collectors.toList());
        return rocketMQTemplate.syncSend(destination, messageList);
    }

4. Tag

In rocketmq, both topic and tag are used for business classification. The difference is that topic is a first-level classification, while tag can be understood as a second-level classification. By definition:

  • topic: message topic, which classifies different business messages by topic.
  • tag: message tag, which is used to further distinguish the message classification under a topic, and the attribute that the message is sent from the producer.

In actual business, when should topic or tag be used? There are several suggestions:

  • message types are consistent: example, ordinary messages, transaction messages, timing (delayed) messages, and sequence messages, different message types use different topics, which cannot be distinguished by tags.
  • business is related: not directly related to the news, such as Taobao transaction news, Jingdong logistics news uses different topics to distinguish; and the same is Tmall transaction news, the news of electrical orders, women's clothing orders, cosmetics orders can be Use Tag to distinguish.
  • priority of 161f2c6eeb77ad messages is the same: also a logistics message, Hema must be delivered within hours, Tmall supermarket within 24 hours, Taobao logistics is relatively slower, and messages with different priorities are distinguished by different topics. .
  • message magnitude of Some business messages are small in volume but require high real-time performance. If the same topic is used with some trillion-level messages, it may starve to death due to excessive waiting time. , you need to split messages of different magnitudes and use different topics.

Let's take an example of a real project: a project I just made is called a sharing center, and all the resources that need to be shared come from various business service parties. Instructions such as creating shared resources, withdrawing shared resources, etc., I define them as different topics. In the message body sent to the topic, there is a "resource type" field. In fact, each service access party only cares about the message corresponding to its own resource type, so the "resource type" is defined as a tag, and the consumers of each business party only You can monitor the tags you need.

If there is no tag mechanism, the consumer has to receive all messages, and after deserialization, only the messages corresponding to the "resource type" are processed. With the tag mechanism, messages are automatically filtered and distributed on the way to the consumer.

and RabbitMQ AMQP protocol comparison

After using the tag mechanism, I immediately thought of the AMQP protocol of RabbitMQ, which is very similar to the mechanism of exchanges and queues. When tags are used, it is like a sector switch; when tags are not used, it is like a direct-connected switch.

  • Exchange: The message exchange, which specifies what rules the message is routed to which queue.
  • Binding: Binding, its function is to bind Exchange and Queue according to routing rules.
  • Queue: message queue carrier, each message will be put into one or more queues.

I think the purpose of both designs is the same, that is, to decouple the producer and the consumer, so that a message can flow freely to different message ends. In this regard, compared to RabbitMQ, RocketMQ does not provide rich enough functions, but it is more practical and concise.

Example code: Producer

From the previous example code, the simplest sending synchronous message code, RocketMQTemplate class encapsulated in the starter , the message sending target is String destination . And it contains topic and tag . That is, in the private conversion method: destination = topic:tag .

    private final RocketMQTemplate rocketMQTemplate;

    private String convertDestination(String topic, String tag) {
        return StringUtils.isBlank(tag) ? topic : StringUtils.join(topic, ":", tag);
    }

    /**
     * 同步发送
     */
    public SendResult syncSend(String topic, String tag, String content) {
        String destination = this.convertDestination(topic, tag);
        return rocketMQTemplate.syncSend(destination, content);
    }
Sample Code: Consumer
@RocketMQMessageListener(consumerGroup = ShareRocketMqConstants.GROUP_PREFIX + ShareRocketMqConstants.TOPIC_SHARE_RSRC_TO_BIZ_CALLBACK,
        topic = ShareRocketMqConstants.TOPIC_SHARE_RSRC_TO_BIZ_CALLBACK,
        selectorExpression = "2||3||4", 
        consumeThreadMax = 3)
public class ShareRsrcMqConsumer implements RocketMQListener<RsrcToBiz4Mq> {
     ... ...
}

In the above consumer code, it is stated that only messages with tag values of 2, 3, and 4 are consumed. There are two core attributes in the annotation:

  • selectorType: default value of 061f2c6eeb7942 is SelectorType.TAG, so it is not set in the sample code.
  • The expression corresponding to selectorExpression: For SelectorType.TAG type, you need to set the tag expression. The default value is * , that is, all tags are consumed. If you want to specify the consumption of multiple tags, use || or match to connect.

Note: On the 161f2c6eeb798a producer side, only one tag can be specified when However, on the consumer side, multiple tags can be specified for receiving messages.

5. Delayed/Timed Messages

Timing message means that after the message is sent to the broker, it cannot be consumed by the consumer immediately, and can only be consumed after a specific time point or waiting for a specific time.

Principle

In fact, the implementation principle of timed messages is relatively simple. If a message corresponding to a topic is set as a timed message at the sender, the message will be stored in SCHEDULE_TOPIC_XXXX first, and the information of the original message will be stored in the commitLog file. , since the topic is SCHEDULE_TOPIC_XXXX, the message will not be sent immediately, and then the message that arrives at the delay time will be converted into a correct message by regular scanning, and sent to the corresponding queue for consumption.

Latency level

Although rocketmq supports timing messages, the timing time supported by the current open source version of rocketmq is limited and time with different levels of precision, not any unlimited timing time. By default, the Broker server has 18 timing levels, and each level corresponds to a different delay time:

Latency leveldelay
11s
25s
310s
430s
51m
62m
73m
84m
95m
106m
117m
128m
139m
1410m
1520m
1630m
171h
182h
code

There is no special method for sending delayed messages, but it is overloaded based on the common method of sending messages (such as: rocketMQTemplate.syncSend), and an incoming parameter int delayLevel . The default value is 0, that is, it is sent immediately.

    /**
     * 同步延迟发送
     *
     * @param delayLevel 延时等级:现在RocketMq并不支持任意时间的延时,需要设置几个固定的延时等级,从1s到2h分别对应着等级 1 到 18
     *                   1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
     */
    public SendResult syncSendDelay(String topic, String tag, String content, long timeout, int delayLevel) {
        String destination = this.convertDestination(topic, tag);
        Message message = MessageBuilder.withPayload(content).build();
        return rocketMQTemplate.syncSend(destination, message, timeout, delayLevel);
    }

6. Sequence messages

A sequential message is a message that has strict requirements on the order of message sending and consumption. For a specified topic, messages are published and consumed in strict accordance with the principle of first-in, first-out (FIFO), that is, messages published first are consumed first and then published. consumption after the message.

RocketMQ can only guarantee sequential messages in the same partition, so the messages in the queue must be in the same partition. Therefore, the following scenarios are implemented in the following ways:

  • partitions are ordered: RocketMQ supports sequential messages within the same queue partition. In another topic, all messages are partitioned according to ShardingKey, and messages with the same ShardingKey must be sent to the same partition queue. Therefore, it is only necessary to ensure that the messages are sent according to the same ShardingKey, and then to ensure that the Consumer consumes the same queue in a single thread.
  • Global order: When there is only one partition under Topic, global order can be achieved.

The performance of global ordering is too poor, and partition ordering is recommended. Suppose we want to process messages within the order through mq. For the same topic, usually we only need to ensure that the messages under the same order are published and consumed in order, and the messages under different orders should not interfere with each other. Therefore, partition order can be used to convert the order number into ShardingKey, as long as the messages under the same order are guaranteed to flow to the same queue, and then consumed sequentially.

The most common way to convert order number to ShardingKey is hashKey.

By the way, this is the same as the sequential consumption implementation of RabbitMQ messages are also FIFO in the same queue, so the implementation of sequential consumption can only deliver similar messages to one queue.

Producer
    /**
     * 同步顺序发送
     *
     * @param hashKey 根据 hashKey 和 队列size() 取模,保证同一 hashKey 的消息发往同一个队列,以实现 同一hashKey下的消息 顺序发送
     *                因此 hashKey 建议取 业务上唯一标识符,如:订单号,只需保证同一订单号下的消息顺序发送
     */
    public SendResult syncSendOrderly(String topic, String tag, String content, String hashKey) {
        String destination = this.convertDestination(topic, tag);
        Message message = MessageBuilder.withPayload(content).build();
        return rocketMQTemplate.syncSendOrderly(destination, message, hashKey);
    }
Consumer

For the consumption of sequential messages, the code is also very easy, mainly the @RocketMQMessageListener annotation, by setting consumeMode = ConsumeMode.ORDERLY , it means that sequential consumption is used.

ConsumeMode has two values:

  • CONCURRENTLY : Default, concurrently receive asynchronously delivered messages concurrently.
  • ORDERLY : Open for sequential consumption, only one thread is opened, and only one queue of messages is received in order at the same time.
@RocketMQMessageListener(topic = "xxx-topic",
        consumerGroup = "xxxGroup",
        consumeMode = ConsumeMode.ORDERLY)
public class OrderConsumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        ... ...
    }
}
question: If multiple spring instances are started at the same time for sequential message consumers, will it affect it?

I thought about this problem for a long time at the time. In order to ensure that messages are consumed in order, consumers are single-threaded. But the actual online program will not be a single node. If there are multiple spring instances, can it be understood as "multi-threaded" processing?

First, let’s review a few knowledge points:

  • Sequential consumption can only be used for "cluster mode" consumption, ie messageModel = MessageModel.CLUSTERING .
  • In cluster mode, how do multiple consumers load the message queue? The message queue load mechanism follows a general idea: A message queue is only allowed to be consumed by one consumer at the same time, and a consumer can consume multiple message queues.
  • Although in consumer code, RocketMQ listeners are like "push mode" in mq. But in fact, the RocketMQ message push mode is implemented based on the pull mode, wrapping a layer on the pull mode. After one pull task is completed, the next pull task starts.

When consuming sequential messages, each time a task is pulled, it will apply to the broker to lock the queue. Therefore, even if there are multiple consumer instances running at the same time, sequential messages in a single queue are still consumed sequentially.

7. Transactional messages

Here, it is emphasized that the transaction mechanism of RocketMQ is not the same as the distributed transaction mechanism that we usually say to achieve eventual consistency through MQ.

The transaction mechanism of RocketMQ is only reflected in the producer, which guarantees the producer's local transaction execution and message sending, and the two transactions reach consistency. As for the transaction processing after the consumer receives the message, it is not within the current mechanism.

normal transaction

The normal transaction flow follows the scheme 2PC

  1. Call the send transaction message method to send the message normally. The method to send the transaction is named syncSendInTransaction .
  2. After the mq server successfully receives the message, the message is in a half-received state and responds to the producer client.
  3. After the producer receives the response successfully received by the server, it executes the local transaction. The local transaction is written in the executeLocalTransaction method, and the return result is the enumeration RocketMQLocalTransactionState , which has three values COMMIT、ROLLBACK、UNKNOWN
  4. After the server receives the COMMIT status, it will send the message to the consumer
  5. After the server receives the ROLLBACK status, it will delete the current half-received message and no longer process it.
  6. The server side receives the UNKNOWN status, or the server side does not receive the message after timeout, or the producer does not respond to the status, then the message compensation mechanism will be implemented.
Message compensation mechanism

This part is relatively simple. For several situations in point 6 of the above transaction process, a message check will be triggered.

  1. When the transaction message appears UNKNOWN , timeout, and no response, the server will actively call the producer's local check-back method checkLocalTransaction to query the execution of the local transaction, and the returned result is still the enumeration value RocketMQLocalTransactionState .
  2. The processing flow of the server receiving the returned result is the same as the previous normal flow.
  3. If it is still UNKNOWN , timeout, and no response, it will continue to retry. If there is still no result after exceeding the maximum number of retries, it will be regarded as ROLLBACK and the current message will be deleted.

The parameters related to transaction messages are basically org.apache.rocketmq.common.BrokerConfig class, such as the default values of the following common attributes:

  • transactionTimeOut = 6000L : The server does not receive a transaction local message with a timeout of 1 minute.
  • transactionCheckMax = 15 message compensation mechanism is 15.
  • transactionCheckInterval = 6000L message compensation mechanism is 1 minute.

Because it is BrokerConfig class, if you don't want to use the default value, you can broker.conf file.

Code: Producer sends transaction message
    /**
     * 事务发送
     */
    public TransactionSendResult syncSendInTransaction(String topic, String tag, String content) {
        String destination = this.convertDestination(topic, tag);
        String transactionId = UUID.randomUUID().toString();
        Message message = MessageBuilder.withPayload(content)
                .setHeader(RocketMQHeaders.TRANSACTION_ID, transactionId)
                .build();
        return rocketMQTemplate.sendMessageInTransaction(destination, message, content);
    }
Code: Producer defines transaction native method

On the producer client, the two above methods of the RocketMQLocalTransactionListener

@RocketMQTransactionListener
public class LocalTransactionListener implements RocketMQLocalTransactionListener {
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
        System.out.println("executeLocalTransaction: "+ LocalDateTime.now());
        return RocketMQLocalTransactionState.UNKNOWN;
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        System.out.println("checkLocalTransaction: "+ LocalDateTime.now());
        return RocketMQLocalTransactionState.COMMIT;
    }
}
Question: If there are multiple transaction message producers in the spring project, how to distinguish different RocketMQLocalTransactionListener ?

@RocketMQLocalTransactionListener This annotation provides attributes that can distinguish different transaction message producers. In stater version 2.0.4, the txProducerGroup attribute is provided to point to a message sender group, which maps different transaction message sending logic. But there seems to be a bug. In subsequent new version iterations, this attribute is removed.

In version 2.1.1, it can only be achieved by specifying rocketMQTemplateBeanName , that is, when different transaction messages are sent, different RocketMQTemplates must be defined. It's quite troublesome, and I look forward to this feature being perfected in subsequent iterations.

8. Retry queue, dead letter queue

RocketMQ has many security mechanisms for multiple exceptions, such as the mechanism of message retransmission, which can be divided into two categories:

  • Producer retransmission: In the previous three methods of sending messages, when synchronous and asynchronous transmission fails, they will be retransmitted. The corresponding retransmission times correspond to the property values retryTimesWhenSendFailed and retryTimesWhenSendAsyncFailed DefaultMQProducer class, and can also be configured in properties custom settings in the file.
  • Consumer retransmission: When the message has entered the broker, the consumer fails to receive it, and the broker will also retransmit it to the consumer. The following derives the retry queue and dead letter queue.
retry queue

If the consumer side fails to consume this time due to various types of exceptions, in order to prevent the loss of the message, it needs to be sent back to the broker side to save, and the message queue that saves this kind of message queue that is sent back to mq because the exception cannot be consumed normally is called for the retry queue.

RocketMQ will have settings for each consumer group a topic name “%RETRY%+consumerGroup” retry queue (It should be noted that this Topic retry queue is for the consumer group, rather than a set for each Topic).

It is used to temporarily save messages that the consumer cannot consume due to various exceptions. Considering that it takes some time to recover from an exception, multiple retry levels will be set for the retry queue. Each retry level has a corresponding re-delivery delay. The more retries, the greater the delivery delay. RocketMQ's processing of retry messages is first saved to “SCHEDULE_TOPIC_XXXX” , and the background scheduled tasks are delayed according to the corresponding time and then saved to the retry queue of "%RETRY%+consumerGroup".

Dead letter queue

Due to some reasons, the consumer side cannot consume the business messages pulled from the broker side normally for a long time. In order to ensure that the messages will not be discarded for no reason, after exceeding the configured "maximum number of retry consumption", it will be moved to this dead end. in the letter queue.

In RocketMQ, the SubscriptionGroupConfig configuration constant sets two parameters by default, one is retryQueueNums is 1 (the number of retry queues is 1), and the other is retryMaxTimes is 16 (the maximum number of retry consumption is 16 times). The Broker side checks and judges that if the maximum number of retry consumption is exceeded, the message will be moved to the dead letter queue mentioned here. Here, RocketMQ will have to set a topic named for each consumer group “%DLQ%+consumerGroup" dead letter queue. But if a consumer group does not generate dead letter messages, the message queue RocketMQ will not create a corresponding dead letter queue for it.

Because the messages in the dead letter queue cannot be consumed, it also confirms that some messages have unexpected behavior. Therefore, in practical applications, the messages moved to the dead letter queue require manual intervention. For example, use the console to check whether there is a private message queue. After solving the problem, you can manually resend the message on the console.


KerryWu
641 声望159 粉丝

保持饥饿