SpringCloud microservice technology stack. Dark horse follow-up (12)

SpringCloud microservice technology stack. Dark horse follow-up 12

today's goal

insert image description here

Service Asynchronous Communication - Advanced

During the use of message queues, there are many practical problems that need to be considered:
insert image description here

1. Message reliability

From sending a message to receiving it by a consumer, there will be multiple processes:
insert image description here
each step may cause the message to be lost. Common reasons for loss include:

  • Lost on send:
    • The message sent by the producer was not delivered to the exchange
    • The message did not reach the queue after it arrived at the exchange
  • MQ is down, the queue will lose the message
  • The consumer crashes without consuming the message after receiving the message

For these problems, RabbitMQ gives solutions respectively:

  • Producer Confirmation Mechanism
  • mq persistence
  • Consumer Confirmation Mechanism
  • Failure retry mechanism

Below we demonstrate each step through a case.
First, import the demo project provided by the pre-class materials:
insert image description here
the project structure is as follows:
insert image description here
just start it with docker

docker start mq

To create a queue named simple.queue
insert image description here
and then bind the amq.topic switch to the queue simple.queue created above in the switch, we manually configure it.
insert image description here
After entering the amq.topic switch, bind the queue.
insert image description here
After binding, the figure is as follows:
insert image description here

1.1. Producer message confirmation

RabbitMQ provides a publisher confirm mechanism to avoid loss of messages sent to MQ. This mechanism must assign a unique ID to each message. After the message is sent to MQ, a result will be returned to the sender, indicating whether the message is processed successfully.

There are two ways to return results:

  • publisher-confirm, the sender confirms
    • The message is successfully delivered to the switch and returns ack
    • The message was not delivered to the exchange, return nack
  • publisher-return, sender receipt
    • The message was delivered to the exchange, but not routed to the queue. Return ACK and the reason for routing failure.

insert image description here
insert image description here

Notice:
insert image description here

1.1.1. Modify configuration

First, modify the application.yml file in the publisher service and add the following content:

spring:
  rabbitmq:
    publisher-confirm-type: correlated
    publisher-returns: true
    template:
      mandatory: true

illustrate:

  • publish-confirm-type: Enable publisher-confirm, here supports two types:
    • simple: Synchronously wait for the confirm result until timeout
    • correlated⭐: Asynchronous callback, define ConfirmCallback, this ConfirmCallback will be called back when MQ returns the result
  • publish-returns: Enable the publish-return function, which is also based on the callback mechanism, but defines the ReturnCallback
  • template.mandatory: Defines the policy when message routing fails. true, call ReturnCallback; false: discard the message directly

1.1.2. Define the Return callback

Only one ReturnCallback can be configured per RabbitTemplate, so it needs to be configured when the project is loaded:

Modify the publisher service and add one:

package cn.itcast.mq.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
    
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    
    
        // 获取RabbitTemplate
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        // 设置ReturnCallback
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
    
    
            // 投递失败,记录日志
            log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
                     replyCode, replyText, exchange, routingKey, message.toString());
            // 如果有业务需要,可以重发消息
        });
    }
}

1.1.3. Define ConfirmCallback

ConfirmCallback can be specified when sending a message, because the logic of each business processing confirm success or failure is not necessarily the same.

In the cn.itcast.mq.spring.SpringAmqpTest class of the publisher service, define a unit test method:

public void testSendMessage2SimpleQueue() throws InterruptedException {
    
    
    // 1.消息体
    String message = "hello, spring amqp!";
    // 2.全局唯一的消息ID,需要封装到CorrelationData中
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    // 3.添加callback
    correlationData.getFuture().addCallback(
        result -> {
    
    
            if(result.isAck()){
    
    
                // 3.1.ack,消息成功
                log.debug("消息发送成功, ID:{}", correlationData.getId());
            }else{
    
    
                // 3.2.nack,消息失败
                log.error("消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason());
            }
        },
        ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage())
    );
    // 4.发送消息
    rabbitTemplate.convertAndSend("task.direct", "task", message, correlationData);

    // 休眠一会儿,等待ack回执
    Thread.sleep(2000);
}

After all configurations are completed, run the test class SpringAmqpTest.java, which means that the message is sent successfully.
insert image description here
Then, we have a situation where the message fails to be sent. We deliberately fill in the name of the switch.
insert image description here
After the call, the background print log is as follows:
insert image description here
Then we try to fill in the wrong, Take a look at routingKey.
insert image description here
The error message is as follows:
insert image description here
After that, we restore the code and make sure it is correct.

Summary:
Several situations for processing message confirmation in SpringAMQP:
● publisher-comfirm:

  • The message is successfully sent to the exchange and returns ack
  • Failed to send the message, did not reach the switch, returned nack
  • An exception occurred during message sending and no receipt was received

● The message is successfully sent to the exchange, but not routed to the queue,

  • callReturnCallback

1.2. Message Persistence

The producer confirms that the message can be delivered to the RabbitMQ queue, but after the message is sent to RabbitMQ, if there is a sudden downtime, the message may also be lost.

To ensure that messages are safely stored in RabbitMQ, the message persistence mechanism must be enabled.

  • switch persistence
  • queue persistence
  • message persistence

1.2.1. Switch Persistence

The switch in RabbitMQ is non-persistent by default, and it will be lost after mq restarts.


We restart mq by command

docker restart mq

Then check the status of the queue and the switch. For example, we created a persistent queue.
insert image description here
In SpringAMQP, you can specify the persistence of the switch through code:

@Bean
public DirectExchange simpleExchange(){
    
    
    // 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除
    return new DirectExchange("simple.direct", true, false);
}

In fact, by default, switches declared by Spring AMQP are persistent.

You can see the mark on the persistent switch in the RabbitMQ console D:
insert image description here

1.2.2. Queue Persistence

The queue in RabbitMQ is non-persistent by default, and it will be lost after mq restarts.
In SpringAMQP, switch persistence can be specified by code:

We can go to the mq graphical interface to delete simple.queue

@Bean
public Queue simpleQueue(){
    
    
    // 使用QueueBuilder构建队列,durable就是持久化的
    return QueueBuilder.durable("simple.queue").build();
}

In fact, by default, queues declared by Spring AMQP are persistent.
You can see the mark on the persistent queue in the RabbitMQ console D:
insert image description here

After these are done, we start ConsumerApplication.java, and then view the graphical interface of mq.
The switch is persistent
insert image description here
and the queue is persistent.
insert image description here

1.2.3. Message persistence

First stop the consumer service, don't consume our messages.
We click on the simple.queue queue in the mq graphical interface, then edit the message, click send
insert image description here
to see that there is 1 message,
insert image description here
and then we restart the mq in the docker

docker restart mq

Then come back and look at the graphical interface of mq, and find that the queue is still there, but the message is gone
insert image description here

When using SpringAMQP to send messages, you can set the properties of the message (MessageProperties) and specify the delivery-mode:

  • 1: non-persistent
  • 2: Persistence

Specify with java code:
insert image description here

By default, any message sent by Spring AMQP is persistent, without specifying it.
After running the test class SpringAmqpTest.java, check the graphical interface of mq
insert image description here
to check the specific information
insert image description here
and then restart the mq container of docker

docker restart mq

insert image description here

Note: The switches, queues, and messages created in AMQP are persistent by default
switch:
insert image description here
queue:
insert image description here

information:
insert image description here
insert image description here

1.3. Consumer message confirmation

RabbitMQ is a mechanism that burns after reading . RabbitMQ confirms that the message will be deleted immediately after being consumed by the consumer.
RabbitMQ confirms whether the consumer has successfully processed the message through the consumer receipt: after the consumer obtains the message, it should send an ACK receipt to RabbitMQ to indicate that it has processed the message.

Imagine this scenario:

  • 1) RabbitMQ delivers messages to consumers
  • 2) After the consumer gets the message, it returns ACK to RabbitMQ
  • 3) RabbitMQ delete message
  • 4) The consumer is down, and the message has not been processed

In this way, the message is lost. Therefore, the timing of the consumer returning ACK is very important.

SpringAMQP allows configuration of three confirmation modes:

  • manual: manual ack, you need to call the api to send ack after the business code ends.
  • auto⭐: Automatic ack. Spring monitors whether the listener code is abnormal. If there is no exception, it will return ack; if an exception is thrown, it will return nack.
  • none: close ack, MQ assumes that the consumer will successfully process the message after getting it, so the message will be deleted immediately after delivery

From this we can see:

  • In none mode, message delivery is unreliable and may be lost
  • The auto mode is similar to the transaction mechanism. When an exception occurs, it returns nack, and the message is rolled back to mq; if there is no exception, it returns ack
  • manual: According to the business situation, judge when to ack

Generally, we can use the default auto.

1.3.1. Demo none mode

Modify the application.yml file of the consumer service and add the following content:

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: none # 关闭ack

Modify the method in the SpringRabbitListener class of the consumer service to simulate a message processing exception:
Modify SpringRabbitListener.java

@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(String msg) {
    
    
    log.info("消费者接收到simple.queue的消息:【{}】", msg);
    // 模拟异常
    System.out.println(1 / 0);
    log.debug("消息处理完成!");
}

The test can find that when the message processing throws an exception, the message is still deleted by RabbitMQ.
Dubug starts Consumer
and finds that the message has not been received yet, so it disappears immediately
insert image description here

insert image description here
That is to say, although the consumer has received the message, if the consumer has not read it, an error occurs or a downtime occurs, the message will be lost

1.3.2. Demo auto mode

Change the confirmation mechanism to auto again:

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: auto # 关闭ack

We go to the graphical interface of mq to create a message.
insert image description here
After sending it, we see that there is a message in the graphical interface.
insert image description here
Because we think that we have written a 1/0 wrong arithmetic operation in the IDEA background, IDEA keeps resending the request to retry the push of the message. , which obviously does not meet our requirements
insert image description here

Break the point at the exception position and send the message again. When the program is stuck at the breakpoint, you can find that the message status is unack (undetermined status): after the
insert image description here
exception is thrown, because Spring will automatically return nack, the message will return to the Ready state , and was not removed by RabbitMQ:
insert image description here

1.4. Consumption failure retry mechanism

When the consumer has an exception, the message will continue to requeue (re-enter the queue) to the queue, and then resend to the consumer, and then the exception again, requeue again, infinite loop, causing the message processing of mq to soar, bringing unnecessary pressure:
insert image description here
How to do it?

1.4.1. Local retry

We can use Spring's retry mechanism to use local retry when an exception occurs in the consumer, instead of unlimited requeue to the mq queue.

Modify the application.yml file of the consumer service and add the content:

spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true # 开启消费者失败重试
          initial-interval: 1000 # 初始的失败等待时长为1秒
          multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
          max-attempts: 4 # 最大重试次数
          stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false

Modify SpringRabbitListener.java
to the form of log printing

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg) {
    
    
        log.debug("消费者接收到simple.queue的消息:【" + msg + "】");
        System.out.println(1 / 0);
        log.info("消费者处理消息成功!");
    }

Restart the consumer service and repeat the previous test. It can be found:
insert image description here

  • SpringAMQP throws exception after 4 retries

insert image description here
insert image description here

AmqpRejectAndDontRequeueException, indicating that the local retry triggers

  • Check the RabbitMQ console and find that the message has been deleted, indicating that SpringAMQP returned ack at the end, and mq deleted the message

in conclusion:

  • When local retry is enabled, if an exception is thrown during message processing, it will not be requeed to the queue, but will be retried locally by the consumer
  • After the maximum number of retries is reached, Spring will return ack and the message will be discarded

1.4.2. Failure strategy

In the previous test, after reaching the maximum number of retries, the message will be discarded, which is determined by the internal mechanism of Spring.

After the retry mode is turned on, the number of retries is exhausted. If the message still fails, the MessageRecovery interface is required to handle it. It contains three different implementations:

  • RejectAndDontRequeueRecoverer: After the retries are exhausted, directly reject and discard the message. This is the default

  • ImmediateRequeueMessageRecoverer: After retries are exhausted, nack is returned, and the message is re-queued

  • RepublishMessageRecoverer: After the retries are exhausted, deliver the failure message to the specified exchange⭐
    insert image description here

A more elegant solution is RepublishMessageRecoverer. After a failure, the message will be delivered to a designated queue dedicated to storing exception messages, and subsequent processing will be done manually.

1) Define the switch and queue for processing failed messages in the consumer service

@Bean
public DirectExchange errorMessageExchange(){
    
    
    return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue(){
    
    
    return new Queue("error.queue", true);
}
@Bean
public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
    
    
    return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");
}

2) Define a RepublishMessageRecoverer, associate queues and switches

@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
    
    
    return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}

Full code:

package cn.itcast.mq.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
import org.springframework.context.annotation.Bean;

@Configuration
public class ErrorMessageConfig {
    
    
    @Bean
    public DirectExchange errorMessageExchange(){
    
    
        return new DirectExchange("error.direct");
    }
    @Bean
    public Queue errorQueue(){
    
    
        return new Queue("error.queue", true);
    }
    @Bean
    public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
    
    
        return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");
    }

    @Bean
    public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
    
    
        return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
    }
}

After the above configuration is completed, we repeat the steps to send the message.
insert image description here
After sending, we see that the failed switch has
insert image description here
a queue and a
insert image description here
look at the background of IDEA.
insert image description here
Look at the messages in error.queue. It is very clear that the error stack is output.
insert image description here

1.5. Summary

How to ensure the reliability of RabbitMQ messages?

  • Enable the producer confirmation mechanism to ensure that the producer's message can reach the queue
  • Enable the persistence function to ensure that the message will not be lost in the queue before it is consumed
  • Turn on the consumer confirmation mechanism as auto, and the spring will complete the ack after confirming that the message is processed successfully
  • Enable the consumer failure retry mechanism and set MessageRecoverer. After multiple retries fail, the message will be delivered to the abnormal switch and handed over to manual processing.

2. Dead letter switch

2.1. Getting to know dead-letter switches

2.1.1. What is a dead letter switch

What is dead letter?

When a message in a queue meets one of the following conditions, it can become a dead letter:

  • The consumer uses basic.reject or basic.nack to declare consumption failure, and the requeue parameter of the message is set to false
  • The message is an expired message, no one consumes after timeout
  • The queue message to be delivered is full and cannot be delivered

If the queue containing dead letters is configured with dead-letter-exchangeattributes and a switch is specified, the dead letters in the queue will be delivered to this switch, and this switch is called a dead letter exchange (Dead Letter Exchange, check DLX).

As shown in the figure, a message is rejected by the consumer and becomes a dead letter:
insert image description here
because simple.queue is bound to the dead letter exchange dl.direct, the dead letter will be delivered to this exchange:
insert image description here

If the dead letter switch is also bound to a queue, the message will eventually enter the queue where the dead letter is stored:
insert image description here

In addition, when the queue delivers the dead letter to the dead letter exchange, it must know two pieces of information:

  • dead letter switch name
  • The RoutingKey bound to the dead letter exchange and the dead letter queue

Only in this way can we ensure that the delivered message can reach the dead letter exchange and be correctly routed to the dead letter queue.
insert image description here

2.1.2. Using the dead letter exchange to receive dead letters (extension)

In the failure retry strategy, the default RejectAndDontRequeueRecoverer will send reject to RabbitMQ after the number of local retries is exhausted, and the message becomes a dead letter and is discarded.

We can add a dead letter switch to simple.queue and bind a queue to the dead letter switch. In this way, the message will not be discarded after it becomes a dead letter, but will be finally delivered to the dead letter exchange and routed to the queue bound to the dead letter exchange.
insert image description here

In the consumer service, we define a set of dead letter switches and dead letter queues:

// 声明普通的 simple.queue队列,并且为其指定死信交换机:dl.direct
@Bean
public Queue simpleQueue2(){
    
    
    return QueueBuilder.durable("simple.queue") // 指定队列名称,并持久化
        .deadLetterExchange("dl.direct") // 指定死信交换机
        .build();
}
// 声明死信交换机 dl.direct
@Bean
public DirectExchange dlExchange(){
    
    
    return new DirectExchange("dl.direct", true, false);
}
// 声明存储死信的队列 dl.queue
@Bean
public Queue dlQueue(){
    
    
    return new Queue("dl.queue", true);
}
// 将死信队列 与 死信交换机绑定
@Bean
public Binding dlBinding(){
    
    
    return BindingBuilder.bind(dlQueue()).to(dlExchange()).with("simple");
}

2.1.3. Summary

What kind of message becomes dead letter?

  • The message is rejected by the consumer or returns nack
  • Message timed out and not consumed
  • queue is full

What is the usage scenario of the dead letter exchange?

  • If the queue is bound to a dead letter exchange, the dead letter will be delivered to the dead letter exchange;
  • The dead letter switch can be used to collect all messages (dead letters) that consumers fail to process and hand them over to manual processing to further improve the reliability of the message queue.

2.2.TTL

If a message in a queue is not consumed after a timeout, it will become a dead letter. There are two cases of timeout:

  • The queue where the message is located has a timeout set
  • The message itself sets a timeout
    insert image description here

2.2.1. Dead letter switch receiving timeout dead letter

In the SpringRabbitListener of the consumer service, define a new consumer, and declare the dead letter switch and dead letter queue:

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "dl.ttl.queue", durable = "true"),
    exchange = @Exchange(name = "dl.ttl.direct"),
    key = "ttl"
))
public void listenDlQueue(String msg){
    
    
    log.info("接收到 dl.ttl.queue的延迟消息:{}", msg);
}

2.2.2. Declare a queue and specify TTL

To set a timeout for a queue, you need to configure the x-message-ttl attribute when declaring the queue:

@Bean
public Queue ttlQueue(){
    
    
    return QueueBuilder.durable("ttl.queue") // 指定队列名称,并持久化
        .ttl(10000) // 设置队列的超时时间,10秒
        .deadLetterExchange("dl.ttl.direct") // 指定死信交换机
        .build();
}

Note that this queue sets the dead-letter switch todl.ttl.direct

Declare the switch and bind the ttl to the switch:

@Bean
public DirectExchange ttlExchange(){
    
    
    return new DirectExchange("ttl.direct");
}
@Bean
public Binding ttlBinding(){
    
    
    return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with("ttl");
}

Send a message, but don't specify a TTL:

@Test
public void testTTLQueue() {
    
    
    // 创建消息
    String message = "hello, ttl queue";
    // 消息ID,需要封装到CorrelationData中
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    // 发送消息
    rabbitTemplate.convertAndSend("ttl.direct", "ttl", message, correlationData);
    // 记录日志
    log.debug("发送消息成功");
}

Log of sent message:
insert image description here

View the log of the received message:
insert image description here

Because the TTL value of the queue is 10000ms, which is 10 seconds. You can see that the time difference between message sending and receiving is exactly 10 seconds.

2.2.3. When sending a message, set TTL

When sending a message, it is also possible to specify a TTL:

@Test
public void testTTLMsg() {
    
    
    // 创建消息
    Message message = MessageBuilder
        .withBody("hello, ttl message".getBytes(StandardCharsets.UTF_8))
        .setExpiration("5000")
        .build();
    // 消息ID,需要封装到CorrelationData中
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    // 发送消息
    rabbitTemplate.convertAndSend("ttl.direct", "ttl", message, correlationData);
    log.debug("发送消息成功");
}

View sent message log:
insert image description here

Receive message log:
insert image description here

This time, the delay between sending and receiving was only 5 seconds.Note that when TTL is set for both the queue and the message, any expired one will become a dead letter.

2.2.4. Summary

What are the two ways of message timeout?

  • Set the ttl attribute for the queue, and the messages that exceed the ttl time after entering the queue become dead letters
  • Set the ttl attribute for the message, and the queue will become a dead letter after receiving the message exceeding the ttl time

How to realize that the consumer receives the message 20 seconds after sending a message?

  • Specify a dead-letter exchange for the message's destination queue
  • Bind the queue listened by the consumer to the dead letter exchange
  • When sending a message, set the timeout period for the message to 20 seconds

2.3. Delay Queue

Using TTL combined with dead-letter switches, we realize the effect that consumers delay receiving messages after messages are sent. This message mode is called the delay queue (Delay Queue) mode.

Use cases for delay queues include:

  • Delay in sending SMS
  • The user places an order, if the user does not pay within 15 minutes, it will be automatically canceled
  • Schedule a work meeting and automatically notify all participants 20 minutes later

Because there are so many demands for delay queues, RabbitMQ officially launched a plug-in that natively supports delay queue effects.

This plugin is the DelayExchange plugin. Refer to RabbitMQ's plugin list page: https://www.rabbitmq.com/community-plugins.html
insert image description here

For usage, please refer to the official website address: https://blog.rabbitmq.com/posts/2015/04/scheduling-messages-with-rabbitmq

2.3.1. Install the DelayExchange plugin

Reference pre-class materials:
insert image description here

1. Install the DelayExchange plugin

The official installation guide address is: https://blog.rabbitmq.com/posts/2015/04/scheduling-messages-with-rabbitmq

The above document is based on installing RabbitMQ natively on linux, and then installing the plug-in.

Because we installed RabbitMQ based on Docker before, we will explain how to install the RabbitMQ plugin based on Docker.

2. Download the plugin

RabbitMQ has an official plugin community at: https://www.rabbitmq.com/community-plugins.html

It contains various plugins, including the DelayExchange plugin we're going to use:

insert image description here

You can go to the corresponding GitHub page to download the 3.8.9 version of the plug-in, the address is https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/tag/3.8.9 This corresponds to RabbitMQ 3.8.5 and above Version.

The pre-class materials also provide downloaded plug-ins:
insert image description here

3. Upload plugin

Because we are installing based on Docker, we need to check the data volume corresponding to the RabbitMQ plugin directory first. If you are not based on Docker, please refer to the first chapter to recreate the Docker container.

The data volume name of RabbitMQ we set before is mq-plugins, so we use the following command to view the data volume:

docker volume inspect mq-plugins

The following results can be obtained:

insert image description here
Next, upload the plugin to this directory:
insert image description here

4. Install the plugin

Finally, it is installed, and you need to enter the inside of the MQ container to perform the installation. My container is named mq, so execute the following command:

docker exec -it mq bash

When executing, -itplease mqreplace the following with your own container name.

After entering the container, execute the following command to enable the plugin:

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

The result is as follows:
insert image description here

2.3.2. Principle of Delay Exchange

DelayExchange requires an exchange to be declared as delayed. When we send a message to delayExchange, the flow is as follows:

insert image description here

  • receive message
  • Determine whether the message has the x-delay attribute
  • If there is an x-delay attribute, it means that it is a delayed message, which is persisted to the hard disk, and the x-delay value is read as the delay time
  • Return the routing not found result to the message sender
  • After the x-delay time expires, re-deliver the message to the specified queue

2.3.3. Using DelayExchange

The use of the plug-in is also very simple: declare a switch, the type of the switch can be any type, just set the delayed attribute to true, and then declare the queue to bind it.

1) Declare the DelayExchange switch

Annotation-based (recommended):
insert image description here
SpringRabbitListener.java

/**
     * 延迟交换机和队列
     *
     * @param message
     */
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "delay.queue", durable = "true"),
            exchange = @Exchange(name = "delay.direct", delayed = "true"),
            key = "delay"
    ))
    public void listenDelayExchange(String message) {
    
    
        log.info("消费者接收到了delay.queue的消息" + message);
    }

It can also be based on @Bean:
insert image description here

2) Send a message

When sending a message, be sure to carry the x-delay attribute to specify the delay time:
insert image description here
SpringAmqpTest.java

    /**
     * 发送延迟消息
     *
     * @throws InterruptedException
     */
    @Test
    public void testSendDealyMessage() throws InterruptedException {
    
    
        String routingKey = "delay";

        // 创建消息
        Message message = MessageBuilder.withBody("hello, delay message !".getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
                .setHeader("x-delay", 5000)
                .build();
        // 准备CorrelationData
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        rabbitTemplate.convertAndSend("delay.direct", routingKey, message, correlationData);
    }
    log.info("发送delay消息成功!");

Take a look at the graphical interface of mq
insert image description here

Run the test class SpringAmqpTest.java to send a message. Although the delay message is sent successfully, an error is reported below.
insert image description here
Check the consumer, and the message is received successfully after 5 seconds
insert image description here

Then why is there an error here? This is because the delay switch sends the message after holding the message for 5 seconds. This is not an error report, but the message is temporarily stored for 5 seconds, and it is sent to the queue after 5 seconds. Then we Can you prevent them from reporting errors? Of course, we can continue to modify. Judging whether to resend
according to whether there is a value, or not to resend if there is a value Modify CommonConfig.java in the publisherreceivedDelay

insert image description here

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    
    
        // 获取RabbitTemplate对象
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        // 配置eturnCallback
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
    
    
            // 判断是否是延迟消息
            if (message.getMessageProperties().getReceivedDelay() > 0) {
    
    
                // 是一个延迟消息,忽略报错信息
                return;
            }
            // 记录日志
            log.error("消息发送到队列失败,响应码:{},失败原因:{},交换机:{},routingKey:{},消息:{}",
                    replyCode, replyText, exchange, routingKey, message);
            // 如果有失败的,可以进行消息的重发

        });
    }

Then send the message through the test class, and the error will disappear
insert image description here

2.3.4. Summary

What are the steps to use the delay queue plugin?

• Declare a switch and add the delayed attribute to true

• When sending a message, add the x-delay header, the value is the timeout time

3. Lazy queue

3.1. Message accumulation problem

When the speed at which producers send messages exceeds the speed at which consumers can process messages, messages in the queue will accumulate until the queue stores messages to the upper limit. Messages sent later will become dead letters and may be discarded. This is the problem of message accumulation .

insert image description here
There are three ways to solve message accumulation:

  • Add more consumers and increase consumption speed. That is the work queue mode we said before
  • Open the thread pool in the consumer to speed up message processing
  • Expand the queue volume and increase the stacking limit

To increase the queue capacity, it is obviously not possible to store messages in memory.

3.2. Lazy queue

Starting from version 3.6.0 of RabbitMQ, the concept of Lazy Queues, that is, lazy queues, has been added . The characteristics of lazy queue are as follows:

  • After receiving the message, store it directly on disk instead of memory
  • Consumers only read from disk and load them into memory when they want to consume messages
  • Support millions of message storage

3.2.1. Set lazy-queue based on the command line

To set a queue as a lazy queue, you only need to specify the x-queue-mode attribute as lazy when declaring the queue. A running queue can be changed to a lazy queue via the command line:

rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues  

Command interpretation:

  • rabbitmqctl: command-line tool for RabbitMQ
  • set_policy: add a strategy
  • Lazy: Policy name, which can be customized
  • "^lazy-queue$": Match the name of the queue with a regular expression
  • '{"queue-mode":"lazy"}': Set the queue mode to lazy mode
  • --apply-to queues : The target of the policy is all queues

3.2.2. Declare lazy-queue based on @Bean

insert image description here
New class LazyConfig.java

@Configuration
public class LazyConfig {
    
    
    /**
     * 惰性队列
     *
     * @return
     */
    @Bean
    public Queue lazyQueue() {
    
    
        return QueueBuilder.durable("lazy.queue").lazy().build();
    }

    /**
     * 普通队列
     *
     * @return
     */
    @Bean
    public Queue normalQueue() {
    
    
        return QueueBuilder.durable("normal.queue").build();
    }
}

Start the Consumer service and view the mq graphical interface
insert image description here
Modify SpringAmqpTest.java

    /**
     * 测试惰性队列
     *
     * @throws InterruptedException
     */
    @Test
    public void testSendLazyQueue() throws InterruptedException {
    
    
        for (int i = 0; i < 1000000; i++) {
    
    
            String routingKey = "lazy.queue";

            // 创建消息
            Message message = MessageBuilder.withBody("hello, lazy queue".getBytes(StandardCharsets.UTF_8))
                    .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT)
                    .build();
            rabbitTemplate.convertAndSend(routingKey, message);
            //log.info("发送lazy队列消息成功!");
        }
    }

    /**
     * 测试惰性队列
     *
     * @throws InterruptedException
     */
    @Test
    public void testSendNormalQueue() throws InterruptedException {
    
    
        for (int i = 0; i < 1000000; i++) {
    
    
            String routingKey = "normal.queue";

            // 创建消息
            Message message = MessageBuilder.withBody("hello, normal queue".getBytes(StandardCharsets.UTF_8))
                    .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT)
                    .build();
            rabbitTemplate.convertAndSend(routingKey, message);
            //log.info("发送normal队列消息成功!");
        }
    }

We see that in the lazy queue, it is not in the memory, and it is directly written to the disk, which is relatively stable.
insert image description here
We see that in the normal queue, it is written to the memory, and after writing for a while, part of it will be refreshed to the disk, which is not stable.
insert image description here

3.2.3. Declare LazyQueue based on @RabbitListener

insert image description here

3.3. Summary

The solution to the message accumulation problem?

  • Bind multiple consumers on the queue to increase consumption speed
  • Using lazy queues, you can store more messages in mq

What are the advantages of lazy queues?

  • Based on disk storage, the upper limit of messages is high
  • There is no intermittent page-out, and the performance is relatively stable

What are the disadvantages of lazy queues?

  • Based on disk storage, message timeliness will be reduced
  • Performance is limited by disk IO

4. MQ cluster

4.1. Cluster classification

RabbitMQ is written based on the Erlang language, and Erlang is a concurrency-oriented language that naturally supports cluster mode. The RabbitMQ cluster has two modes:

Ordinary cluster : It is a distributed cluster, which distributes the queues to each node of the cluster, thereby improving the concurrency capability of the entire cluster.

Mirror cluster : It is a master-slave cluster. On the basis of ordinary clusters, a master-slave backup function is added to improve the data availability of the cluster.

Although the mirror cluster supports master-slave, master-slave synchronization is not strongly consistent, and there may be a risk of data loss in some cases. Therefore, after RabbitMQ version 3.8, a new function was introduced: the arbitration queue to replace the mirror cluster, and the bottom layer uses the Raft protocol to ensure the data consistency between the master and the slave.

4.2. Ordinary cluster

4.2.1. Cluster structure and characteristics

Ordinary clusters, or classic clusters, have the following characteristics:

  • Part of the data will be shared between each node in the cluster, including: switch and queue metadata. Messages in the queue are not included.
  • When accessing a node in the cluster, if the queue is not on the node, it will be passed from the node where the data is located to the current node and returned
  • If the node where the queue is located goes down, the messages in the queue will be lost

The structure is shown in the figure:

insert image description here

4.2.2. Deployment

Reference pre-class materials: "RabbitMQ Deployment Guide.md"

Cluster deployment
Next, let's see how to install a RabbitMQ cluster.

1. Cluster classification

In the official documentation of RabbitMQ, two cluster configuration methods are described:

  • Normal mode: Normal mode clusters do not perform data synchronization, and each MQ has its own queue and data information (other metadata information such as switches will be synchronized). For example, we have 2 MQs: mq1 and mq2. If your message is in mq1 and you are connected to mq2, then mq2 will go to mq1 to pull the message and return it to you. If mq1 goes down, the message will be lost.
  • Mirror mode: Different from the normal mode, the queue will be synchronized between the mirror nodes of each mq, so you can get the message when you connect to any mirror node. And if a node goes down, no data will be lost. However, this approach increases bandwidth consumption for data synchronization.

Let's first look at the normal mode cluster. Our plan is to deploy a 3-node mq cluster:

CPU name console port amqp communication port
mq1 8081 —> 15672 8071 —> 5672
mq2 8082 —> 15672 8072 —> 5672
mq3 8083 —> 15672 8073 —> 5672

The default labels of nodes in the cluster are: rabbit@[hostname], so the names of the above three nodes are:

  • rabbit@mq1
  • rabbit@mq2
  • rabbit@mq3

2. Get cookies

The bottom layer of RabbitMQ depends on Erlang, and the Erlang virtual machine is a distributed-oriented language that supports cluster mode by default. Each RabbitMQ node in cluster mode uses a cookie to determine if they are allowed to communicate with each other.

For two nodes to be able to communicate, they must have the same shared secret, called an Erlang cookie . A cookie is just a string of alphanumeric characters up to 255 characters.

Every cluster node must have the same cookie . It is also needed between instances to communicate with each other.

We first get a cookie value in the previously started mq container as the cluster cookie. Execute the following command:

docker exec -it mq cat /var/lib/rabbitmq/.erlang.cookie

You can see the cookie value as follows:

FXZMCVGLBIXZCDEMMVZQ

Next, stop and delete the current mq container, and we rebuild the cluster.

docker rm -f mq

insert image description here
clean data volume

docker volume prune

3. Prepare the cluster configuration

Create a new configuration file rabbitmq.conf in the /tmp directory:

cd /tmp
# 创建文件
touch rabbitmq.conf

The content of the file is as follows:

loopback_users.guest = false
listeners.tcp.default = 5672
cluster_formation.peer_discovery_backend = rabbit_peer_discovery_classic_config
cluster_formation.classic_config.nodes.1 = rabbit@mq1
cluster_formation.classic_config.nodes.2 = rabbit@mq2
cluster_formation.classic_config.nodes.3 = rabbit@mq3

Create another file to record cookies

cd /tmp
# 创建cookie文件
touch .erlang.cookie
# 写入cookie
echo "FXZMCVGLBIXZCDEMMVZQ" > .erlang.cookie
# 修改cookie文件的权限
chmod 600 .erlang.cookie

Prepare three directories, mq1, mq2, mq3:

cd /tmp
# 创建目录
mkdir mq1 mq2 mq3

Then copy rabbitmq.conf and cookie files to mq1, mq2, mq3:

# 进入/tmp
cd /tmp
# 拷贝
cp rabbitmq.conf mq1
cp rabbitmq.conf mq2
cp rabbitmq.conf mq3
cp .erlang.cookie mq1
cp .erlang.cookie mq2
cp .erlang.cookie mq3

4. Start the cluster

Create a network:

docker network create mq-net

docker volume create

run command

docker run -d --net mq-net \
-v ${PWD}/mq1/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=itcast \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq1 \
--hostname mq1 \
-p 8071:5672 \
-p 8081:15672 \
rabbitmq:3.8-management
docker run -d --net mq-net \
-v ${PWD}/mq2/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=itcast \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq2 \
--hostname mq2 \
-p 8072:5672 \
-p 8082:15672 \
rabbitmq:3.8-management
docker run -d --net mq-net \
-v ${PWD}/mq3/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=itcast \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq3 \
--hostname mq3 \
-p 8073:5672 \
-p 8083:15672 \
rabbitmq:3.8-management

5. Test

Add a queue to the mq1 node:
insert image description here

As shown in the figure, it can also be seen in both mq2 and mq3 consoles:
insert image description here

6. Data sharing test

Click on this queue to enter the management page:
insert image description here

Then use the console to send a message to this queue:
insert image description here

As a result, this message can be seen on mq2 and mq3:

insert image description here

7. Usability testing

We let one of the nodes mq1 go down:

docker stop mq1

Then log in to the console of mq2 or mq3, and find that simple.queue is no longer available:
insert image description here

It means that the data is not copied to mq2 and mq3.

4.3. Mirror cluster

4.3.1. Cluster structure and characteristics

Mirror cluster: the essence is the master-slave mode, with the following characteristics:

  • Switches, queues, and messages in queues will be backed up synchronously between mirror nodes of each mq.
  • The node that creates the queue is called the primary node of the queue, and the other nodes that are backed up to are called the mirror nodes of the queue.
  • A queue's master node may be another queue's mirror node
  • All operations are completed by the master node, and then synchronized to the mirror node
  • After the master goes down, the mirror node will be replaced by the new master

The structure is shown in the figure:
insert image description here

4.3.2. Deployment

Reference pre-class materials: "RabbitMQ Deployment Guide.md"
mirror mode

In the case just now, once the host that created the queue goes down, the queue will be unavailable. Does not have high availability capabilities. If you want to solve this problem, you must use the official mirror cluster solution.

Official document address: https://www.rabbitmq.com/ha.html

1. Features of Mirror Mode

By default, queues are only persisted on the node that created the queue. In the mirror mode, the node that creates the queue is called the master node of the queue , and the queue is also copied to other nodes in the cluster, also called the mirror node of the queue.

However, different queues can be created on any node in the cluster, so different queues can have different master nodes. Even, the master node of one queue may be the mirror node of another queue .

All requests sent by users to the queue, such as sending messages and message receipts, will be completed on the master node by default. If the request is received from the slave node, it will also be routed to the master node for completion. The mirror node only plays the role of backing up data .

When the master node receives the consumer's ACK, all mirrors delete the data in the node.

Summarized as follows:

  • The mirror queue structure is one master and multiple slaves (slave is mirror image)
  • All operations are completed by the master node, and then synchronized to the mirror node
  • After the master goes down, the mirror node will replace it as the new master (if the master is down before the master-slave synchronization is completed, data loss may occur)
  • Does not have load balancing function, because all operations will be completed by the master node (but different queues, the master node can be different, you can use this to improve throughput)

2. Mirror mode configuration

There are 3 modes for mirror mode configuration:

ha-mode ha-params Effect
exact mode exactly The number of copies of the queue count The number of queue replicas (sum of primary and mirror servers) in the cluster. If count is 1, it means a single copy: the queue master node. A count value of 2 means 2 copies: 1 queue master and 1 queue mirror. In other words: count = number of mirrors + 1. If there are fewer than count nodes in the cluster, the queue will be mirrored to all nodes. If there is a cluster total greater than count+1, and the node containing the mirror fails, a new mirror will be created on another node.
all (none) Queues are mirrored across all nodes in the cluster. The queue will be mirrored to any newly joined nodes. Mirroring to all nodes will put additional pressure on all cluster nodes, including network I/O, disk I/O, and disk space usage. It is recommended to use exactly, and set the number of replicas to (N / 2 +1).
nodes node names Specify which nodes the queue is created to. If none of the specified nodes exist, an exception will occur. If the specified node exists in the cluster but is temporarily unavailable, a node will be created to the node the current client is connected to.

Here we use the rabbitmqctl command as an example to explain the configuration syntax.

Syntax example:

3.exactly mode

rabbitmqctl set_policy ha-two "^two\." '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'
  • rabbitmqctl set_policy: Fixed wording
  • ha-two: policy name, custom
  • "^two\.": Match the regular expression of the queue, the queue that conforms to the naming rules will take effect, here is any two.queue name starting with
  • '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}': Policy content
    • "ha-mode":"exactly": strategy mode, here is exactly mode, specify the number of copies
    • "ha-params":2: Policy parameter, here is 2, that is, the number of replicas is 2, 1 master and 1 mirror
    • "ha-sync-mode":"automatic": Synchronization strategy, the default is manual, that is, newly added mirror nodes will not synchronize old messages. If it is set to automatic, the newly added mirror node will synchronize all the messages in the master node, which will bring additional network overhead

4. Mode

rabbitmqctl set_policy ha-all "^all\." '{"ha-mode":"all"}'
  • ha-all: policy name, custom
  • "^all\.": matches all all.queue names starting with
  • '{"ha-mode":"all"}': policy content
    • "ha-mode":"all": Strategy mode, here is all mode, that is, all nodes will be called mirror nodes

5.nodes mode

rabbitmqctl set_policy ha-nodes "^nodes\." '{"ha-mode":"nodes","ha-params":["rabbit@nodeA", "rabbit@nodeB"]}'
  • rabbitmqctl set_policy: Fixed wording
  • ha-nodes: policy name, custom
  • "^nodes\.": Match the regular expression of the queue, the queue that conforms to the naming rules will take effect, here is any nodes.queue name starting with
  • '{"ha-mode":"nodes","ha-params":["rabbit@nodeA", "rabbit@nodeB"]}': Policy content
    • "ha-mode":"nodes": Strategy mode, here is the nodes mode
    • "ha-params":["rabbit@mq1", "rabbit@mq2"]: Policy parameter, here specify the name of the node where the copy is located

6. Test

We use mirroring in exactly mode, because the number of cluster nodes is 3, so the number of mirroring is set to 2.

Run the command below:

docker exec -it mq1 rabbitmqctl set_policy ha-two "^two\." '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'

Next, we create a new queue:

insert image description here

Check the queue on any mq console:
insert image description here

7. Test data sharing

Send a message to two.queue:
insert image description here

Then check the message in any console of mq1, mq2, mq3:

insert image description here

8. Test high availability

Now, we let the master node mq1 of two.queue go down:

docker stop mq1

View cluster status:
insert image description here

View queue status:
insert image description here

Discovery is still healthy! And its master node switched to rabbit@mq2

5. Arbitration queue

Starting from version 3.8 of RabbitMQ, a new quorum queue has been introduced. It has similar functions to the mirror team, but it is more convenient to use.

4.4. Arbitration Queue

4.4.1. Cluster characteristics

Arbitration queue: Arbitration queue is a new function available after version 3.8. It is used to replace the mirror queue and has the following characteristics:

  • Like the mirror queue, it is a master-slave mode and supports master-slave data synchronization
  • Very easy to use, no complicated configuration
  • Master-slave synchronization based on Raft protocol, strong consistency

4.4.2. Deployment

Reference pre-class materials: "RabbitMQ Deployment Guide.md"

Arbitration Queue
Starting from RabbitMQ 3.8, a new arbitration queue has been introduced. It has similar functions to the mirrored queue, but it is more convenient to use.

1. Add quorum queue

To add a queue in any console, be sure to select the queue type as Quorum type.
insert image description here
View queues on any console:
insert image description here

As you can see, the words + 2 of the quorum queue. It means that this queue has 2 mirror nodes.

Because the default mirror number of the quorum queue is 5. If your cluster has 7 nodes, then the number of mirrors must be 5; and our cluster has only 3 nodes, so the number of mirrors is 3.

2. Test

You can refer to the test on the mirrored cluster, the effect is the same.

3. Cluster expansion

4. Join the cluster

1) Start a new MQ container:

docker run -d --net mq-net \
-v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=itcast \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq4 \
--hostname mq5 \
-p 8074:15672 \
-p 8084:15672 \
rabbitmq:3.8-management

2) Enter the container console:

docker exec -it mq4 bash

3) Stop the mq process

rabbitmqctl stop_app

4) Reset the data in RabbitMQ:

rabbitmqctl reset

5) Join mq1:

rabbitmqctl join_cluster rabbit@mq1

6) Start the mq process again

rabbitmqctl start_app

insert image description here

5. Increase the copy of the arbitration queue

Let's first check the current copy of the quorum.queue queue and enter the mq1 container:

docker exec -it mq1 bash

Excuting an order:

rabbitmq-queues quorum_status "quorum.queue"

Result:
insert image description here
Now, let's add mq4:

rabbitmq-queues add_member "quorum.queue" "rabbit@mq4"

result:
insert image description here

View again:

rabbitmq-queues quorum_status "quorum.queue"

insert image description here
Check the console and find that the number of mirror images of quorum.queue has also changed from +2 to +3:
insert image description here

4.4.3. Java code to create an arbitration queue

@Bean
public Queue quorumQueue() {
    
    
    return QueueBuilder
        .durable("quorum.queue") // 持久化
        .quorum() // 仲裁队列
        .build();
}

4.4.4. SpringAMQP connects to MQ cluster

Note that address is used here instead of host and port

spring:
  rabbitmq:
    addresses: 192.168.150.105:8071, 192.168.150.105:8072, 192.168.150.105:8073
    username: itcast
    password: 123321
    virtual-host: /

Guess you like

Origin blog.csdn.net/sinat_38316216/article/details/129840089