RabbitMQ 发布确认
1.1 发布确认原理
生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消息都已经得到了处理。
confirm模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息。
1.2 发布确认策略
1.2.1 开启发布确认方法
发布确认默认是没有开启的,如果要开启需要调用方法confirmSelect,每当你要想使用发布确认,都需要在channel上调用该方法
1.2.2 单个确认发布
这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。
这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。
public static void individualConfirmation() throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMQUtils.getChannel();
//队列声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
channel.confirmSelect();
long begin = System.currentTimeMillis();
for (int i = 0; i < COUNT; i++) {
String message = i + "";
channel.basicPublish("",queueName,null,message.getBytes(StandardCharsets.UTF_8));
//单个消息马上立即确认
boolean b = channel.waitForConfirms();
if (b){
System.out.println("消息发送确认完毕.....");
}
}
long end = System.currentTimeMillis();
System.out.println("单个确认耗时时间:"+(end-begin)+"ms");
}
1.2.3 批量确认发布
上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
public static void batchConfirmation() throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//队列声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
channel.confirmSelect();
//开始时间
long begin = System.currentTimeMillis();
//批量确认消息大小
int batchSize = 100;
for (int i = 0; i < COUNT; i++) {
String message = i + "";
channel.basicPublish("",queueName,null,message.getBytes(StandardCharsets.UTF_8));
//判断达到100条间隔时,进行确认
if (i % batchSize == 0){
//发布确认
channel.waitForConfirms();
}
}
//结束时间
long end = System.currentTimeMillis();
System.out.println("批量确认耗时时间:"+(end-begin)+"ms");
}
1.2.4 异步确认发布
异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功,下面就让我们来详细讲解异步确认是怎么实现的。
public static void asynchronousConfirmation() throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//队列声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
channel.confirmSelect();
//开始时间
long begin = System.currentTimeMillis();
//消息确认成功 回调函数
ConfirmCallback ackCallback = (deliverTag,multiple)->{
System.out.println("确认的消息:"+deliverTag);
};
//消息确认失败 回调函数
ConfirmCallback nckCallback = (deliverTag,multiple)->{
System.out.println("未确认的消息:"+deliverTag);
};
//准备消息监听器 监听那些消息成功 那些失败
channel.addConfirmListener(ackCallback,nckCallback);
for (int i = 0; i < COUNT; i++) {
String message = i + "";
channel.basicPublish("",queueName,null,message.getBytes(StandardCharsets.UTF_8));
}
//结束时间
long end = System.currentTimeMillis();
System.out.println("异步确认耗时时间:"+(end-begin)+"ms");
}
1.2.5 如何处理异步未确认消息
最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,比如说用ConcurrentLinkedQueue这个队列在confirm callbacks与发布线程之间进行消息的传递。
public static void asynchronousConfirmation() throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//队列声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
channel.confirmSelect();
/**
* 线程安全有序的哈希表 适用于高并发的情况下
* 1.轻松将序号与消息进行关联
* 2.轻松批量删除条目 只要给到序号
* 3.支持高并发
*/
ConcurrentSkipListMap<Long,String> outstandingConfirms = new ConcurrentSkipListMap<>();
//开始时间
long begin = System.currentTimeMillis();
//消息确认成功 回调函数
ConfirmCallback ackCallback = (deliverTag,multiple)->{
if (multiple){
//2:删除掉已经确认的消息,剩下的就是未确认的消息
ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(deliverTag);
confirmed.clear();
}else {
outstandingConfirms.remove(deliverTag);
}
System.out.println("确认的消息:"+deliverTag);
};
//消息确认失败 回调函数
ConfirmCallback nckCallback = (deliverTag,multiple)->{
String message = outstandingConfirms.get(deliverTag);
System.out.println("未确认的消息是:"+message);
};
//准备消息监听器 监听那些消息成功 那些失败 异步
channel.addConfirmListener(ackCallback,nckCallback);
for (int i = 0; i < COUNT; i++) {
String message = i + "";
channel.basicPublish("",queueName,null,message.getBytes(StandardCharsets.UTF_8));
//1:此处记录下所有发送的消息
outstandingConfirms.put(channel.getNextPublishSeqNo(),message);
}
//结束时间
long end = System.currentTimeMillis();
System.out.println("异步确认耗时时间:"+(end-begin)+"ms");
}
1.2.6 以上3种发布确认速度对比
-
单独发布消息同步等待确认
简单,但吞吐量非常有限。
-
批量发布消息批量同步等待确认
简单,合理的吞吐量,一旦出现问题但很难推断出是那条消息出现了问题。
-
异步处理:最佳性能和资源使用
在出现错误的情况下可以很好地控制,但是实现起来稍微难些
1.3 发布确认高级
在生产环境中由于一些不明原因,导致RabbitMQ重启,在RabbitMQ重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行RabbitMQ的消息可靠投递呢?特别是在这样比较极端的情况,RabbitMQ集群不可用的时候,无法投递的消息该如何处理呢?
1.4 发布确认SpringBoot版本
1.4.1 确认机制方案
1.4.2 代码架构图
1.4.3 配置文件
在配置文件当中需要添加
spring.rabbitmq.publisher-confirm-type=correlated
- NONE 禁用发布确认模式,是默认值
- CORRELATED 发布消息成功到交换器后会触发回调方法
- SIMPLE 经测试有两种效果,
- 其一效果和CORRELATED值一样会触发回调方法,
- 其二效果在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broke
#配置rabbitmq服务
spring:
rabbitmq:
host: 192.168.3.17
port: 5672
username: admin
password: admin
virtual-host: /
publisher-confirm-type: correlated
1.4.4 配置类
@Configuration
public class ConfirmConfig {
public final static String CONFIRM_EXCHANGE = "confirm.exchange";
public final static String CONFIRM_QUEUE = "confirm.queue";
public final static String CONFIRM_ROUTING_KEY = "key1";
@Bean
public Queue queue(){
return QueueBuilder.durable(CONFIRM_QUEUE).build();
}
@Bean("confirmExchange")
public DirectExchange directExchange(){
return new DirectExchange(CONFIRM_EXCHANGE);
}
@Bean
public Binding binding(@Qualifier("queue") Queue queue,
@Qualifier("confirmExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(CONFIRM_ROUTING_KEY).noargs();
}
}
1.4.5 生产者
@Slf4j
@RestController
@RequestMapping("/confirm")
public class Producer {
@Autowired
private RabbitTemplate rabbitTemplate;
//发布确认, 发送消息
@GetMapping("/sendConfirmMsg/{message}")
public void sendMessage(@PathVariable("message") String message){
//指定消息的id为 1
CorrelationData correlationData1 = new CorrelationData("1");
//指定路由key为 key1
String routingKey1 = "key1";
log.info("当前时间:{},发送一条消息",new Date().toString(),message);
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE,routingKey1,"发布确认消息:"+message+routingKey1,correlationData1);
//指定消息的id为 2
CorrelationData correlationData2 = new CorrelationData("2");
//指定路由key为 key2 错误的路由key只是为了测试
String routingKey2 = "key2";
log.info("当前时间:{},发送一条消息",new Date().toString(),message);
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE,routingKey2,"发布确认消息:"+message+routingKey2,correlationData2);
}
1.4.6 回调接口
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
//回调接口
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
//注入
rabbitTemplate.setConfirmCallback(this);
}
/**
* 交换机确认回调方法
* 1.发消息 交换机接收到了 回调
* 1.1 correlationData 保存着回调消息的ID及相关信息
* 1.2 ack 交换机收到消息 true
* 1.3 cause null
* 2.发消息 交换机接收失败 回调
* 2.1 correlationData 保存着回调消息的ID及相关信息
* 2.2 ack 交换机收到消息 false
* 2.3 cause 失败原因
* @param correlationData
* @param ack
* @param cause
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String id = correlationData.getId()!=null?correlationData.getId():"";
if (ack){
log.info("交换机已经收到了消息 ID :{}"+id);
}else {
log.info("交换机还未收到ID 为:{}的消息,由于原因:{}"+id,cause);
}
}
}
1.4.7 消费者
@Slf4j
@Component
public class Consumer {
@RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE)
public void receive(Message message){
String msg = new String(message.getBody());
log.info("接收到的队列confirm.queue 消息:{}"+msg);
}
}
可以看到,发送了两条消息,第一条消息的RoutingKey 为"key1",第二条消息的RoutingKey 为"key2",两条消息都成功被交换机接收,也收到了交换机的确认回调,但消费者只收到了一条消息,因为第二条消息的RoutingKey 与队列的BindingKey 不一致,也没有其它队列能接收这个消息,所以第二条消息被直接丢弃了。
1.5 回退消息
1.5.1 Mandatory 参数
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置mandatory参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。
1.5.2 配置文件
#配置rabbitmq服务
spring:
rabbitmq:
host: 192.168.3.17
port: 5672
username: admin
password: admin
virtual-host: /
publisher-confirm-type: correlated
publisher-returns: true
1.5.3 消息生产者
@Slf4j
@RestController
@RequestMapping("/confirm")
public class Producer {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private MyCallBack myCallBack;
//发布确认, 发送消息
@GetMapping("/sendConfirmMsg/{message}")
public void sendMessage(@PathVariable("message") String message){
//指定消息的id为 1
CorrelationData correlationData1 = new CorrelationData("1");
//指定路由key为 key1
String routingKey1 = "key1";
log.info("当前时间:{},发送一条消息",new Date().toString(),message);
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE,routingKey1,"发布确认消息:"+message+routingKey1,correlationData1);
//指定消息的id为 2
CorrelationData correlationData2 = new CorrelationData("2");
//指定路由key为 key2
String routingKey2 = "key2";
log.info("当前时间:{},发送一条消息",new Date().toString(),message);
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE,routingKey2,"发布确认消息:"+message+routingKey2,correlationData2);
}
}
1.5.4 回调接口
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
//回调接口
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
//注入
rabbitTemplate.setConfirmCallback(this);
/**
* true:交换机无法将消息进行路由时,会将该消息返回给生产者
* false:如果发现消息无法进行路由,则直接丢弃
*/
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnsCallback(this);
}
/**
* 交换机确认回调方法
* 1.发消息 交换机接收到了 回调
* 1.1 correlationData 保存着回调消息的ID及相关信息
* 1.2 ack 交换机收到消息 true
* 1.3 cause null
* 2.发消息 交换机接收失败 回调
* 2.1 correlationData 保存着回调消息的ID及相关信息
* 2.2 ack 交换机收到消息 false
* 2.3 cause 失败原因
* @param correlationData
* @param ack
* @param cause
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String id = correlationData.getId()!=null?correlationData.getId():"";
if (ack){
log.info("交换机已经收到了消息 ID :{}"+id);
}else {
log.info("交换机还未收到ID 为:{}的消息,由于原因:{}"+id,cause);
}
}
//只有不可达目的地的时候 才进行回退
@Override
public void returnedMessage(ReturnedMessage returned) {
log.error("消息{},被交换机{}退回,退回原因:{},路由key{}"+returned.getMessage(),returned.getExchange(),returned.getReplyText(),returned.getRoutingKey());
}
}
1.5.5 结果分析
1.6 备份交换机
有了mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置mandatory 参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。
在RabbitMQ中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么是备份交换机呢?备份交换机可以理解为RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为Fanout ,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
1.6.1 代码架构图
1.6.2 修改配置类
package com.ausware.yao.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ConfirmConfig {
//普通交换机
public final static String CONFIRM_EXCHANGE = "confirm.exchange";
//备份交换机
public final static String BACKUP_CONFIRM_EXCHANGE ="backup.exchange";
//普通队列
public final static String CONFIRM_QUEUE = "confirm.queue";
//备份队列
public final static String BACKUP_CONFIRM_QUEUE ="backup.queue";
//报警队列
public static final String WARNING_CONFIRM_QUEUE ="warning.queue";
public final static String CONFIRM_ROUTING_KEY = "key1";
@Bean
public Queue queue(){
return QueueBuilder.durable(CONFIRM_QUEUE).build();
}
@Bean
public Queue backupQueue(){
return QueueBuilder.durable(BACKUP_CONFIRM_QUEUE).build();
}
@Bean
public Queue warningQueue(){
return QueueBuilder.durable(WARNING_CONFIRM_QUEUE).build();
}
@Bean("confirmExchange")
public DirectExchange directExchange(){
return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE).durable(true)
.alternate(BACKUP_CONFIRM_EXCHANGE) //转发到备份交换机
.build();
}
@Bean("backupExchange")
public FanoutExchange fanoutExchange(){
return new FanoutExchange(BACKUP_CONFIRM_EXCHANGE);
}
@Bean
public Binding binding(@Qualifier("queue") Queue queue,
@Qualifier("confirmExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(CONFIRM_ROUTING_KEY).noargs();
}
@Bean
public Binding backupExchangeBindingBackupQueue(@Qualifier("backupQueue") Queue backupQueue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(backupQueue).to(backupExchange);
}
@Bean
public Binding backupExchangeBindingWarningQueue(@Qualifier("warningQueue") Queue warningQueue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(warningQueue).to(backupExchange);
}
}
1.6.3 消费者
@Slf4j
@Component
public class WarningConsumer {
@RabbitListener(queues = ConfirmConfig.WARNING_CONFIRM_QUEUE)
public void receiveMessage(Message message){
String msg = new String(message.getBody());
log.info("报警发现不可路由消息:{}"+msg);
}
}
重新启动项目的时候需要把原来的confirm.exchange删除因为我们修改了其绑定属性,不然会报错
1.6.4 结果分析
mandatory参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先级高,经过上面结果显示答案是备份交换机优先级高。