RabbitMQ如何保证消息不丢失呢?
代码地址:https://gitee.com/webprogram/springboot_rabbit
在使用RabbitMQ的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败的场景,RabbitMQ为我们提供了两种方式来控制消息的可靠性
- confirm确认模式,开机交换机应答
- return退回模式,开启队列应答
RabbitMQ从生产到消费链路如下:
producer-> rabbitmq broker->exchange->queue->consumer
- 消息从生产者到exchange会回调confirmCallback这样一个回调函数,来给生产一个应答,是否放入成功,该方法无论消息放入成功或失败都会执行
- 消息从exchange到queue,投递失败则会返回一个returnCallback函数,注意是失败时才会回调
利用上述两个机制则可以确保消息能成功放入队列
代码示例
confirm确认模式
-
开启确认模式:connetionFactory开启
@Bean public ConnectionFactory connectionFactory() { CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); connectionFactory.setAddresses(host); connectionFactory.setPort(port); connectionFactory.setUsername(username); connectionFactory.setPassword(password); connectionFactory.setVirtualHost(virtualHost); // 开启确认模式即交换机应答 // 已弃用 // connectionFactory.setPublisherConfirms(true); // 开启交换机应答 setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED) // 开启回退模式即队列应答 connectionFactory.setPublisherReturns(true); return connectionFactory; }
在基于springboot2.5.4测试时,setPublisherConfirms(true)方法已废弃,改用setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED)方法
上述Bean为自己手动配置,也可以直接在application.yml配置,效果等效
spring: rabbitmq: host: 127.0.0.1 port: 5672 username: admin password: admin virtual-host: my_vhost publisher-confirm-type: correlated #开启交换机应答 publisher-returns: true #开启队列应答
在实际测试中,单独只开启队列应答时,交换应答也开启了(原因不详,个人推测因为消息是先通过交换机,然后再到队列,如果开启队列应答,那势必在队列之前的交换机应答也应该开启才能确保消息的可靠性传递),而开启交换机应答时,队列应答没开启
-
rabbitTemplate定义confirmCallbakc回调方法
private RabbitTemplate rabbitTemplate; @Autowired public void setRabbitTemplate(RabbitTemplate rabbitTemplate) { this.rabbitTemplate = rabbitTemplate; // 定义回调函数 rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { /** * 参数说明: * correlationData:配置信息,在发送消息时设置的 * ack:交换机是否成功收到消息,true为收到,false为没收到 * cause: 错误原因 */ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { System.out.println("我是回调函数"); if(ack){ System.out.println("消息成功放入exchange"); }else{ System.out.println("消息发送失败:" + cause); // 重新发送。。。。 } } }); }
3.发送消息测试,控制台输出如下:
成功时:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-usgOXN5y-1632540624584)(C:\Users\cxu\AppData\Roaming\Typora\typora-user-images\image-20210923084140405.png)]
将消息发送的交换机改成一个不存在的,失败时:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0UkCJ8pM-1632540624586)(C:\Users\cxu\AppData\Roaming\Typora\typora-user-images\image-20210923085133065.png)]
打印出错误信息:no exchange ‘topic_exchange1’ in vhost
return回退模式
-
开启回退模式 connectionFactory.setPublisherReturns(true)或者配置文件publisher-returns: true
-
定义回调函数
//定义return回调函数 rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback(){ // 该方法执行就代表未成功放入queue /** * 参数说明 * returnedMessage:消息放入失败时,信息集合实体类,里面包含: * message:发送的消息对象 * replyCode:错误码 * replyText:错误信息 * exchange:交换机 * proutingKey:路由key */ @Override public void returnedMessage(ReturnedMessage returnedMessage) { System.out.println("消息放入失败"); System.out.println("原因:" + returnedMessage.getReplyCode() + "-" + returnedMessage.getReplyText()); } });
-
设置exchange处理消息的模式
- 如果消息没路由到queue,则丢弃消息(默认)
- 如果消息没路由到queue,返回信息给消息发送方即ReturnCallback
//设置交换机处理失败消息的模式,失败时执行回调 rabbitTemplate.setMandatory(true); //或者配置文件 spring.rabbitmq.template.mandatory = true
该配置作用:Enable mandatory messages. If a mandatory message cannot be routed to a queue by the server, it will return an unroutable message with a Return method
大意为:开启强制消息投递(
mandatory
为设置为true
),但消息未被路由至任何一个queue
,则回退一条消息到RabbitTemplate.ReturnCallback
中的returnedMessage
方法springboot这步可以省略(建议还是加上),具体看如下源码:
// RabbitTemplateConfigurer中configure()中,这句代码会为属性赋值 template.setMandatory(determineMandatoryFlag()); private boolean determineMandatoryFlag() { Boolean mandatory = this.rabbitProperties.getTemplate().getMandatory(); return (mandatory != null) ? mandatory : this.rabbitProperties.isPublisherReturns(); }
如果设置了
mandatory
参数,则直接取值;如若mandatory
参数为空,则取之于否起开了队列应答发送消息测试,成功时,控制台无输出,设置一个不合规的RoutingKey发送时,未成功放入队列,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZsGms1el-1632540624588)(C:\Users\cxu\AppData\Roaming\Typora\typora-user-images\image-20210923094132277.png)]
No_ROUTE 无该路由对应的队列
消费者ACK
ack指Acknowledge,确认,表示消费端接收到消息后的确认方式
提供三种确认方式:
- 自动确认,acknowledge=“none”
- 手动确认,acknowledge=“manual”
- 根据异常情况确认,acknowledge=“auto”
自动确认:当消息被消费者接收到,则自动确认收到,并将消息从队列中移除,在实际生产中可能在接收到消息后处理异常,那么就会造成消息丢失
手动确认:业务处理成功后,调用channel.basicAck()手动签收,如出现异常,则调用channel.basicNack()或channel.basicReject()让消息重入队列
代码示例
- 开启手动签收
// 方式一:在配置队列监听时配置ackMode = "MANUAL"
@RabbitListener(queues = "topic_queue",ackMode = "MANUAL")
// 方式二: 配置文件application.yml
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual #手动应答
- 手动签收代码
@RabbitListener(queues = "topic_queue",ackMode = "MANUAL")
public void printMessage(Message message, Channel channel) throws IOException {
// 消息标识
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
// 1. 接收消息
System.out.println(new String(message.getBody()));
// 2.处理业务
//模拟异常
int i = 2 / 0;
// 3.手动签收
/**
* 参数说明:
* deliveryTag:消息唯一标识
* multiple:boolean类型 是否应用多消息 为true时,会同时签收之前的多个消息
*/
channel.basicAck(deliveryTag, true);
}catch (Exception e){
// 发生异常重回队列
/**
* 参数说明:
* deliveryTag:
* multiple:
* requeue:是否重回队列
*/
channel.basicNack(deliveryTag,true,true);
}
}
手动签收涉及关键方法有:channel.basicAck(),channel.basicNack(),channel.basicReject(),channel.basicRecover(),详解链接:https://blog.csdn.net/fly_leopard/article/details/102821776
消息可靠性总结:
- 持久化:Durable=true,包括交换机,队列,消息
- 生产方确认confirm、return
- 消费方确认ack
- Broker高可用