一、理论基础
1.1、什么是消息确认机制
RabbitMQ在传递消息过程中充当了代理人(broker)角色,生产者发送消息到代理服务器broker默认情况下是不会返回任何消息给生产者的,生产者不知道消息有没有正常到达代理服务器。MQ提供了两种方式实现消息确认:
AMQP事务模式、将信道channel设置为Confirm模式
1.2、为什么要使用消息确认机制
RabbitMQ这个中间件默认的一个行为,就是只要仓储服务收到一个订单消息,RabbitMQ就会立马把这条订单消息给标记为删除,这个行为叫做自动ack,也就是投递完成一条消息就自动确认这个消息处理完毕了。
但是接着如果此时服务收到了消息,结果直接就宕机了,就会有问题。要关闭自动Ack的行为,不要自作主张的认为消息处理成功了,需要在业务逻辑手动ACK(RocketMQ、Kafka还是RabbitMQ,都有类似的autoAck或者是手动ack的机制)
此时,明显这个订单消息就丢失了啊,因为RabbitMQ那里已经没有了。。
都知道通过持久化(MQ设置的持久化和redis实现的数据持久化)来保障服务器崩溃时重启服务数据不会丢失。但是无法保障生产者将消息发送出去后到底有没有正确到达代理服务器broker,如果在到达broker之前数据已经丢失,则redis持久化也解决不了问题。只能通过消息确认机制。
保证消息可靠性以及消息防丢,完整的是需要三个地方保障:生产者(消息确认机制)、MQ(MQ持久化)、消费者中(手动Ack应答),具体可参考:MQ中保证消息可靠性
二、开启Confirm模式
2.1、在生产者中——开启Confirm
原理:在每次写消息都会分配一个唯一id,如果写入RabbitMQ中它会回传一个ack消息告诉这个消息ok;如果RabbitMQ没能处理这个消息则会回调生产者的nack接口告诉这个消息失败,你可以重试。
- —— ack ,表示消息成功送达broker并被broker接收
- —— nack,broker拒收消息,原因很多种,比如 队列已满,消息限流,IO异常等
RabbitTemplate已经帮封装好了,直接调用“setConfirmCallback(this)”即可。
原生角度讲,开启Confirm通过“channel.confirmSelect();”开启,实现confiem模式有三种编程方式:
(1)普通confirm模式,每发送一条消息,调用waitForConfirms()方法等待服务端confirm;
通过for循环调用Channel的basicPublish方法发送了5条消息到消息队列中,调用waitForConfirms方法等待broker服务端返回ack或者nack消息,这种模式每发送一条消息就会等待broker代理服务器返回消息。
public class ProducerTest {
public static void main(String[] args) {
String exchangeName = "confirmExchange";
String queueName = "confirmQueue";
String routingKey = "confirmRoutingKey";
String bindingKey = "confirmRoutingKey";
int count = 5;
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("172.16.151.74");
factory.setUsername("test");
factory.setPassword("test");
factory.setPort(5672);
//创建生产者
Sender producer = new Sender(factory, count, exchangeName, queueName,routingKey,bindingKey);
producer.run();
}
}
class Sender
{
private ConnectionFactory factory;
private int count;
private String exchangeName;
private String queueName;
private String routingKey;
private String bindingKey;
public Sender(ConnectionFactory factory,int count,String exchangeName,String queueName,String routingKey,String bindingKey) {
this.factory = factory;
this.count = count;
this.exchangeName = exchangeName;
this.queueName = queueName;
this.routingKey = routingKey;
this.bindingKey = bindingKey;
}
public void run() {
Channel channel = null;
try {
Connection connection = factory.newConnection();
channel = connection.createChannel();
//创建exchange
channel.exchangeDeclare(exchangeName, "direct", true, false, null);
//创建队列
channel.queueDeclare(queueName, true, false, false, null);
//绑定exchange和queue
channel.queueBind(queueName, exchangeName, bindingKey);
channel.confirmSelect();
//发送持久化消息
for(int i = 0;i < count;i++)
{
//第一个参数是exchangeName(默认情况下代理服务器端是存在一个""名字的exchange的,
//因此如果不创建exchange的话我们可以直接将该参数设置成"",如果创建了exchange的话
//我们需要将该参数设置成创建的exchange的名字),第二个参数是路由键
channel.basicPublish(exchangeName, routingKey,MessageProperties.PERSISTENT_BASIC, ("第"+(i+1)+"条消息").getBytes());
if(channel.waitForConfirms())
{
System.out.println("发送成功");
}
}
final long start = System.currentTimeMillis();
System.out.println("执行waitForConfirmsOrDie耗费时间: "+(System.currentTimeMillis()-start)+"ms");
} catch (Exception e) {
e.printStackTrace();
}
}
}
(2)批量confirm模式,每发送一批消息之后,调用waitForConfirmsOrDie方法,等待服务端confirm;
waitForConfirmsOrDie()方法作用:该方法会等到最后一条消息得到确认或者得到nack才会结束,也就是说在waitForConfirmsOrDie处会造成当前程序的阻塞。
channel.confirmSelect();
//发送持久化消息
for(int i = 0;i < count;i++)
{
//第一个参数是exchangeName(默认情况下代理服务器端是存在一个""名字的exchange的,
//因此如果不创建exchange的话我们可以直接将该参数设置成"",如果创建了exchange的话
//我们需要将该参数设置成创建的exchange的名字),第二个参数是路由键
channel.basicPublish(exchangeName, routingKey,MessageProperties.PERSISTENT_BASIC, ("第"+(i+1)+"条消息").getBytes());
}
long start = System.currentTimeMillis();
channel.waitForConfirmsOrDie();
System.out.println("执行waitForConfirmsOrDie耗费时间: "+(System.currentTimeMillis()-start)+"ms");
(3)异步Confirm模式:channel.addConfirmListener() 异步监听发送方确认模式
采用的是Channel信道的waitForConfirmsOrDie等待broker端回传回ack确认消息的,但我们没法拿到这个ack消息进行后期操作。要想拿到ack消息的话,我们可以给当前Channel信道绑定监听器,具体来说就是调用Channel信道的addConfirmListener方法进行设置,Channel信道在收到broker的ack消息之后会回调设置在该信道监听器上的handleAck(ack调用)方法,在收到nack消息之后会回调设置在该信道监听器上的handleNack(nack调用)方法
channel.confirmSelect();
//发送持久化消息
for(int i = 0;i < count;i++)
{
//第一个参数是exchangeName(默认情况下代理服务器端是存在一个""名字的exchange的,
//因此如果不创建exchange的话我们可以直接将该参数设置成"",如果创建了exchange的话
//我们需要将该参数设置成创建的exchange的名字),第二个参数是路由键
channel.basicPublish(exchangeName, routingKey,MessageProperties.PERSISTENT_BASIC, ("第"+(i+1)+"条消息").getBytes());
}
long start = System.currentTimeMillis();
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("nack: deliveryTag = "+deliveryTag+" multiple: "+multiple);
}
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("ack: deliveryTag = "+deliveryTag+" multiple: "+multiple);
}
});
System.out.println("执行waitForConfirmsOrDie耗费时间: "+(System.currentTimeMillis()-start)+"ms");
调用waitForConfirmsOrDie会造成程序的阻塞,通过监听器并不会造成程序的阻塞
2.2、在消费者中——手动ACK处理
关闭RabbitMQ自动ack,然后每次确定代码处理完后在程序里ack一下。这样如果没处理完就没有ack,那RabbitMQ就知道还没处理完,就会把这个消费给其他消费者,从而不会丢失
// 手动确认消息
channel.basicAck(envelope.getDeliveryTag(), false);
// 关闭自动确认
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
channel.basicAck():
参数:
deliveryTag:该消息的index
multiple:是否批量处理.true:将一次性ack所有小于deliveryTag的消息
void basicAck(long deliveryTag, boolean multiple) throws IOException;
channel.basicNack():
参数:
deliveryTag:该消息的index
multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息
requeue:被拒绝的是否重新入队列 注意:如果设置为true ,则会添加在队列的末端
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;
三、AMQP事务机制模式
原理:
生产者发送数据之前开启RabbitMQ事物channel.txSelect,然后再发送消息;如果消息没有成功被RabbitMQ成功接收,生产者会受到异常报错,此时可以回滚事物channel.txRollback,然后重试发送消息;如果RabbitMQ收到了消息,可以提交事物channel.txCommit。
- txSelect :将当前channel设置为transaction模式
- txCommit :提交当前事务
- txRollback :事务回滚
// 开启事务
channel.txSelect
try {
// 这里发送消息
} catch (Exception e) {
channel.txRollback
// 这里再次重发这条消息
}
// 提交事务
channel.txCommit
只有消息成功被broker接收事务提交才能成功,否则我们便可以在捕获异常进行事务回滚操作同时进行消息重发,
使用事务机制会降低RabbitMQ的性能,慎用!
参考:
https://www.rabbitmq.com/confirms.html
https://blog.csdn.net/hzw19920329/article/details/54315940
https://blog.csdn.net/RuiKe1400360107/article/details/102588176