在使用RabbitMQ的时候,可以通过消息持久化操作来解决因为服务器的异常崩溃而导致的消息丢失。除此之外,我们还会遇到一个问题,当消息的生产者将消息发送出去之后,消息到底有没有正确地到达服务器呢?如果不进行特殊配置,默认情况下发送消息的操作是不会返回任何信息给生产者的,也就是默认情况下生产者是不知道消息有没有正确地到达服务器。如果在消息到达服务器之前已经丢失,持久化操作也解决不了这个问题,因为消息根本没有到达服务器,怎么持久化?RabbitMQ针对这个问题,提供了两种解决方案:
- 通过事务机制实现。
- 通过生产者确认机制实现。
1. 事务机制
RabbitMQ客户端中与事务机制相关的方法有三个:channel.txSelect
、channel.txCommit
和channel.txRollback
。
channel.txSelect
:用于将当前的信道设置成事务模式。channel.txCommit
:用于提交事务。channel.txRollback
:用于事务回滚。
1.1 事务正常提交
当执行channel.txCommit
那么消息一定到达服务器,此时消费者接收到消息进行消费。
channel.exchangeDeclare("demo.direct.exchange", "direct", true, false, null);
channel.queueDeclare("demo.queue", true, false, false, null);
channel.queueBind("demo.queue", "demo.direct.exchange", "demo.routingKey");
try {
channel.txSelect();
channel.basicPublish("demo.direct.exchange", "demo.routingKey",
MessageProperties.PERSISTENT_TEXT_PLAIN, "transaction message".getBytes());
channel.txCommit();
System.out.println("commit successful!");
} catch (Exception e) {
System.out.println("rollback!");
channel.txRollback();
}
1.2 事务回滚
channel.exchangeDeclare("demo.direct.exchange", "direct", true, false, null);
channel.queueDeclare("demo.queue", true, false, false, null);
channel.queueBind("demo.queue", "demo.direct.exchange", "demo.routingKey");
try {
channel.txSelect();
channel.basicPublish("demo.direct.exchange", "demo.routingKey",
MessageProperties.PERSISTENT_TEXT_PLAIN, "transaction message".getBytes());
int result = 10 / 0;
channel.txCommit();
System.out.println("commit successful!");
} catch (Exception e) {
System.out.println("rollback!");
channel.txRollback();
}
2. 生产者确认
事务机制虽然能够解决消息是否真正地到达了RabbitMQ,但是采用事务机制实现会严重降低RabbitMQ的消息吞吐量,而生产者确认方案则相对来说更加的轻量级。
生产者将信道设置成confirm
模式,一旦信道进入confirm
模式,所有在该信道上面发布的消息都会被指派一个唯一的id,一旦消息被投递到所有匹配的队列之后,RabbitMQ就会发送一个确认(Basic.Ack
)给生产者,这就使得生产者知晓消息已经正确到达目的地了。如果消息的和队列是可持久化的,那么确认消息会在消息写入磁盘之后发出。如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条Basic.Nack
消息给生产者。
事务机制在一条消息发送之后会使发送端阻塞,以等待RabbitMQ的回应,之后才能继续发送下一条消息。相比之下,发送方确认机制最大的好处在于它是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息。当消息得到确认之后,再通过回调方法来处理该确认消息。
2.1 普通confirm模式
普通的confirm模式是一种串行同步等待的方式,和事务机制一样,发送消息之后等待服务端确认,之后再发送消息。
channel.exchangeDeclare("demo.direct.exchange", "direct", true, false, null);
channel.queueDeclare("demo.queue", true, false, false, null);
channel.queueBind("demo.queue", "demo.direct.exchange", "demo.routingKey");
try {
channel.confirmSelect();
channel.basicPublish("demo.direct.exchange", "demo.routingKey",
MessageProperties.PERSISTENT_TEXT_PLAIN, "confirm message".getBytes());
if (channel.waitForConfirms() == false) {
System.out.println("send message failed");
}
} catch (Exception e) {
System.out.println("error!");
}
2.2 批量confirm模式
客户端在批量发送一批消息后,再调用channel.waitForConfirms
来等待RabbitMQ的确认返回。相比普通的confirm模式,其极大地提升了confirm的效率,但是问题在于出现返回Basic.Nack
或者超时情况时,客户端需要将这一批次的消息全部重发,这会带来明显的重复消息数量,并且当消息经常丢失时,批量confirm的性能应该是不升反降的。
List<String> waitingForConfirms = new ArrayList<>();
try {
channel.confirmSelect();
int index = 0;
while (true) {
String message = "confirm message " + index;
waitingForConfirms.add(message);
channel.basicPublish("demo.direct.exchange", "demo.routingKey",
MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
if (++index >= 10) {
index = 0;
try {
if (channel.waitForConfirms()) {
// 当前批次消息已经全部发送成功,清空缓存,进行下一批次的消息发送。
waitingForConfirms.clear();
} else {
// 当前批次消息确认失败,重新发送
}
} catch (Exception e) {
// 当前批次消息确认失败,重新发送
}
}
}
} catch (Exception e) {
System.out.println("error!");
}
2.3 异步confirm模式
提供ConfirmListener
这个回调接口即可。
SortedSet<Long> confirmSet = new TreeSet<>();
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("Ack,SeqNo:" + deliveryTag + ",multiple:" + multiple);
if (multiple) {
confirmSet.headSet(deliveryTag - 1).clear();
} else {
confirmSet.remove(deliveryTag);
}
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("Nack,SeqNo:" + deliveryTag + ",multiple:" + multiple);
if (multiple) {
confirmSet.headSet(deliveryTag - 1).clear();
} else {
confirmSet.remove(deliveryTag);
}
// 处理消息重发的业务逻辑
}
});
while (true) {
TimeUnit.SECONDS.sleep(10);
long nextSqlNo = channel.getNextPublishSeqNo();
String message = "confirm message " + nextSqlNo;
channel.basicPublish("demo.direct.exchange", "demo.routingKey",
MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
confirmSet.add(nextSqlNo);
}