消息應答與消息持久化
消息應答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
boolean autoAck = true;(自動確認模式)
一旦 Rabbit MQ 將消息分發給消費者,就會從內存中刪除,這種情況下,如果殺死正在執行的消費者,就會丟失正在處理的消息
boolean autoAck = false;(手動模式)
如果有一個消費者掛掉,就會交付給其它消費者,Rabbit MQ 支持消息應答,消費者發送一個消息應答,告訴 Rabbit MQ 這個消息我已經處理完成,你可以刪了,然後 Rabbit MQ 就刪除內存中的消息
消息應答默認是打開的,false
Message acknowledgment
大家想想如果我們的 Rabbit MQ 掛了,我們的消息仍然會丟失
消息的持久化
boolean durable = false;
channel.queueDeclare(QUEUE_NAME, durable, false, false, null);
我們將程序中的 boolean durable = false; 改成 true; 是不可以的,儘管代碼是正確的,它也不會運行成功,因爲我們已經定義了一個叫 work_queue 的消息隊列,這個 queue 是未持久化的,Rabbit MQ 不允許重新定義(不同參數)一個已存在的隊列
訂閱模式 publish/subscribe
模型
解讀:
1.一個生產者,多個消費者
2.每一個消費者都有自己的隊列
3.生產者沒有直接把消息發送到隊列 而是發到了交換機 轉發器 exchange
4.每個隊列都要綁定到交換機上
5.生產者發送的消息,經過交換機,到達隊列,就能實現一個消息被多個消費者消費
註冊 -> 郵件 -> 短信
生產者
public class Send {
private final static String EXCHANGE_NAME = "exchange_fanout";
public static void main(String[] args) throws Exception {
// 獲取一個連接
Connection connection = ConnectionUtils.getConnection();
// 從連接中獲取一個通道
Channel channel = connection.createChannel();
// 聲明exchange
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 消息內容
String message = "hello ps!";
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println("[x] Send '" + message + "'");
channel.close();
connection.close();
}
}
消息哪去了???丟失了!!!因爲交換機沒有存儲的能力,在 Rabbit MQ 中只有隊列只有存儲能力,因爲這時候還沒有隊列綁定到交換機,所以數據丟失了
消費者 1
public class Receive1 {
private final static String QUEUE_NAME = "queue_work1";
private final static String EXCHANGE_NAME = "exchange_fanout";
public static void main(String[] args) throws Exception {
// 獲取一個連接
Connection connection = ConnectionUtils.getConnection();
// 從連接中獲取一個通道
Channel channel = connection.createChannel();
// 創建隊列聲明
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 綁定隊列到交換機
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
// 同一時刻服務器只會發一條消息給消費者
channel.basicQos(1);
// 保證一次只分發一個
channel.basicQos(1);
// 定義隊列的消費者
DefaultConsumer consumer = new DefaultConsumer(channel) {
/**
* 獲取到到達的消息
* @param consumerTag
* @param envelope
* @param properties
* @param body
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[1] recv msg:" + msg);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("[1] done");
// 手動回執
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
// 監聽隊列,自動應答改爲 false
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
}
}
消費者 2
public class Receive2 {
private final static String QUEUE_NAME = "queue_work2";
private final static String EXCHANGE_NAME = "exchange_fanout";
public static void main(String[] args) throws Exception {
// 獲取一個連接
Connection connection = ConnectionUtils.getConnection();
// 從連接中獲取一個通道
Channel channel = connection.createChannel();
// 創建隊列聲明
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 綁定隊列到交換機
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
// 同一時刻服務器只會發一條消息給消費者
channel.basicQos(1);
// 保證一次只分發一個
channel.basicQos(1);
// 定義隊列的消費者
DefaultConsumer consumer = new DefaultConsumer(channel) {
/**
* 獲取到到達的消息
* @param consumerTag
* @param envelope
* @param properties
* @param body
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[2] recv msg:" + msg);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("[2] done");
// 手動回執
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
// 監聽隊列,自動應答改爲 false
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
}
}
Exchange(交換機、轉發器)
一方面是接收生產者的消息,另一方面是向隊列推送消息
匿名轉發 “”
Fanout(不處理路由鍵)
Direct(處理路由鍵)
路由模式
模型
生產者
public class Send {
private static final String EXCHANGE_NAME = "exchange_direct";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
String msg = "hello direct!";
String routingKey = "error";
channel.basicPublish(EXCHANGE_NAME, routingKey, null, msg.getBytes());
System.out.println("send " + msg);
channel.close();
connection.close();
}
}
消費者 1
public class Receive1 {
private static final String EXCHANGE_NAME = "exchange_direct";
private static final String QUEUE_NAME = "queue_direct_1";
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.basicQos(1);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "error");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[1] recv msg:" + msg);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("[1] done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
}
}
消費者 2
public class Receive2 {
private static final String EXCHANGE_NAME = "exchange_direct";
private static final String QUEUE_NAME = "queue_direct_2";
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.basicQos(1);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "error");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "info");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "warning");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[2] recv msg:" + msg);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("[2] done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
}
}
Topic exchange
將路由鍵和某模式匹配
# 匹配一個或多個
* 匹配一個
Goods.#
模型
商品:發佈 修改 刪除 查詢
生產者
public class Send {
private final static String EXCHANGE_NAME = "exchange_topic";
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
String msgString = "商品...";
channel.basicPublish(EXCHANGE_NAME, "goods.add", null, msgString.getBytes());
System.out.println("---send " + msgString);
channel.close();
connection.close();
}
}
消費者 1
public class Receive1 {
private static final String EXCHANGE_NAME = "exchange_topic";
private static final String QUEUE_NAME = "queue_topic_1";
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.add");
channel.basicQos(1);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[1] recv msg:" + msg);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("[1] done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
}
}
消費者 2
public class Receive2 {
private static final String EXCHANGE_NAME = "exchange_topic";
private static final String QUEUE_NAME = "queue_topic_2";
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");
channel.basicQos(1);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[2] recv msg:" + msg);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("[2] done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
}
}
Rabbit MQ 的消息確認機制(事務 + confirm)
在 Rabbit MQ 中,我們可以持久化數據解決 Rabbit MQ 服務器異常的數據丟失問題
問題:生產者將消息發送出去之後,消息到底有沒有到達 Rabbit MQ 服務器,默認的情況是不知道的
兩種方式:
1.AMQP 實現了事務機制
2.Confirm 模式
事務機制
txSelect txCommit txRollBack
txSelect:用戶將當前 channel 設置成 transaction 模式
txCommit:用於提交事務
txRollBack:回滾事務
生產者
public class TxSend {
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);
String msgString = "hello tx message";
try {
channel.txSelect();
channel.basicPublish("", QUEUE_NAME, null, msgString.getBytes());
System.out.println("send " + msgString);
channel.txCommit();
} catch (Exception e) {
channel.txRollback();
System.out.println("send message txRollBack");
} finally {
channel.close();
connection.close();
}
}
}
消費者
public class TxReceive {
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.basicConsume(QUEUE_NAME, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("[recv] tx msg:" + new String(body, "utf-8"));
}
});
}
}
此種模式還是很耗時的,採用這種方式降低了 Rabbit MQ 的消息吞吐量
Confirm 模式
生產者端 confirm 模式的實現原理
生產者將信道設置成 confirm 模式,一旦信道進入 confirm 模式,所有在該信道上面發佈的消息,都會被指派爲一個唯一的 ID(從 1 開始),一旦消息被投遞到所有匹配的隊列之後,broker 就會發送一個確認給生產者(包含消息的唯一 ID),這就使得生產者知道消息已經正確到達目的隊列了,如果消息和隊列是可持久化的,那麼確認消息會將消息寫入磁盤之後發出,broker 回傳給生產者的確認消息中,deliver-tag 域中包含了確認消息的序列號,此外,broker 也可以設置 basic.ack 的 multiple 域,表示到這個序列號之前的所有消息都已經得到了處理
Confirm 模式最大的好處在於它是異步的
Nack
開啓 confirm 模式
channel.confirmSelect()
編程模式:
1.普通:發一條 waitForConfirms()
2.批量的:發一批 waitForConfirms()
3.異步 confirm 模式:提供一個回調方法
Confirm 單條
public class Send1 {
private static final String QUEUE_NAME = "queue_confirm1";
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();
String msgString = "hello confirm message!";
channel.basicPublish("", QUEUE_NAME, null, msgString.getBytes());
if (!channel.waitForConfirms()) {
System.out.println("message send failed");
} else {
System.out.println("message send ok");
}
channel.close();
connection.close();
}
}
批量
public class Send2 {
private static final String QUEUE_NAME = "queue_confirm2";
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();
String msgString = "hello confirm message!";
for (int i = 0; i < 10; i++) {
channel.basicPublish("", QUEUE_NAME, null, msgString.getBytes());
}
if (!channel.waitForConfirms()) {
System.out.println("message send failed");
} else {
System.out.println("message send ok");
}
channel.close();
connection.close();
}
}
異步模式
Channel 對象提供的 confirmListener() 回調方法只包含 deliveryTag(當前 Channel 發出的消息序號),我們需要自己爲每一個 Channel 維護一個 unconfirm 的消息序號集合,每 publish 一條數據,集合中元素加 1,沒回調一次 handlerAck() 方法,unconfirm 集合刪掉相應的一條(multiple=false)或多條(multiple=true)記錄,從程序運行效率上看,這個 unconfirm 集合最好採用有序集合 SortedSet 存儲結構
public class Send3 {
private static final String QUEUE_NAME = "queue_confirm3";
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.confirmSelect();
final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long l, boolean b) throws IOException {
if (b) {
System.out.println("---handleAck---multiple");
confirmSet.headSet(l + 1).clear();
} else {
System.out.println("---handleAck---multiple false");
confirmSet.remove(l);
}
}
@Override
public void handleNack(long l, boolean b) throws IOException {
if (b) {
System.out.println("---handleNack---multiple");
confirmSet.headSet(l + 1).clear();
} else {
System.out.println("---handleNack---multiple false");
confirmSet.remove(l);
}
}
});
String msgStr = "ssssss";
while (true) {
long seqNo = channel.getNextPublishSeqNo();
channel.basicPublish("", QUEUE_NAME, null, msgStr.getBytes());
confirmSet.add(seqNo);
}
}
}