一、理論基礎
1.1、什麼是消息確認機制
RabbitMQ在傳遞消息過程中充當了代理人(broker)角色,生產者發送消息到代理服務器broker默認情況下是不會返回任何消息給生產者的,生產者不知道消息有沒有正常到達代理服務器。MQ提供了兩種方式實現消息確認:
AMQP事務模式、將信道channel設置爲Confirm模式
1.2、爲什麼要使用消息確認機制
都知道通過持久化(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