Spring Boot integra RabbitMQ
Spring tiene tres métodos de configuración
- Basado en XML
- Basado en JavaConfig
- 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
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
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
- El mensaje se elimina del corredor después de recibir una confirmación.
- 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