下面对RabbitMQ可能发生消息丢失的场景进行说明。
一、生产者发送消息到MQ失败
在生产者发送消息时,可能由于网络等其他原因导致消息没有到MQ中,但是生产者又不知道消息到底到达没有,这就会造成消息的丢失,解决这个问题可以使用事务机制和发送方确认机制,由于事务机制过于耗费性能所以一般不用,这里主要说说发送方确认机制。这个机制就是消息发送到MQ后,MQ会回一个确认收到的消息给我们,这个消息回调又分两个,ConfirmCallback和ReturnCallback.
1.ConfirmCallback:当消息成功到达exchange的时候触发的回调,但是他不能保证消息投递到目标 queue 里。
2.ReturnCallback:当消息成功到达exchange,但是没有队列与之绑定的时候触发的回调。
下面将演示这两个回调的使用。
进行基本配置
下面这段是配置Queue和DirectExchange,并且进行绑定,这里想说一下ConfirmCallback接口下的confirm方法参数,好吧,其实重点想说CorrelationData对象,我们在发送消息的时候需要配置一个CorrelationData对象,CorrelationData对象内部有 id 属性,用来表示当前消息唯一性,如果不去指定的话,会传null,回调时候confirm中的CorrelationData对象就是我们发送时候的对象,所以,在一些场景下如果用消息id做逻辑的话,需要在发送的时候指定。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMqConfig {
private Logger logger = LoggerFactory.getLogger(RabbitMqConfig.class);
@Bean
public Queue queue() {
return new Queue("TestDirectQueue", true);
}
@Bean
public DirectExchange directExchange() {
return new DirectExchange("TestDirectExchange", true, false);
}
@Bean
Binding bindingDirect() {
return BindingBuilder
.bind(queue())
.to(directExchange())
.with("TestDirectRouting");
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
if (ack) {
logger.info("{}发送{}",correlationData.getId(),ack?"成功":"失败");
}
}
});
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message,
int replyCode,
String replyText,
String exchange,
String routingKey) {
logger.info("returnedMessage回调消息={},错误码={},回复文字={},exchange={},routingKey={}",message,replyCode,replyText,exchange,routingKey);
}
});
return rabbitTemplate;
}
}
注意最后两个,否则接收不到回调。
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.publisher-confirm-type=correlated
spring.rabbitmq.publisher-returns=true
发送测试
这里使用UUID生成一个id,发送的时候发Map对象。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@CrossOrigin()
public class TestController {
private Logger log = LoggerFactory.getLogger(TestController.class);
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("send")
public String test(){
Map<String,String> map =new HashMap<>();
map.put("data","data");
map.put("id", UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("TestDirectExchange","TestDirectRouting",map,new CorrelationData(map.get("id")));
return "OK";
}
}
接收端
@Configuration
public class RabbitMQListener {
private Logger logger = LoggerFactory.getLogger(RabbitMQListener.class);
@RabbitListener(queues="TestDirectQueue")
public void process(@Payload Map<String,String> map) {
logger.info("收到消息{}",map);
}
}
测试
如果此时进行解绑后在发送,就会收到ReturnCallback回调。
但是这个和rabbitTemplate.setMandatory(true);
有关,如果不设置为true,ReturnCallback是不进行回调的,另外ReturnCallback也比ConfirmCallback先回调,即使ReturnCallback回调了找不到队列,那么ConfirmCallback中ack还是为true,这里要注意。
二、消息入队之后MQ宕机
这种情况就是MQ突然宕机了或者被关闭,所以就必须要对消息做持久化,当MQ重新启动之后消息还能重新恢复过来,当然还要对队列、交换器进行持久化。
我们在声明队列和交换器的时候,参数durable就是代表是否持久化,为true代表持久化,否则在RabbitMq重启后将丢失这些信息。
还有autoDelete 参数:当所有消费客户端连接断开后,是否自动删除队列,一般设置成false就行。
@Bean
public Queue queue() {
return new Queue("TestDirectQueue", true);
}
@Bean
public DirectExchange directExchange() {
return new DirectExchange("TestDirectExchange", true, false);
}
// 参数1 name :队列名
// 参数2 durable :是否持久化
// 参数3 exclusive :仅创建者可以使用的私有队列,断开后自动删除
// 参数4 autoDelete : 当所有消费客户端连接断开后,是否自动删除队列
new Queue(name, durable, exclusive, autoDelete);
// 参数1 name :交换器名
// 参数2 durable :是否持久化
// 参数3 autoDelete :当所有消费客户端连接断开后,是否自动删除队列
new DirectExchange(name, durable, autoDelete)
发送的消息默认就是持久化消息,当Rabbitmq中还有数据时候关闭他,在重启后依然存在。
三、消费者消费
最后就是消费者端的问题了,解决这个问题就是消费者进行消息确认,开启的配置如下:
spring.rabbitmq.listener.simple.acknowledge-mode=manual
他还有其他的模式:
AcknowledgeMode.NONE:不确认
AcknowledgeMode.AUTO:自动确认
AcknowledgeMode.MANUAL:手动确认
开启后,如果我们不手动确认消息,可能会重复的收到同样的消息,这就需要我们做系统幂等性。
channel.basicAck
的作用就是告诉RabbitMq服务器我收到这条消息,已经被我消费了,可以在队列删掉了, 以后就不会再发了,参数deliveryTag代表了消息的唯一标识ID, 是一个单调递增的正整数,也可以通过message.getMessageProperties().getDeliveryTag()获取,multiple表示是否批量,true会一次性ack所有小于deliveryTag的消息。
@RabbitListener(queues="TestDirectQueue")
public void process(Message message, @Payload Map<String,String> map,
@Header(AmqpHeaders.DELIVERY_TAG) long tag,
Channel channel) throws IOException {
logger.info("收到消息{}",map);
channel.basicAck(tag,false);
}