1、发布确认原理
书面文:生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置basic.ack 的multiple 域,表示到这个序列号之前的所有消息都已经得到了处理
confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。
声明队列持久化,然后生产消费消息时设置持久化,最后消息持久化到磁盘里后才进行发布确认,保证消息的持久化不丢失。这个是消息持久化的发布确认
2、发布确认策略
2.1、开启发布确认的方法
Channel channel = RabbitMQUtil.getChannel();
//开启发布确认
channel.confirmSelect();
2.2、单个确认发布
它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,最大的缺点就是:发布速度特别的慢,这种方式最多提供每秒不超过数百条发布消息的吞吐量
/**
* 单个确认
*/
public static void single() throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMQUtil.getChannel();
//队列名字
String queueName = UUID.randomUUID().toString();
//开启发布确认
channel.confirmSelect();
channel.queueDeclare(queueName,true,false,false,null);
//开始时间
long begin = System.currentTimeMillis();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String msg = i+"";
channel.basicPublish("",queueName,null,msg.getBytes());
// waitForConfirm():消息发布确认是否成功
if(channel.waitForConfirms()){
System.out.println("消息发布成功");
}
}
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) + "ms");
}
2.3、批量确认发布
与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布
/**
* 批量确认
*/
public static void batch() throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMQUtil.getChannel();
//队列名字
String queueName = UUID.randomUUID().toString();
//开启发布确认
channel.confirmSelect();
channel.queueDeclare(queueName,true,false,false,null);
//开始时间
long begin = System.currentTimeMillis();
//批量确认消息大小
int batchSize = 100;
for (int i = 0; i < MESSAGE_COUNT; i++) {
String msg = i+"";
channel.basicPublish("",queueName,null,msg.getBytes());
int j = 0;
j++;
//发100条确认一次
if(j == 99){
channel.waitForConfirms();
j =0;
}
}
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) + "ms");
}
2.4、异步确认发布
异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功,也因为是异步的,发消息和确认回调是异步的,所以生产者不需要管确认,只需要疯狂发送消息就行
/**
* 异步发布确认
*/
public static void Async() throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
//队列名字
String queueName = UUID.randomUUID().toString();
//开启发布确认
channel.confirmSelect();
channel.queueDeclare(queueName,true,false,false,null);
//开始时间
long begin = System.currentTimeMillis();
//消息确认成功的回调函数,两个参数:一个是消息的标识,一个是开启批量
ConfirmCallback ackCallback = (long deliveryTag, boolean multiple)->{
System.out.println("成功发布确认的消息:"+deliveryTag);
};
//消息确认失败回调函数
ConfirmCallback nackCallback = (long deliveryTag, boolean multiple)->{
System.out.println("未成功发布确认的消息:"+deliveryTag);
};
//准备消息的监听器,监听成功发布消息以及没成功的
channel.addConfirmListener(ackCallback,nackCallback);//这是异步的
for (int i = 0; i < MESSAGE_COUNT; i++) {
String msg = i+"";
channel.basicPublish("",queueName,null,msg.getBytes());
}
//结束时间
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) + "ms");
}
2.5、处理异步未确认消息
这个高并发有序Map进行线程之间消息的传递,很有用
最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,比如说用 ==ConcurrentSkipListMap(基于跳表的Map,有序)==这个队列在 confirm callbacks 线程与发布线程之间进行消息的传递,或者 ConcurrentNavigableMap
- 记录所有需要发送的消息,消息总和
- 删除已经发送确认的消息,剩下的就是未确认的消息
- 剩下的就是没有确认的,可以进行其他操作
/**
* 异步发布确认
*/
public static void Async() throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
//队列名字
String queueName = UUID.randomUUID().toString();
//开启发布确认
channel.confirmSelect();
channel.queueDeclare(queueName, true, false, false, null);
/**
* 用于回调线程与发布线程之间的消息传递
* 线程安全有序的哈希表,适用于高并发有序map
* 1、将序号标签与消息内容进行关联
* 2、轻松批量的根据序号删除。因为是有序的
* 3、支持高并发
*/
ConcurrentSkipListMap<Long, String> outstandingConfirm = new ConcurrentSkipListMap<>();
//消息确认成功的回调函数,两个参数:一个是消息的标识,一个是开启批量
ConfirmCallback ackCallback = (long deliveryTag, boolean multiple) -> {
if (multiple) {
//2、删除已经发送确认的消息,剩下的就是未确认的消息
//这里 headMap() 是拿到这个小于等于该参数为key的map,也就是确认了的消息map,第二个参数是包不包括这个key
//因为这里是批量的,所以这个deliveryTag之前的都是确认的,没有不确认的
ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirm.headMap(deliveryTag, true);
confirmed.clear();
}else {
//如果不是批量的直接删除就行,因为剩下的就是没有确认的消息
outstandingConfirm.remove(deliveryTag);
}
System.out.println("成功发布确认的消息:" + deliveryTag);
};
//消息确认失败回调函数
ConfirmCallback nackCallback = (long deliveryTag, boolean multiple) -> {
//打印未确认的消息
String msg = outstandingConfirm.get(deliveryTag);
System.out.println("未成功发布确认的消息标记:" + deliveryTag+"消息内容:"+msg);
};
//准备消息的监听器,监听成功发布消息以及没成功的
channel.addConfirmListener(ackCallback, nackCallback);//这是异步的
//开始时间
long begin = System.currentTimeMillis();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String msg = i + "";
//1、记录所有需要发送的消息,消息总和
//其实这个PublishSeqNo也就是消息的标识 deliveryTag
outstandingConfirm.put(channel.getNextPublishSeqNo(), msg);
channel.basicPublish("", queueName, null, msg.getBytes());
}
//结束时间
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) + "ms");
}