消息应答
消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉了,会发生什么情况。RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给该消费这的消息,因为它无法接收到。
为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。
消息应答的方法
自动应答:(默认)消息发送之后立即被认为已经传送成功,
手动应答:
- A.Channel.basicAck(用于肯定确认)
RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了 - B.Channel.basicNack(用于否定确认)
- C.Channel.basicReject(用于否定确认)
与 Channel.basicNack 相比少一个参数
不处理该消息了直接拒绝,可以将其丢弃了
若未发送ACK确认,消息会重新入队
手动应答可以进行批量应答:
持久化机制
队列持久化
声明队列时声明为持久化
消息持久化
,消息生产者添加属性:MessageProperties.PERSISTENT_TEXT_PLAIN
channel.basicPublic(EXCHANG_NAME,QUEUE_NAME,MessageProperties.PERSISTENT_TEXT_PLAIN,Message);
发布确认
生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了。如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理
单个发布确认
这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认
的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。
public static void publishMessageIndividually() throws Exception {
try (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 < MESSAGE_COUNT; i++) {
String message = i + "";
channel.basicPublish("", queueName, null, message.getBytes());
//服务端返回 false 或超时时间内未返回,生产者可以消息重发
boolean flag = channel.waitForConfirms();
if(flag){
System.out.println("消息发送成功");
}
}
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) +
"ms");
}
}
批量发布确认
上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
异步发布确认
public static void publishMessageAsync() throws Exception {
try (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<>();
/**
* 确认收到消息的一个回调
* 1.消息序列号
* 2.true 可以确认小于等于当前序列号的消息
* false 确认当前序列号消息
*/
ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
if (multiple) {
//返回的是小于等于当前序列号的未确认消息 是一个 map
ConcurrentNavigableMap<Long, String> confirmed =
outstandingConfirms.headMap(sequenceNumber, true);
//清除该部分未确认消息
confirmed.clear();
}else{
//只清除当前序列号的消息
outstandingConfirms.remove(sequenceNumber);
}
};
ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {
String message = outstandingConfirms.get(sequenceNumber);
System.out.println("发布的消息"+message+"未被确认,序列号"+sequenceNumber);
};
/**
* 添加一个异步确认的监听器
* 1.确认收到消息的回调
* 2.未收到消息的回调
*/
channel.addConfirmListener(ackCallback, null);
long begin = System.currentTimeMillis();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = "消息" + i;
/**
* channel.getNextPublishSeqNo()获取下一个消息的序列号
* 通过序列号与消息体进行一个关联
* 全部都是未确认的消息体
*/
outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
channel.basicPublish("", queueName, null, message.getBytes());
}
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个异步确认消息,耗时" + (end - begin) +
"ms");
}
}
如何处理异步未确认消息
最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,
比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传递。
消息幂等性
幂等操作的特点是其任意多次执行所产生的结果与一次执行的产生的结果相同。
解决方案
1、生成全局id,存入redis或者数据库,在消费者消费消息之前,查询一下该消息是否有消费过。
2、如果该消息已经消费过,则告诉mq消息已经消费,将该消息丢弃(手动ack)。
3、如果没有消费过,将该消息进行消费并将消费记录写进redis或者数据库中。
注:还有一种方式,数据库操作可以设置唯一键(消息id),防止重复数据的插入,这样插入只会报错而不会插入重复数据,本人没有测试。
消息退回机制
Mandatory 参数
通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。
备份交换机
当我们为某一个交换机声明一个对应的备份交换机时,
就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由
备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout
** 配置**
//备份交换机
public static final String backup_exchange="exchange.backup";
//普通交换机
public static final String normal_exchange="exchange.normal";
//警告队列
public static final String warning_queue="queue.backup";
//声明普通交换机时绑定备份交换机
@Bean
public DirectExchange normalExchange(){
return ExchangeBuilder.directExchange(normal_exchange).withArgument("alternate-exchange",backup_exchange).build();
}
@Bean
public Queue warnQueue(){
return QueueBuilder.durable(warning_queue).build();
}
@Bean
public FanoutExchange backupExchange(){
return ExchangeBuilder.fanoutExchange(backup_exchange).build();
}
/**
* 将告警队列与备份交换机绑定
* @param backupExchange 备份交换机
* @param warnQueue 告警队列
* @return 绑定关系
*/
@Bean
public Binding backBinding(@Qualifier("backupExchange")FanoutExchange backupExchange,
@Qualifier("warnQueue")Queue warnQueue){
return BindingBuilder.bind(warnQueue).to(backupExchange);
}
消费者代码
@RabbitListener(queues = MqConfig.warning_queue)
public void backup(Message message,Channel channel){
log.info("消息路由失败{}",new String(message.getBody()));
}