首先说一下消息发送确认模式:
消息确认模式也可以细分为两种:
下面的代码一起实现了两种模式:
spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.template.mandatory=true
如上文所说,消息发送确认模式下有两种不同的子模式,如果想都启用则需要实现两个不同的回调接口:
@Component
public class CallBackSender implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
System.out.println("消息[" + correlationData.getId() + "]成功发送到指定ExChange");
} else {
System.out.println("消息[" + correlationData.getId() + "]发送到ExChange失败:" + cause);
}
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("消息未能成功路由到指定queues");
System.out.println("return--message:" + new String(message.getBody()) + ",replyCode:" + replyCode
+ ",replyText:" + replyText + ",exchange:" + exchange + ",routingKey:" + routingKey);
}
}
在你的RabbitMQConfiguration配置类里,启用配置,同时将刚刚实现的两个回调接口传入,参考代码如下:
@Configuration
public class RabbitMQConfiguration {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Value("${spring.rabbitmq.host}")
private String host;
@Value("${spring.rabbitmq.port}")
private int port;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
@Value("${spring.rabbitmq.publisher-confirms}")
private boolean publisherConfirm;
@Value("${spring.rabbitmq.virtual-host}")
private String virtualHost;
@Value("${spring.rabbitmq.template.mandatory}")
private boolean mandatory;
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host,port);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost(virtualHost);
connectionFactory.setPublisherConfirms(publisherConfirm); // 开启消息确认模式
return connectionFactory;
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
/* 此处设置为prototype是因为一个RabbitTemplate实例只能被设置一次ConfirmCallBack,否则会报错
所以如果想保持singleton类型,则需要保证template不会被重复setConfirmCallBack */
public RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(connectionFactory());
template.setMandatory(mandatory); // 开启消息返回模式
// 将两种不同确认模式的回调接口实例传入
template.setConfirmCallback(new CallBackSender());
template.setReturnCallback(new CallBackSender());
return template;
}
}
这样就算配置完成了,接下来进行测试:
发送的方法具体代码如下(省去controller等):
public void send() {
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
System.out.println("生成消息成功,id:" + correlationData.getId());
try {
this.rabbitTemplate.convertAndSend("exchange1", "test-ack1",
"测试".getBytes("UTF-8"), correlationData);
System.out.println("消息发送到RabbitMQ服务器成功,id:" + correlationData.getId());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
由于我的RabbitMQ服务器中存在名为exchange1的Exchange,但该Exchange没有名为test-ack1的binding。所以此处消息能被发送Exchange,但是无法被路由到指定的queue。而输出的测试信息与情况吻合:
可以看到,消息确实是成功发送到了Exchange,因此ConfirmCallBack在调用confirm方法时传入了ack=true,但是由于指定的routingKey=test-ack1并不存在,所以消息未能成功路由到queue,因此ReturnCallback的returnedMessage方法也被调用。
在RabbitMQ中,消息被Consumer从MQ中获取后,如果没有特殊设置,这条消息会自动被确认消费然后从MQ中删除。
在这种自动确认模式下,如果Consumer在处理某条消息时出现了异常,无法成功处理该条消息,而又由于自动确认模式下,这条消息已经被RabbitMQ从队列删除,所以这条信息最后将无法复原。
想改变这种情况可以设置RabbitMQ为手动确认模式,这样在消息被获取后,如果处理成功,则可以通过
channel.basicAck(long deliveryTag, boolean multiple);
方法来告诉RabbitMQ该条消息已被确认。
如果处理失败,则可以通过
channel.basicNack(long deliveryTag, boolean multiple, boolean requeue);
方法来告诉RabbitMQ处理失败,其中requeue变量是用来告诉RabbitMQ是否将这条信息重新返回到队列中,此处需要注意避免死锁,具体的可以参考这篇文章:https://www.jianshu.com/p/7d1a0f4a9da5?utm_source=oschina-app
实现demo如下:
@Bean
public SimpleMessageListenerContainer messageContainer() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory());
container.setQueueNames("test-ack");
container.setExposeListenerChannel(true);
container.setMaxConcurrentConsumers(1);
container.setConcurrentConsumers(1);
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
container.setMessageListener(new ChannelAwareMessageListener() {
public void onMessage(Message message, Channel channel) throws Exception {
try {
long deliverTag = message.getMessageProperties().getDeliveryTag();
System.out.println(
"消费端接收到消息:" + new String(message.getBody()));
System.out.println("[deliverTag:" + deliverTag + "] " + message.getMessageProperties().getReceivedRoutingKey());
if (deliverTag%2 == 0) {
System.out.println("消息处理失败,重新返回队列");
throw new Exception("数据处理错误");
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
});
return container;
}
在相关配置类中声明SimpleMessageListenerContainer,这是一个默认的实现类,在其中通过
container.setAcknowledgeMode(AcknowledgeMode.MANUAL)
来开启手动确认模式,并且在最后的MessageListener中手动返回ack或者nack。