¿Cómo garantiza RabbitMQ la entrega confiable de mensajes?

Spring Boot integra RabbitMQ

Spring tiene tres métodos de configuración

  1. Basado en XML
  2. Basado en JavaConfig
  3. Basado en anotaciones

Por supuesto, XML rara vez se usa para la configuración ahora, solo introduzca el método de configuración usando JavaConfig y anotaciones

RabbitMQ integra Spring Boot, solo necesitamos agregar el arrancador correspondiente

 <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-amqp</artifactId>
 </dependency>

Basado en anotaciones

La configuración en application.yaml es la siguiente

spring:
  rabbitmq:
    host: myhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /

log:
  exchange: log.exchange
  info:
    queue: info.log.queue
    binding-key: info.log.key
  error:
    queue: error.log.queue
    binding-key: error.log.key
  all:
    queue: all.log.queue
    binding-key: '*.log.key'

El código de consumidor es el siguiente

@Slf4j
@Component
public class LogReceiverListener {

    /**
     * 接收info级别的日志
     */
    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(value = "${log.info.queue}", durable = "true"),
                    exchange = @Exchange(value = "${log.exchange}", type = ExchangeTypes.TOPIC),
                    key = "${log.info.binding-key}"
            )
    )
    public void infoLog(Message message) {
        String msg = new String(message.getBody());
        log.info("infoLogQueue 收到的消息为: {}", msg);
    }

    /**
     * 接收所有的日志
     */
    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(value = "${log.all.queue}", durable = "true"),
                    exchange = @Exchange(value = "${log.exchange}", type = ExchangeTypes.TOPIC),
                    key = "${log.all.binding-key}"
            )
    )
    public void allLog(Message message) {
        String msg = new String(message.getBody());
        log.info("allLogQueue 收到的消息为: {}", msg);
    }
}

Los productores son los siguientes

@RunWith(SpringRunner.class)
@SpringBootTest
public class MsgProducerTest {

    @Autowired
    private AmqpTemplate amqpTemplate;
    @Value("${log.exchange}")
    private String exchange;
    @Value("${log.info.binding-key}")
    private String routingKey;

    @SneakyThrows
    @Test
    public void sendMsg() {
        for (int i = 0; i < 5; i++) {
            String message = "this is info message " + i;
            amqpTemplate.convertAndSend(exchange, routingKey, message);
        }

        System.in.read();
    }
}

El enfoque de Spring Boot para message ack es un poco diferente de la forma en que la API nativa apunta a message ack.

Método de ack de mensaje de API nativo

Hay 2 formas de confirmar el mensaje

Confirmación automática (autoAck = true)
Confirmación manual (autoAck = false)

Cuando los consumidores consumen mensajes, pueden especificar el parámetro autoAck

String basicConsume (cola de cadenas, autoAck booleano, devolución de llamada del consumidor)

autoAck = false: RabbitMQ esperará a que el consumidor muestre un mensaje de confirmación de respuesta antes de eliminar el mensaje de la memoria (o disco)

autoAck = true: RabbitMQ establecerá automáticamente el mensaje enviado como confirmación y luego lo eliminará de la memoria (o del disco), independientemente de si el consumidor realmente consume estos mensajes

El método de confirmación manual es el siguiente, hay 2 parámetros

basicAck (etiqueta de entrega larga, múltiplo booleano)

deliveryTag: se utiliza para identificar el mensaje entregado en el canal. Cuando RabbitMQ envía un mensaje al consumidor, adjuntará una etiqueta de entrega para que el consumidor pueda decirle a RabbitMQ qué mensaje se ha confirmado cuando se confirma el mensaje.
RabbitMQ garantiza que en cada canal, el deliveryTag de cada mensaje aumenta de 1

multiple = true: los mensajes con ID de mensaje <= deliveryTag serán todos confirmados

myltiple = false: los mensajes con id de mensaje = deliveryTag serán confirmados

¿Qué pasa si no se confirma el mensaje?

Si el mensaje en la cola se envía al consumidor y el consumidor no confirma el mensaje, el mensaje permanecerá en la cola hasta que se confirme.
Si el mensaje enviado al consumidor A no ha sido confirmado, rabbitmq considerará volver a enviar el mensaje no confirmado del consumidor A a otro consumidor solo cuando se interrumpa la conexión entre el consumidor A y rabbitmq.

La forma de recibir mensajes en Spring Boot

Hay tres formas, definidas en la clase de enumeración AcknowledgeMode

¿Cómo garantiza RabbitMQ la entrega confiable de mensajes?

El modo de confirmación predeterminado de Spring Boot para mensajes es AUTO.

En escenarios reales, generalmente acordamos manualmente.

La configuración de application.yaml se cambia a la siguiente

spring:
  rabbitmq:
    host: myhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual # 手动ack,默认为auto

El código de consumidor correspondiente se cambia a

@Slf4j
@Component
public class LogListenerManual {

    /**
     * 接收info级别的日志
     */
    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(value = "${log.info.queue}", durable = "true"),
                    exchange = @Exchange(value = "${log.exchange}", type = ExchangeTypes.TOPIC),
                    key = "${log.info.binding-key}"
            )
    )
    public void infoLog(Message message, Channel channel) throws Exception {
        String msg = new String(message.getBody());
        log.info("infoLogQueue 收到的消息为: {}", msg);
        try {
            // 这里写各种业务逻辑
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }
    }
}

Las anotaciones que usamos anteriormente son las siguientes

¿Cómo garantiza RabbitMQ la entrega confiable de mensajes?

Basado en JavaConfig

Dado que el uso de anotaciones es tan conveniente, ¿por qué necesita JavaConfig?
JavaConfig es conveniente para personalizar varios atributos, como configurar varios hosts virtuales al mismo tiempo, etc.

Consulte GitHub para obtener un código específico

¿Cómo asegura RabbitMQ la entrega confiable de mensajes?

Un mensaje a menudo pasa por las siguientes etapas

[Error al cargar la imagen ... (image-555f54-1603419542750)]

Inserte la descripción de la imagen aquí

Por lo tanto, para garantizar la entrega confiable de mensajes, solo necesita garantizar la entrega confiable de estas tres etapas.

Etapa de producción

La entrega confiable en esta etapa se basa principalmente en ConfirmListener (confirmación del editor) y ReturnListener (notificación de falla).
Como se mencionó anteriormente, el flujo de un mensaje en RabbitMQ es
productor -> cluster de broker rabbitmq -> intercambio -> cola -> consumidor

ConfirmListener puede obtener si el mensaje se envía desde el productor al corredor
ReturnListener puede obtener el mensaje que no se enruta a la cola desde el intercambio

Utilizo la API de Spring Boot Starter para demostrar el efecto

application.yaml

spring:
  rabbitmq:
    host: myhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual # 手动ack,默认为auto

log:
  exchange: log.exchange
  info:
    queue: info.log.queue
    binding-key: info.log.key

El editor confirma la devolución de llamada

@Component
public class ConfirmCallback implements RabbitTemplate.ConfirmCallback {

    @Autowired
    private MessageSender messageSender;

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String msgId = correlationData.getId();
        String msg = messageSender.dequeueUnAckMsg(msgId);
        if (ack) {
            System.out.println(String.format("消息 {%s} 成功发送给mq", msg));
        } else {
            // 可以加一些重试的逻辑
            System.out.println(String.format("消息 {%s} 发送mq失败", msg));
        }
    }
}

Devolución de llamada de notificación de falla

@Component
public class ReturnCallback implements RabbitTemplate.ReturnCallback {

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        String msg = new String(message.getBody());
        System.out.println(String.format("消息 {%s} 不能被正确路由,routingKey为 {%s}", msg, routingKey));
    }
}


@Configuration
public class RabbitMqConfig {

    @Bean
    public ConnectionFactory connectionFactory(
            @Value("${spring.rabbitmq.host}") String host,
            @Value("${spring.rabbitmq.port}") int port,
            @Value("${spring.rabbitmq.username}") String username,
            @Value("${spring.rabbitmq.password}") String password,
            @Value("${spring.rabbitmq.virtual-host}") String vhost) {
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host);
        connectionFactory.setPort(port);
        connectionFactory.setUsername(username);
        connectionFactory.setPassword(password);
        connectionFactory.setVirtualHost(vhost);
        connectionFactory.setPublisherConfirms(true);
        connectionFactory.setPublisherReturns(true);
        return connectionFactory;
    }

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory,
                                         ReturnCallback returnCallback, ConfirmCallback confirmCallback) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setReturnCallback(returnCallback);
        rabbitTemplate.setConfirmCallback(confirmCallback);
        // 要想使 returnCallback 生效,必须设置为true
        rabbitTemplate.setMandatory(true);
        return rabbitTemplate;
    }
}

Aquí he hecho un paquete de RabbitTemplate, lo principal es agregar la identificación del mensaje al enviar y guardar la correspondencia entre la identificación del mensaje y el mensaje, porque RabbitTemplate.ConfirmCallback solo puede obtener la identificación del mensaje, pero no el contenido del mensaje, por lo que necesitamos nuestra propia relación Save this mapping. En algunos sistemas con requisitos de alta confiabilidad, puede almacenar esta relación de mapeo en la base de datos, enviarla correctamente para eliminar la relación de mapeo y enviarla todo el tiempo si falla.

@Component
public class MessageSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public final Map<String, String> unAckMsgQueue = new ConcurrentHashMap<>();

    public void convertAndSend(String exchange, String routingKey, String message) {
        String msgId = UUID.randomUUID().toString();
        CorrelationData correlationData = new CorrelationData();
        correlationData.setId(msgId);
        rabbitTemplate.convertAndSend(exchange, routingKey, message, correlationData);
        unAckMsgQueue.put(msgId, message);
    }

    public String dequeueUnAckMsg(String msgId) {
        return unAckMsgQueue.remove(msgId);
    }

}

El código de prueba es

@RunWith(SpringRunner.class)
@SpringBootTest
public class MsgProducerTest {

    @Autowired
    private MessageSender messageSender;
    @Value("${log.exchange}")
    private String exchange;
    @Value("${log.info.binding-key}")
    private String routingKey;

    /**
     * 测试失败通知
     */
    @SneakyThrows
    @Test
    public void sendErrorMsg() {
        for (int i = 0; i < 3; i++) {
            String message = "this is error message " + i;
            messageSender.convertAndSend(exchange, "test", message);
        }
        System.in.read();
    }

    /**
     * 测试发布者确认
     */
    @SneakyThrows
    @Test
    public void sendInfoMsg() {
        for (int i = 0; i < 3; i++) {
            String message = "this is info message " + i;
            messageSender.convertAndSend(exchange, routingKey, message);
        }
        System.in.read();
    }
}

Primero venga a la notificación de falla de la prueba

La salida es

消息 {this is error message 0} 不能被正确路由,routingKey为 {test}
消息 {this is error message 0} 成功发送给mq
消息 {this is error message 2} 不能被正确路由,routingKey为 {test}
消息 {this is error message 2} 成功发送给mq
消息 {this is error message 1} 不能被正确路由,routingKey为 {test}
消息 {this is error message 1} 成功发送给mq

Los mensajes se envían correctamente al corredor, pero no se enrutan a la cola.

Probemos la confirmación del editor

La salida es

消息 {this is info message 0} 成功发送给mq
infoLogQueue 收到的消息为: {this is info message 0}
infoLogQueue 收到的消息为: {this is info message 1}
消息 {this is info message 1} 成功发送给mq
infoLogQueue 收到的消息为: {this is info message 2}
消息 {this is info message 2} 成功发送给mq

Los mensajes se envían correctamente al corredor y también se enrutan correctamente a la cola

Fase de almacenamiento

No he estudiado la alta disponibilidad en esta etapa. Después de todo, el clúster se construye por operación y mantenimiento. Si tengo tiempo, agregaré este contenido rápido.

Etapa de consumo

La entrega confiable en la etapa de consumo está garantizada principalmente por ack.
El artículo anterior presentó el método ack de API nativo y el método ack del marco de Spring Boot.

En general, en el entorno de producción, generalmente usamos un solo reconocimiento manual . Después de que falla el consumo, no volveremos a ingresar a la cola (porque hay una alta probabilidad de que vuelva a fallar), sino que reenviaremos el mensaje a la cola de mensajes no entregados para facilitar la resolución de problemas en el futuro.

Resume las diversas situaciones

  1. El mensaje se elimina del corredor después de recibir una confirmación.
  2. Después de nack o rechazar, se divide en las siguientes dos situaciones
    (1) reque = true, el mensaje se volverá a ingresar en la cola (2) reque = fasle, el mensaje se descartará directamente, si hay una cola de mensajes no entregados especificado, se entregará a la cola de mensajes no entregados

Supongo que te gusta

Origin blog.csdn.net/m0_46657043/article/details/109237163
Recomendado
Clasificación