本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
前言
一般情况下,消息由生产者生产后发送到Broker并存储到队列Queue中,再由消费者消费。但如果消息由生产端发送到Broker过程中出现意外情况,那么就会造成消息的丢失。那么我们如何保证消息成功发送到Broker端呢?
这个时候消息确认机制就派上了用场。消息确认机制是指,生产者投递完消息之后,如果Broker收到消息,则会给生产者发送一条确认消息。生产者通过确认消息来判断消息是否成功发送到Broker,这种方式是消息投递可靠性的核心保障。
RabbitMQ提供了两种确认消息是否投递成功的机制:
-
AMQP协议提供的事务机制; -
RabbitMQ提供的confirm模式
注:两种模式不能同时使用,只能选择其中的一种
AMQP提供的事务机制
事务的实现主要是对Channel进行设置,主要的方法有三个:
channel.txSelect() // 声明启动事务模式;
channel.txCommit() // 提交事务;
channel.txRollBack() // 回滚事务;
通过txSelect()方法开启事务之后,如果txCommit()提交成功,则表明消息已经成功达到了Broker之中,如果在txCommit执行之前Broker异常崩溃或者出现网络异常等等,这个时候就可以通过txRollBack()方法回滚事务,然后重发消息。但需要注意的是,事务会降低RabbitMQ的性能。下面是事务的演示代码。
在本地启动Rabbit,然后在pom.xml引入RabbitMQ的依赖
<!-- RabbitMQ依赖 -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.4.0</version>
</dependency>
编写测试代码
public class TxProducer {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
// ConnectionUtil 抽离出连接本地Rabbit的逻辑用以复用
Connection connection= ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
String exchangeName="exchange_tx_confirm";
String routingKey="tx.confirm";
String message="test for tx mode";
//开启事务模式
channel.txSelect();
//用于比较事务的性能
long start=System.currentTimeMillis();
try {
channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
channel.txCommit();
System.out.println("消息已成功发送到Broker");
} catch (IOException e) {
e.printStackTrace();
System.out.println("消息发送到Broker失败");
channel.txRollback();
// 重发 或者 入库
}
//测试性能
// for (int i = 0; i <100 ; i++) {
// try {
// channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
// channel.txCommit();
// System.out.println("消息已成功发送到Broker");
// } catch (IOException e) {
// e.printStackTrace();
// System.out.println("消息发送到Broker失败");
// channel.txRollback();
// // 重发 或者 入库
// }
// }
//
// long end=System.currentTimeMillis();
//
// System.out.println("事务耗时:"+(end-start)+" ms");
channel.close();
connection.close();
}
}
public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
String exChangeName="exchange_ordinary_confirm";
String type="direct";
String queueName="queue_ordinary_queue";
String routingKey="ordinary.confirm";
channel.exchangeDeclare(exChangeName, type, true);
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, exChangeName, routingKey);
MyConsumer consumer=new MyConsumer(channel);
channel.basicConsume(queueName, true, consumer);
}
}
public class MyConsumer extends DefaultConsumer {
private Channel channel;
public MyConsumer(Channel channel) {
super(channel);
}
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("consumerTag: \n"+consumerTag);
System.out.println("envelope: \n"+envelope);
System.out.println("properties: \n"+properties.getMessageId());
System.out.println("message: \n"+new String(body));
}
}
分别启动生产端和消费端,生产端控制台输出:
生产端已发送消息,message:test for tx mode 消费端控制台输出:
consumerTag: amq.ctag-Ig5FN1baG9OcRORQ0xhu0w
envelope: Envelope(deliveryTag=1, redeliver=false, exchange=exchange_tx_confirm, routingKey=tx.confirm)
properties: null
message: test for tx mode confirm机制
confirm机制
使用事务机制来保证消息被成功从生产端发送到Broker中,但事务机制会降低RabbitMQ的性能。这时,我们就可以使用Rabbit提供的confirm模式。
confirm模式的最大好处在于它是异步执行的。使用confirm模式时,生产端就可以在等待返回确认的同时继续发送下一条消息,提高效率。Broker成功收到消息之后会给生产端发送一条Ack消息;如果消息服务器出现内部错误等原因导致消息丢失,则会发送一条Nack消息。
当生产端将channel设为confirm模式时,所有在该channel上发布的消息都会被指派一个唯一的id(从1开始),当消息被投递到匹配的队列后,Broker就会发送一个确认(包含消息的唯一id)给生产端,此时生产端就知道消息已成功发送到Broker端了。如果消息和队列是可持久化的,那么确认消息将在消息被写入磁盘之后发出,Broker回传给生产端的确认消息中的deliver-tag域包含了确认消息的序列号,此外Broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消息都已经得到了处理。
当channel被设置成confirm模式后,所有被publish的后续消息都将被ack或者被nack一次,不会既被ack又被nack。需要注意的是,RabbitMQ并没有对confirm消息的快慢做保证。
生产端的confirm模式有三种:
-
普通
confirm模式 -
批量
confirm模式 -
异步
confirm模式。
普通confirm模式
普通confirm模式就是生产端发送一条消息就等待确认一条。使用方式如下:
channel.confirmSelect();
channel.basicPublish(exchange,routingKey,null,message.getByte());
channel.waitForConfirms();
// channel.waitForConfirms()返回true或者false,消息成功返回true;反之,则false
public class Producer {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Connection connection= ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
String exchangeName="exchange_ordinary_confirm";
String routingKey="ordinary.confirm";
String message="test for ordinary confirm mode";
long start=System.currentTimeMillis();
//开启confirm模式
channel.confirmSelect();
channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
if (channel.waitForConfirms()){
System.out.println("消息已成功发送到Broker");
}else {
System.out.println("消息发送到Broker失败");
//业务逻辑 重新发送 或者入库
}
//测试性能
// for (int i = 0; i <100 ; i++) {
// channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
// if (channel.waitForConfirms()){
// System.out.println("消息已成功发送到Broker");
// }else {
// System.out.println("消息发送到Broker失败");
// //业务逻辑 重新发送 或者入库
// }
// }
//
// long end=System.currentTimeMillis();
// System.out.println("普通confirm耗时:"+(end-start)+" ms");
channel.close();
connection.close();
}
}
启动生产端,控制台输出如下:
消息已成功发送到Broker
运行生产端,控制台输出如下:
consumerTag: amq.ctag-e3Pp2h-Yr6NXbJQ2lQqVfQ
envelope: Envelope(deliveryTag=1, redeliver=false, exchange=exchange_ordinary_confirm, routingKey=ordinary.confirm)
properties: null
message: test for ordinary confirm mode
消费端程序可参考之前的事务模式。普通confirm模式相比于事务来说,效率只高了一点点。这是因为普通confirm模式其实也是阻塞的,但比事务模式少了一步提交事务的步骤。事务是发送完消息之后还有提交事务,等待Broker返回事务提交成功的消息,而普通confirm模式发完消息后等待Broker的ack消息。
批量模式
所谓的批量模式,其实是指将多条消息发送完之后,再调用channel.waitForConfirms()方法来判断这一批消息是否成功发送到了Broker。
public class BatchProducer {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Connection connection= ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
String exchangeName="exchange_batch_confirm";
String routingKey="batch.confirm";
long start=System.currentTimeMillis();
//开启confirm模式
channel.confirmSelect();
for (int i = 0; i <100 ; i++) {
String message="test for ordinary batch mode "+i;
channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
}
if (channel.waitForConfirms()){
System.out.println("消息已成功发送到Broker");
}else {
System.out.println("消息发送到Broker失败");
//业务逻辑 重新发送 或者入库
}
long end=System.currentTimeMillis();
System.out.println("批量confirm耗时:"+(end-start)+" ms");
}
}
批量confirm模式相比于普通confirm模式来说,效率提高了不少。但也有明显的缺点,若是waitForConfirms()方法返回false或者超时,那么这一批次的消息要全部重发,这样的话,效率并不比普通confirm模式要高。
异步confirm模式
生产端可以通过调用channel.addConfirmListener(ConfirmListener listener)的方式来实现异步confirm模式。ConfirmListener提供了两个方法 handleAck(long deliveryTag,boolean multiple)和handleNack(long deliveryTag,boolean multuple)方法分别处理队列Broker返回的Basic.Ack和Basic.Nack。其中参数deliveryTag就是我们上面提到过的标记消息的唯一id,而multiple则表示是否批量确认。
对于异步confirm模式来说,我们需要为每一个channel维护一个unconfirm的消息序列集合,每当生产端发送一条消息时,集合中元素+1。若是Broker返回ack,则unconfir”集合则删除相应的一条或多条记录。如果multiple为false,则从集合中删除当前deliveryTag元素,如果multiple为true,则将集合中小于等于当前序号deliveryTag元素的集合清除,表示这批序号的消息都已经被ack了。这个unconfirm集合最好使用有序集合SortedSet的存储结构来存储deliveryTag。
public class AsynchronousProducer {
static SortedSet<Long> unconfirm = Collections.synchronizedSortedSet(new TreeSet<>());
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "exchange_confirm";
String routingKey = "confirm.test";
channel.confirmSelect();
long start=System.currentTimeMillis();
//addConfirmListener要在发布消息之前
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
//批量确认:将unconfirm集合中小于等于当前序号的deliveryTag清除掉,表示这些消息都已经被ack了
if (multiple) {
System.out.println("ack批量确认,deliveryTag:" + deliveryTag + ",multiple:" + multiple + ",当次确认消息序号集合:" + unconfirm.headSet(deliveryTag + 1));
unconfirm.headSet(deliveryTag + 1).clear();
} else {
//单条确认
System.out.println("ack单条确认,deliveryTag:" + deliveryTag + ",multiple:" + multiple + ",当次确认消息序号" + deliveryTag);
unconfirm.remove(deliveryTag);
}
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
unconfirm.headSet(deliveryTag+1).clear();
// 重发 或者 入库
} else {
unconfirm.remove(deliveryTag);
//重发 或者 入库
}
}
});
for (int i = 0; i <100 ; i++) {
String message = "test asynchronous confirm " + i;
long nextSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
System.out.println("消费端发送了消息,message:" + message);
i++;
unconfirm.add(nextSeqNo);
}
long end=System.currentTimeMillis();
System.out.println("异步confirm耗时:"+(end-start)+" ms");
}
}
分别使用这四种方式发消息进行性能对比,每次生产100条消息,取三次结果的平均值,比较其耗时,分别如下:
事务耗时:38.3 ms
普通confirm耗时:37.3 ms
批量confirm耗时:11.3 ms
异步confirm耗时:8.0 ms
总结
综上所述,我们可以使用AMQP协议的事务机制或者RabbitMQ提供的confirm模式来确保生产端的消息成功发送到Broker中。就效率而言,异步confirm>批量confirm>普通confirm>事务。