1、应答机制
1.1场景
有这么一个场景:消费者正在处理一个比较耗时的任务,突然这个时候消费者宕机了,那么会出现什么情况呢?这个消息就会没有处理成功,消息就会丢失。 因此Rabbitmq引入了消息的应答机制:手动应答和自动应答。
1.2 应答机制分类
- 1、手动应答:RabbitMQ 将消息分发给了消费者,并且只有当消费者处理完成了整个消息之后才会被认为消息传递成功了,然后才会将内存中的消息删除,手动应答则能够保证消息不会被丢失,所以在实际的应用当中绝大多数都采用手动应答
- 2、自动应答:RabbitMQ 只要将消息分发给消费者就被认为消息传递成功,就会将内存中的消息删除,而不管消费者有没有处理完消息。
1.3 原理
Producer 生产消息发送给消息队列,Consumer01 消费消息1、Consumer02 消费消息2、Consumer01 接收到了消息之后,在处理完部分逻辑的时候突然宕机了,Consumer01 未发送 ACK,此时消息1 不会丢失,而是重新进入队列,由状态正常的 Consumer02 消费掉
1.4代码示例
设置手动应答:
生产者:
/**
* 功能说明: 消息在手动应答时不丢失,放回队列中重新消费
*
* @author yangxu
* @date 2022/2/19
*/
public class Task2 {
//队列名称
public static String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
//声明队列
boolean durable = true; //队列持久化
channel.queueDeclare(TASK_QUEUE_NAME,durable,false,false,null);
//从控制台输入信息
Scanner scan = new Scanner(System.in);
while (scan.hasNext()){
String message = scan.next();
// 第三个参数:MessageProperties.PERSISTENT_TEXT_PLAIN 消息持久化
channel.basicPublish("",TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("UTF-8"));
System.out.println("生产者发出消息:"+message);
}
}
}
消费者1
/**
* 功能说明:消息在手动应答时不丢失,放回队列中重新消费
*
* @author yangxu
* @date 2022/2/19
*/
public class Work01 {
//队列名称
public static String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
// 更改消费者这一方的代码,设置不公平分发=1 0:表示公平分发
// int prefetchCount = 1;
int prefetchCount = 2;
channel.basicQos(prefetchCount);
System.out.println("C1等待接收消息处理时间短");
DeliverCallback deliverCallback = (consumerTag, message) -> {
SleepUtils.sleep(1);
System.out.println("C1接收到的消息:" + new String(message.getBody(), "UTF-8"));
// 手动应答
/**
* 1、消息的标记
* 2、是否批量应答
* false: 只应答接收到的那个消息
* true: 应答所有传递过来的消息
* */
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
};
// 消费者取消消费接口回调
//采用手动应答
boolean autoAck = false;
channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费接口的回调逻辑");
});
}
}
消费者2:
/**
* 功能说明:消息在手动应答时不丢失,放回队列中重新消费
*
* @author yangxu
* @date 2022/2/19
*/
public class Work02 {
//队列名称
public static String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
// 更改消费者这一方的代码,设置不公平分发=1 0:表示公平分发
// int prefetchCount = 1;
int prefetchCount = 5;
channel.basicQos(prefetchCount);
System.out.println("C2等待接收消息处理时间短");
DeliverCallback deliverCallback = (consumerTag, message)->{
SleepUtils.sleep(30);
System.out.println("C2接收到的消息:"+new String(message.getBody(),"UTF-8"));
//手动应答
/**
* 1、消息的标记
* 2、是否批量应答
* */
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
};
//采用手动应答
boolean autoAck = false;
channel.basicConsume(TASK_QUEUE_NAME,autoAck,deliverCallback,(consumerTag)->{
System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
});
}
}
消息的自动应答:
boolean autoAck = true;
channel.basicConsume(TASK_QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
2、消息和队列的持久化
2.1场景
如果Rabbitmq退出或由于某种原因崩溃时,该队列就会被删除掉,消息就会丢失,因此需要设置队列和消息持久化。
2.2队列和消息持久化
设置方式很简单,只需要在声明队列的时候设置durable=true,即可设置队列支持化。
- 1、队列持久化
//声明队列
boolean durable = true; //队列持久化
channel.queueDeclare(TASK_QUEUE_NAME,durable,false,false,null);
但是需要注意的就是如果之前声明的队列不是持久化的,需要把原先队列先删除,或者重新创建一个持久化的队列,不然就会出现错误。
- 2、消息持久化
要想让消息实现持久化需要在消息生产者修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN 添加这个属性
// 第三个参数:MessageProperties.PERSISTENT_TEXT_PLAIN 消息持久化
channel.basicPublish("",TASK_QUEUE_NAME,MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("UTF-8"));
将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。因此,需要引入发布确认机制进一步保证消息持久化,而不丢失问题。
3、消息的分发机制
3.1场景
如有两个消费者处理任务,一个消费者处理速度非常快,一个消费者处理速度非常慢,采用轮训分发的机制会导致消费快的消费者大部分时间处于空闲状态,这种很不公平,需要避免。为此引入不公平的分发机制。
3.2不公平分发
RabbitMQ 分发消息采用的轮训分发,但是在某种场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是RabbitMQ 并不知道这种情况它依然很公平的进行分发。
- 1、设置不公平分发的方式
// 更改消费者这一方的代码,设置不公平分发=1 0:表示公平分发
int prefetchCount = 1;
channel.basicQos(prefetchCount);
3.3 预取值
4、布确认的原理及策略
4.1 发布确认的原理
生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置basic.ack 的multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。
confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。
4.2发布确认的策略
如何开发confirm模式:
channel.queueDeclare(queueName, false, false, false, null);
// 开启发布确认
channel.confirmSelect();
- 1、单个确认
// 1、单个确认
public static void publishMessageIndividually() throws Exception {
Channel channel = RabbitMqUtils.getChannel();
// 队列的声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, true, 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());
// 单个消息发布确认
boolean flag = channel.waitForConfirms();
if (flag) {
System.out.println("消息发送成功");
}
}
// 结束时间
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时:" + (end - begin));
}
这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。
这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。
- 2、批量确认
public static void publishMessageBatch() throws Exception {
Channel channel = RabbitMqUtils.getChannel();
// 队列的声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, true, false, false, null);
// 开启发布确认模式
channel.confirmSelect();
//开始时间
long begin = System.currentTimeMillis();
int batchSiz = 100;
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = i + "";
channel.basicPublish("", queueName, null, message.getBytes());
if (i % batchSiz == 0) {
channel.waitForConfirms();
}
}
// 结束时间
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时:" + (end - begin));
}
与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
- 3、异步确认
异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的。
// 异步确认发布
public static void publishMessageAsync() 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<>();
/**
* 确认收到消息的一个回调
* 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");
}
从性能和可靠性来看,异步确认机制性能最佳,在出现错误的情况下可以很好地控制,批量确认一旦出现问题很难排查是那条消息未确认,单个确认效率和吞吐量太低。
留一个疑问,如何处理确认的消息?欢迎留言发表自己意见。