整体代码不使用rabbitTemplate(高级?),使用原生channel。
应答功能属于消费者,消费完消息告诉 RabbitMQ 已经消费成功。
发布功能属于生产者,生产消息到 RabbitMQ,RabbitMQ 需要告诉生产者已经收到消息。
消费应答
消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉了,会发生什么情况。RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给该消费者的消息,因为它无法接收到。
为了保证消息在发送过程中不丢失,引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了
自动应答
消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel 关闭,那么消息就丢失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制,当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并以 某种速率能够处理这些消息的情况下使用。
手动应答
消费者处理完业务逻辑,手动返回ack(通知)告诉队列处理完了,队列进而删除消息。
手动应答案例
默认消息采用的是自动应答,所以我们要想实现消息消费过程中不丢失,需要把自动应答改为手动应答
消费者启用两个线程,消费 1 一秒消费一个消息,消费者 2 十秒消费一个消息,然后在消费者 2 消费消息的时候,停止运行,这时正在消费的消息是否会重新进入队列,而后给消费者 1 消费呢?
- 工具类
-
public class SleepUtils { public static void sleep(int second){ try { Thread.sleep(1000*second); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
消息生产者:
-
/** * 消息在手动应答是不丢失、放回队列中重新消费 */ public class Task02 { //队列名称 public static final String TASK_QUEUE_NAME = "ACK_QUEUE"; public static void main(String[] args) throws IOException, TimeoutException { Channel channel = RabbitMQUtils.getChannel(); //声明队列 channel.queueDeclare(TASK_QUEUE_NAME,false,false,false,null); //在控制台中输入信息 Scanner scanner = new Scanner(System.in); System.out.println("请输入信息:"); while (scanner.hasNext()){ String message = scanner.next(); channel.basicPublish("",TASK_QUEUE_NAME,null,message.getBytes("UTF-8")); System.out.println("生产者发出消息:"+ message); } } }
消费者 1:
-
package com.fan.rabbitmqtest.consumer; import com.fan.rabbitmqtest.utils.RabbitMQUtils; import com.fan.rabbitmqtest.utils.SleepUtils; import com.rabbitmq.client.CancelCallback; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; import java.io.IOException; import java.util.concurrent.TimeoutException; public class Work03 { //队列名称 public static final String TASK_QUEUE_NAME = "ACK_QUEUE"; //接受消息 public static void main(String[] args) throws IOException, TimeoutException { Channel channel = RabbitMQUtils.getChannel(); System.out.println("C1等待接受消息处理时间较短"); DeliverCallback deliverCallback =(consumerTag, message) ->{ //沉睡1S SleepUtils.sleep(1); System.out.println("接受到的消息:"+new String(message.getBody(),"UTF-8")); //手动应答 /** * 肯定确认应答 * 1.消息的标记Tag * 2.是否批量应答 false表示不批量应答信道中的消息 */ channel.basicAck(message.getEnvelope().getDeliveryTag(),false); /** * 否定确认应答 * 1.拒绝 deliveryTag 对应的消息 * 2.是否 requeue:true 则重新入队列,false 则丢弃或者进入死信队列。 * 该方法 reject 后,该消费者还是会消费到该条被 reject 的消息。 */ // channel.basicReject(message.getEnvelope().getDeliveryTag(),false); /** * 用于否定确认,表示己拒绝处理该消息,可以将其丢弃了 * 1.拒绝 deliveryTag 对应的消息 * 2.是否 应用于多消息 * Multiple 的解释:手动应答的好处是可以批量应答并且减少网络拥堵 * true 代表批量应答 channel 上未应答的消息 * 比如说 channel 上有传送 tag 的消息 5,6,7,8 当前 tag 是 8 , * 那么此时 5-8 的这些还未应答的消息都会被确认收到消息应答 * false 同上面相比只会应答 tag=8 的消息 5,6,7 这三个消息依然不会被确认收到消息应答 * 3.是否 requeue,与 basicReject 区别就是同时支持多个消息, * 可以 拒绝签收 该消费者先前接收未 ack 的所有消息。拒绝签收后的消息也会被自己消费到。 */ // channel.basicNack(message.getEnvelope().getDeliveryTag(),false,false); /** * 是否恢复消息到队列 * 1.是否 requeue,true 则重新入队列,并且尽可能的将之前 recover 的消息投递给其他消费者消费, * 而不是自己再次消费。false 则消息会重新被投递给自己。 * 消息自动重新入队: * 如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失), * 导致消息未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。 * 如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。 * 这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。 */ // channel.basicRecover(false); }; CancelCallback cancelCallback = (consumerTag -> { System.out.println(consumerTag + "消费者取消消费接口回调逻辑"); }); //采用手动应答 boolean autoAck = false; channel.basicConsume(TASK_QUEUE_NAME,autoAck,deliverCallback,cancelCallback); } }
消费者 2:
将消费者1中的代码,睡眠时间改为 10 秒
-
结果:
当发送者发送消息 DD 到队列,此时是 second 来消费该消息,但是由于它处理时间较长,在还未处理完时间里停止运行,也就是说 second 还没有执行到 ack 代码的时候,second 被停掉了,此时会看到消息被 first 接收到了,说明消息 DD 被重新入队,然后分配给能处理消息的 first处理。 -
发布确认
生产者发布消息到 RabbitMQ 后,需要 RabbitMQ 返回「ACK(已收到)」给生产者,这样生产者才知道自己生产的消息成功发布出去。 -
单个确认发布
这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,
waitForConfirmsOrDie(long)
这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。、
-
public class ConfirmMessage { //单个发消息的个数 public static final int MESSAGE_COUNT = 1000; //Ctrl+Shift+U 变大写 public static void main(String[] args) throws InterruptedException, TimeoutException, IOException { publishMessageIndividually();//发布1000个单独确认消息,耗时:599ms } //单个确认 public static void publishMessageIndividually() throws IOException, TimeoutException, InterruptedException { Channel channel = RabbitMQUtils.getChannel(); //队列的声明 String queueName = UUID.randomUUID().toString(); channel.queueDeclare(queueName,false,true,false,null); //开启发布确认 channel.confirmSelect(); //开始时间 long begin = System.currentTimeMillis(); //批量发消息 for (int i = 0; i < 1000; i++) { String message = i+""; channel.basicPublish("",queueName,null,message.getBytes()); //单个消息就马上进行发布确认 boolean flag = channel.waitForConfirms(); if(flag){ System.out.println("消息发送成功"); } } //结束时间 long end = System.currentTimeMillis(); System.out.println("发布"+MESSAGE_COUNT+"个单独确认消息,耗时:"+(end-begin)+"ms"); } }
确认发布指的是成功发送到了队列,并不是消费者消费了消息。
-
批量确认发布
单个确认发布方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
-
public class ConfirmMessage2 { //批量发消息的个数 public static final int MESSAGE_COUNT = 1000; //Ctrl+Shift+U 变大写 public static void main(String[] args) throws InterruptedException, TimeoutException, IOException { publishMessageBatch(); //发布1000个批量确认消息,耗时:111ms } //批量发布确认 public static void publishMessageBatch() throws IOException, TimeoutException, InterruptedException { Channel channel = RabbitMQUtils.getChannel(); //队列的声明 String queueName = UUID.randomUUID().toString(); channel.queueDeclare(queueName, false, true, false, null); //开启发布确认 channel.confirmSelect(); //开始时间 long begin = System.currentTimeMillis(); //批量确认消息大小 int batchSize =100; //批量发送消息,批量发布确认 for (int i = 0; i < MESSAGE_COUNT; i++) { String message=i+""; channel.basicPublish("",queueName,null,message.getBytes()); //判断达到100条消息的时候,批量确认一次 if((i+1)%batchSize==0){ //发布确认 channel.waitForConfirms(); } } //结束时间 long end = System.currentTimeMillis(); System.out.println("发布"+MESSAGE_COUNT+"个批量确认消息,耗时:"+(end-begin)+"ms"); } }
异步确认发布
异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都很好,利用了回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功,下面详细讲解异步确认是怎么实现的。
-
添加回调函数,在回调函数里进行确认发布
-
public class ConfirmMessage3 { public static final int MESSAGE_COUNT = 1000; //Ctrl+Shift+U 变大写 public static void main(String[] args) throws Exception { publishMessageAsync(); //发布1000个异步发布确认消息,耗时:43ms } //异步发布确认 public static void publishMessageAsync() throws Exception{ Channel channel = RabbitMQUtils.getChannel(); //队列的声明 String queueName = UUID.randomUUID().toString(); channel.queueDeclare(queueName, false, true, false, null); //开启发布确认 channel.confirmSelect(); //开始时间 long begin = System.currentTimeMillis(); //消息确认回调的函数 ConfirmCallback ackCallback = (deliveryTag,multiple) ->{ System.out.println("确认的消息:"+deliveryTag); }; /** * 1.消息的标记 * 2.是否为批量确认 */ //消息确认失败回调函数 ConfirmCallback nackCallback= (deliveryTag,multiple) ->{ System.out.println("未确认的消息:"+deliveryTag); }; //准备消息的监听器 监听那些消息成功了,哪些消息失败了 /** * 1.监听哪些消息成功了 * 2.监听哪些消息失败了 */ channel.addConfirmListener(ackCallback,nackCallback);//异步通知 //批量发送消息 for (int i = 0; i < MESSAGE_COUNT; i++) { String message=i+"消息"; channel.basicPublish("",queueName,null,message.getBytes()); } //结束时间 long end = System.currentTimeMillis(); System.out.println("发布"+MESSAGE_COUNT+"个异步发布确认消息,耗时:"+(end-begin)+"ms"); } }
实际案例里,将发布的消息存入 Map 里,方便获取。
headMap
方法用于将已确认的消息存入新的 Map 缓存区里,然后清除该新缓存区的内容。因为headMap
方法是浅拷贝,所以清除了缓存区,相当于清除了内容的地址,也就清除了队列的确认的消息。 -
如何处理异步未确认消息?
最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传递。
-
public class ConfirmMessage3 { public static final int MESSAGE_COUNT = 1000; //Ctrl+Shift+U 变大写 public static void main(String[] args) throws Exception { publishMessageAsync(); //发布1000个异步发布确认消息,耗时:43ms } //异步发布确认 public static void publishMessageAsync() throws Exception{ Channel channel = RabbitMQUtils.getChannel(); //队列的声明 String queueName = UUID.randomUUID().toString(); channel.queueDeclare(queueName, false, true, false, null); //开启发布确认 channel.confirmSelect(); /** * 线程安全有序的一个哈希表,适用于高并发的情况下 * 1.轻松的将序号与消息进行关联 * 2.轻松批量删除条目 只要给到序号 * 3.支持高并发(多线程) */ ConcurrentSkipListMap<Long,String> outstandingConfirms= new ConcurrentSkipListMap<>(); //消息确认回调的函数 ConfirmCallback ackCallback = (deliveryTag,multiple) ->{ if(multiple) { //2.删除掉已经确认的消息 剩下的就是未确认的消息 ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(deliveryTag); confirmed.clear(); }else { outstandingConfirms.remove(deliveryTag); } System.out.println("确认的消息:" + deliveryTag); }; /** * 1.消息的标记 * 2.是否为批量确认 */ //消息确认失败回调函数 ConfirmCallback nackCallback= (deliveryTag,multiple) ->{ //3.打印一下未确认的消息都有哪些 String message = outstandingConfirms.remove(deliveryTag); System.out.println("未确认的消息是:"+message+":::未确认的消息tag:"+deliveryTag); }; //准备消息的监听器 监听那些消息成功了,哪些消息失败了 /** * 1.监听哪些消息成功了 * 2.监听哪些消息失败了 */ channel.addConfirmListener(ackCallback,nackCallback);//异步通知 //开始时间 long begin = System.currentTimeMillis(); //批量发送消息 for (int i = 0; i < MESSAGE_COUNT; i++) { String message=i+"消息"; channel.basicPublish("",queueName,null,message.getBytes()); //1.此处记录下所有要发送的消息 消息的总和 outstandingConfirms.put(channel.getNextPublishSeqNo(),message); } //结束时间 long end = System.currentTimeMillis(); System.out.println("发布"+MESSAGE_COUNT+"个异步发布确认消息,耗时:"+(end-begin)+"ms"); } }
以上 3 种发布确认速度对比:
-
单独发布消息
同步等待确认,简单,但吞吐量非常有限。
-
批量发布消息
批量同步等待确认,简单,合理的吞吐量,一旦出现问题但很难推断出是那条消息出现了问题。
-
异步处理
最佳性能和资源使用,在出现错误的情况下可以很好地控制,但是实现起来稍微难些
-