- 生产者:消息从生产者生产发送给Broker
- 消息队列:消息在Broker存放,如果是镜像复制集群,消息将被复制到其他副本上
- 消费者:消费者从Broker上拉取信息,经过网络传输发送到Consumer
从上述通信过程可以分析,可能丢失消息一共有3种情况
如何确保消息不丢失
如何保证消息不丢失呢?如下图的解决方案即可
1. 消息在传入Broker过程中丢失
外界环境问题导致:发生网络丢包、网络故障等造成RabbitMQ Server端收不到消息,因为生产环境的网络是很复杂的,网络抖动,丢包现象很常见
解决方案 1:事务功能
RabbitMQ中与事务机制有关的方法有三个:txSelect(), txCommit()以及txRollback(), txSelect用于将当前channel设置成transaction模式,txCommit用于提交事务,txRollback用于回滚事务
// 开启事务
channel.txSelect
try {
// 这里发送消息
} catch (Exception e) {
channel.txRollback
// 这里再次重发这条消息
}
// 提交事务
channel.txCommit
缺点:
因为事务,吞吐量会下来,因为太耗性能。
解决方案 2:confirm功能
将信道设置成 confirm 模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的 ID 。一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一ID)。如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(not acknowledged,未确认)消息。
Confirm的三种实现方式:
方式一:channel.waitForConfirms()普通发送方确认模式;
方式二:channel.waitForConfirmsOrDie()批量确认模式;
方式三:channel.addConfirmListener()异步监听发送方确认模式;
方式一:普通Confirm模式
private static final String QUEUE_NAME="test_simple_queue";
Connection connection= ConnectionUtils.getConnection();
// 创建信道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 开启发送方确认模式
channel.confirmSelect();
String message = String.format("时间 => %s", new Date().getTime());
channel.basicPublish("", config.QUEUE_NAME, null, message.getBytes("UTF-8"));
if (channel.waitForConfirms()) {
System.out.println("消息发送成功" );
}
方式二:channel.waitForConfirmsOrDie()批量确认模式;
private static final String QUEUE_NAME="test_simple_queue";
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++) {
String message = String.format("时间 => %s", new Date().getTime());
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
}
channel.waitForConfirmsOrDie(); //直到所有信息都发布,只要有一个未确认就会IOException
System.out.println("全部执行完成");
方式三:channel.addConfirmListener()异步监听发送方确认模式;
private static final String QUEUE_NAME="test_simple_queue";
Connection connection= ConnectionUtils.getConnection();
// 创建信道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(config.QueueName, false, false, false, null);
// 开启发送方确认模式
channel.confirmSelect();
for (int i = 0; i < 10; i++) {
String message = String.format("时间 => %s", new Date().getTime());
channel.basicPublish("", config.QueueName, null, message.getBytes("UTF-8"));
}
//异步监听确认和未确认的消息
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("未确认消息,标识:" + deliveryTag);
}
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println(String.format("已确认消息,标识:%d,多个消息:%b", deliveryTag, multiple));
}
});
2. 消息在RabbitMQ中暂存内存,未消费就挂了丢失
解决方案 1:持久化
如何持久化?要想消息持久化,前提就是队列、Exchange都持久化
生产者,持久化交换机
消费者,持久化队列
这样rabbitMQ挂掉之后,交换机和队列以及消息就不会丢失了。
3.消费者消费到消息,还未处理就挂了
消息一旦被消费者接收,队列中的消息就会被删除,RabbitMQ是如何知道消息被接受的呢?
解决方案 :ACK机制
RabbitMQ有一个ACK机制,当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接受。
不过这种回执ACK分为两种情况
- 手动ACK:消息一旦被接受,消费者自动发送ACK
channel.basicAck(envelope.getDeliveryTag(),false);// 手动发送ack
boolean autoAck=false;// 关闭自动应答
channel.basicConsume(QUEUE_NAME,autoAck,consumer);// 开启监听队列
- 自动ACK:消息接收后,不会发送ACK,需要手动调用
boolean autoAck=true;// 开启自动应答
channel.basicConsume(QUEUE_NAME,autoAck,consumer);// 开启监听队列
我觉得手动ACK要好,因为,如果消费者领取消息,就自动回复了ACK,还没有执行操作就挂掉了,或者抛出异常,或者消息消费失败,RabbitMQ都无从得知,这样消息就丢失了,而手动ACK则可以让用户控制什么时候返回ACK,可以在确定消息消费成功之后,再调用发送ACK的方法。
使用场景
- 如果消息不太重要,丢失也没有什么影响,就可以使用自动ACK
- 如果消息非常重要,不容有失,那么最好在消费完成后手动ACK,否则接收到消息就自动ACK,RabbitMQ就会把消息从队列中删除造成信息丢失。
注意:
无论你选择手动ACK还是自动ACK,必须进行ACK回复,不然就会进行数据重发。