目录
消息确认机制
在之前的工作模式中,我们会发现,所有的生产者在推送完消息后就结束或者执行其他任务,并不知晓消息是否发送成功。如果要保证消息的可靠性,需要对消息进行持久化处理。除了设置持久化相关代码外,我们还要保证消息是被推送到代理服务器(broker)里。正常情况下,如果消息经过交换机进入队列就可以完成消息的持久化,但如果消息在没有到达broker之前出现意外,那就造成消息丢失。
RabbitMQ有两种方式来解决这个问题:
- 事务模式
- Confirm模式
事务模式
- channel.txSelect();声明事务模式
- channel.txCommit();提交事务
- chnnel.txRollback();回滚事务
生产者
public class Producer {
private static final String QUEUE_NAME = "queue_tx";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.txSelect();
try {
channel.basicPublish("", QUEUE_NAME, null, "hello1".getBytes());
channel.basicPublish("", QUEUE_NAME, null, "hello2".getBytes());
// 模拟错误
int i = 1/0;
channel.txCommit();
} catch (Exception e) {
channel.txRollback();
} finally {
channel.close();
connection.close();
}
}
}
执行之后,我们会发现执行的推送代码里的两条消息,在消费者并没有接收到。
Confirm模式
通过设置生产者Channel为comfirm模式,该Channel上发布的所有消息都会被指派一个唯一ID(每次从1开始累加),当消息到达生产者指定的消息队列后,broker会返回一个确认给生产者(包含之前的ID),这样生产者就能知道哪条消息成功发送了。
1、普通Confirm模式
每发送一条消息,就调用waitForConfirms()方法接收确认消息,返回ture成功,返回false或超时未返回代表失败。使用waitForConfirmsOrDie()失败时会抛出异常。
- channel.confirmSelect();声明Confirm模式(如果Channel已经被设置为事务模式,是不能修改的)
- channel.waitForConfirms();接收确认,括号内可设置超时时间
生产者
public class Producer {
private static final String QUEUE_NAME = "queue_confirm";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.confirmSelect();
for (int i = 0; i < 10; i++) {
channel.basicPublish("", QUEUE_NAME, null, ("hello" + i).getBytes());
if (channel.waitForConfirms()) {
System.out.println("成功发送");
} else {
// 重新发送
}
}
channel.close();
connection.close();
}
}
普通Confirm模式由于waitForConfirms()方法内部使用了同步代码块,是串行化的,每发一条消息就要等待服务器的确认,会阻止后续消息的发布。因此会降低吞吐量,带来效率的下降。
2、批量Confirm模式
批量Confirm模式是指在一批消息发完之后,再调用waitForConfirms()方法,等待服务器确认,大大提高了confirm的吞吐量。但是这一批消息出问题时我们不能定位是哪条消息出问题了,因此所有消息都要重发,也可能会带来效率的下降。
另外,还能够使用waitForConfirmsOrDie()方法来等待服务器确认,该方法会等到最后一条消息得到确认或者得到nack才会结束,也就是说在waitForConfirmsOrDie处会造成当前程序的阻塞。
for (int i = 0; i < 500; i++) {
channel.basicPublish("", QUEUE_NAME, null, ("hello" + i).getBytes());
}
if (channel.waitForConfirms()) {
System.out.println("成功发送");
} else {
// 重新发送
}
------------------或者-------------------
try {
channel.waitForConfirmsOrDie();
} catch (Exception e) {
// 重新发送
}
3、异步Confirm模式
之前的普通和批量模式,实际上都是同步的,会降低服务器的吞吐量。并且假设我们还想针对每个消息进行特定处理时。我们可以通过向Channel添加confirm的监听器来实现异步Confirm模式。这时我们需要自己去维护每个消息的唯一ID(deliveryTag)。
public class Producer {
private static final String QUEUE_NAME = "queue_confirm_async";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.confirmSelect();
// 存放还未确认消息对应的唯一ID
SortedSet<Long> unConfirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
channel.confirmSelect();
//添加监听器
channel.addConfirmListener(new ConfirmListener() {
/**
* // 消息发送确认的回调
* @param deliveryTag 消息唯一ID
* @param multiple 是否是批量确认,如果是true,代表包含当前返回的deliveryTag之前的所有消息都已确认
* @throws IOException
*/
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
/**
* 多条批量清除
* headSet()返回的是指定元素之前的所有元素,因为deliveryTag是累加的,加1为了将其本身包含进来
* clear()从集合中清除headSet所返回的元素
*/
unConfirmSet.headSet(deliveryTag + 1L).clear();
System.out.println("批量清除:" + deliveryTag + "以前的消息," + "剩余" + unConfirmSet.toString());
} else {
// 若是单条确认,再集合中删除对应id
unConfirmSet.remove(deliveryTag);
System.out.println("单独清除:" + deliveryTag + "消息," + "剩余" + unConfirmSet.toString());
}
}
// 消息发送失败的回调
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
// 重发逻辑
}
});
for(int i = 0; i < 10; i++){
// 在发布之前使用getNextPublishSeqNo()获取消息对应唯一ID
long nextSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish("", QUEUE_NAME, null, (" Confirm模式, 第" + (i + 1) + "条消息").getBytes());
// 添加到未确认集合中
unConfirmSet.add(nextSeqNo);
System.out.println("新增消息后剩余:" + unConfirmSet.toString());
}
}
}
从执行结果我们可以看出,消息的发送和服务器确认是同时异步的,同时进行的。这样就能达到最佳性能和资源使用。
总结
- 分别发布消息,同步等待确认:简单,但吞吐量非常有限。
- 批量发布消息,同步等待批量确认:简单,合理的吞吐量,但是很难推断出什么时候出了问题。
- 异步处理:最佳性能和资源使用,在出现错误的情况下可以很好地控制。