在RabbitMQ使用过程当中存在一些问题。比如发送消息我们如何确保消息的投递的可靠性呢?如何保证消费消息可靠性呢?
生产者可靠性投递
- 在使用RabbitMQ的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景.RabbitMQ为我们提供了两种方式用来控制消息的投递可靠性模式,RabbitMQ提供了如下两种模式:
- confirm模式: 生产者发送消息到交换机的时机
- return模式: 交换机转发消息给queue的时机
RabbitMQ投递消息的流程如下:
- 生产者发送消息到交换机
- 交换机根据routingKey转发消息给队列
- 消费者监控队列,获取队列中的消息
- 消费成功后,删除队列中的消息
- 消息从product发送到exchange则会返回一个confirmCallback
- 消息从exchange发送到queue则会返回一个returnCallback
ConfirmCallback
配置文件application.yml
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
publisher-confirm-type: correlated
server:
port: 18081
重点关注publisher-confirm-type: correlated,publisher-confirm-type发布确认属性配置.一共有三种确认类型,如下:
/**
* The type of publisher confirms to use.
*/
public enum ConfirmType {
/**
* Use {@code RabbitTemplate#waitForConfirms()} (or {@code waitForConfirmsOrDie()}
* within scoped operations.
*/
SIMPLE,
/**
* Use with {@code CorrelationData} to correlate confirmations with sent
* messsages.
*/
CORRELATED,
/**
* Publisher confirms are disabled (default).
*/
NONE
}
- NONE:禁用发布确认模式,是默认值;
- CORRELATED:发布消息成功到交换器后会触发回调方法;
- SIMPLE:经测试有两种效果
- 效果和CORRELATED值一样会触发回调方法;
- 在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker;
创建队列;交换机和绑定
@Configuration
public class RabbitMQConfig {
/**
* 创建队列
* @return
*/
@Bean
public Queue createqueue(){
return new Queue("queue_demo01");
}
/**
* 创建交换机
* @return
*/
@Bean
public DirectExchange createExchange(){
return new DirectExchange("exchange_direct_demo01");
}
/**
* 创建绑定
* @return
*/
@Bean
public Binding createBinding(){
return BindingBuilder.bind(createqueue()).to(createExchange()).with("demo");
}
}
创建confirm回调函数
@Component
public class RabbitMQConfirm implements RabbitTemplate.ConfirmCallback {
/**
* confirm模式回调函数
*
* @param correlationData 消息信息
* @param ack 确认标记;true:exchange收到消息;false没有收到消息
* @param cause 没有收到消息的原因;如果收到消息输出null
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack){
System.out.println("消息发送交换机成功;原因:"+cause);
return;
}
System.out.println("消息发送交换机失败;原因:"+cause);
}
}
创建controller发送消息
@RestController
public class DemoController {
@Autowired
RabbitMQConfirm rabbitMQConfirm;
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping(value = "/send/{exchange}/{routingKey}/{msg}")
public void send(@PathVariable(value = "exchange") String exchange,
@PathVariable(value = "routingKey") String routingKey,
@PathVariable(value = "msg") String msg) {
// 设置confirm回调函数
rabbitTemplate.setConfirmCallback(rabbitMQConfirm);
rabbitTemplate.convertAndSend(exchange, routingKey, msg);
}
}
测试结果
正确测试: http://localhost:18081/send/exchange_direct_demo01/demo/一条消息
错误测试(传一个不存在的交换机): http://localhost:18081/send/exchange_direct_demo/demo/一条消息
总结
- 生产者可以根据confirm机制来确保消息是否已经发送到交换机
- confirm机制只能保证消息发送到交换机有回调,不能保证消息转发到queue有回调
ReturnCallback
修改配置文件application.yml
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
publisher-confirm-type: correlated
publisher-returns: true
server:
port: 18081
publisher-returns属性为true时,开启ReturnCallback机制;默认为false关闭.
创建return回调函数
@Component
public class RabbitMQReturns implements RabbitTemplate.ReturnCallback {
/**
* return模式回调函数
*
* @param message 消息信息
* @param replyCode 退回的状态码
* @param replyText 退回的信息
* @param exchange 交换机
* @param routingKey 路由键
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("消息:"+message);
System.out.println("退回的状态码:"+replyCode);
System.out.println("退回的原因:"+replyText);
System.out.println("退回的交换机:"+exchange);
System.out.println("退回的routingKey:"+routingKey);
}
}
修改controller发送消息
@RestController
public class DemoController {
@Autowired
RabbitMQConfirm rabbitMQConfirm;
@Autowired
RabbitMQReturns rabbitMQReturns;
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping(value = "/send/{exchange}/{routingKey}/{msg}")
public void send(@PathVariable(value = "exchange") String exchange,
@PathVariable(value = "routingKey") String routingKey,
@PathVariable(value = "msg") String msg) {
// 设置confirm回调函数
rabbitTemplate.setConfirmCallback(rabbitMQConfirm);
// 设置return回调函数
rabbitTemplate.setReturnCallback(rabbitMQReturns);
rabbitTemplate.convertAndSend(exchange, routingKey, msg);
}
}
测试结果
正确测试: http://localhost:18081/send/exchange_direct_demo01/demo/一条消息
错误测试(传一个不存在的routingKey): http://localhost:18081/send/exchange_direct_demo01/demo1/一条消息
总结
- return模式只有再消息从交换机发送到队列出现错误时使用
- 一般情况下不会使用return模式,因为routingKey是由开发人员来指定,一般不会出现错误
消费者确认机制(ACK)
生产者保证可靠性投递,但是在消费者也有可能出现问题,比如没有接受消息,比如接受到消息之后,在代码执行过程中出现了异常,这种情况下我们需要额外的处理,那么就需要手动进行确认签收消息.RabbitMQ给我们提供了一个机制:ACK机制.
配置文件application.yml
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
listener:
simple:
acknowledge-mode: manual
server:
port: 8081
acknowledge-mode: 签收消息的方式.一共有三种方式,如下:
public enum AcknowledgeMode {
/**
* No acks - {@code autoAck=true} in {@code Channel.basicConsume()}.
*/
NONE,
/**
* Manual acks - user must ack/nack via a channel aware listener.
*/
MANUAL,
/**
* Auto - the container will issue the ack/nack based on whether
* the listener returns normally, or throws an exception.
* <p><em>Do not confuse with RabbitMQ {@code autoAck} which is
* represented by {@link #NONE} here</em>.
*/
AUTO;
/**
* Return if transactions are allowed - if the mode is {@link #AUTO} or
* {@link #MANUAL}.
* @return true if transactions are allowed.
*/
public boolean isTransactionAllowed() {
return this == AUTO || this == MANUAL;
}
/**
* Return if the mode is {@link #NONE} (which is called {@code autoAck}
* in RabbitMQ).
* @return true if the mode is {@link #NONE}.
*/
public boolean isAutoAck() {
return this == NONE;
}
/**
* Return true if the mode is {@link #MANUAL}.
* @return true if manual.
*/
public boolean isManual() {
return this == MANUAL;
}
}
- NONE: 自动确认;当消息一旦被消费者接收到,则自动确认收到,并将相应消息从RabbitMQ的消息缓存中移除.但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失.
- MANUAL: 手动确认,需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()等方法,让其按照业务功能进行处理,比如:重新发送,比如拒绝签收进入死信队列等等.
- AUTO: 根据异常情况来确认(暂时不怎么用)
消费者代码
未签收
@Component
public class Listener {
@RabbitListener(queues = "queue_demo01")
public void msg(Message message, Channel channel , String msg) {
System.out.println("队列queue_demo01收到消息:"+msg);
}
}
测试
消息虽然成功消费了,但是一直没有被签收,等到项目重新启动还会再次进行消费.使用手动签收模式时,一定要记得签收消息.
手动签收
@Component
public class Listener {
@RabbitListener(queues = "queue_demo01")
public void msg(Message message, Channel channel, String msg) {
try {
System.out.println("队列queue_demo01收到消息:" + msg);
System.out.println("业务代码执行中....");
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
System.out.println("消息消费失败");
}
}
}
测试
消息已经被成功签收,但是业务代码出现异常时,还是没有做处理
丢弃消息
@Component
public class Listener {
@RabbitListener(queues = "queue_demo01")
public void msg(Message message, Channel channel, String msg) {
try {
System.out.println("队列queue_demo01收到消息:" + msg);
System.out.println("业务代码执行中....");
int i = 1/0;
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
System.out.println("消息消费失败");
try {
// 丢弃消息
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
}
测试
重回消息队列
@Component
public class Listener {
@RabbitListener(queues = "queue_demo01")
public void msg(Message message, Channel channel, String msg) {
try {
System.out.println("队列queue_demo01收到消息:" + msg);
System.out.println("业务代码执行中....");
int i = 1/0;
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
System.out.println("消息消费失败");
try {
// 丢弃消息
//channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
// 重回消息队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
}
测试
重回消息队列就相当于重新发送一次消息,如果是我们的代码有问题,那么这条消息就会一直进行重发
总结
业务代码出现异常时,可以将消息进行丢弃,也可以重回消息队列,也可以拒签进入死信队列等等,具体怎么做根据业务场景来.
写到最后
RabbitMQ如何保证消息高可靠性传输?
- 持久化,exchange要持久化,queue要持久化,message要持久化
- 生产者确认Confirm,Return
- 消费者手动签收消息,以及消费者代码出现异常的处理(根据业务场景来)