目錄
rabbitMQ提供一下幾種工作模式:
- 簡單隊列
- 工作隊列
- 發佈訂閱
- 路由
- 話題
- RPC
- 發佈者確認
1.工具類抽取
爲了方便代碼能直接使用,先將代碼中用到的公告部分抽取取來。這個工具欄的作用很簡單,就是回去一個Connection,代碼如下:
public class ConnectionUtil {
public static Connection getConnection() throws Exception {
// 定義連接工廠
ConnectionFactory factory = new ConnectionFactory();
// 設置服務地址
factory.setHost("localhost");
// 端口
factory.setPort(5672);
// 設置賬號信息,用戶名、密碼、vhost
factory.setVirtualHost("testhost");
factory.setUsername("admin");
factory.setPassword("admin");
// 通過工程獲取連接
Connection connection = factory.newConnection();
return connection;
}
}
2.簡單隊列
簡單隊列,就如名字一樣,簡單到這個隊列,只有一個生產者,只對應一個消費者了。
2.1消息生產者
public boolean sendMessage(String queueName, String message) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 從連接中創建通道
Channel channel = connection.createChannel();
// 聲明(創建)隊列
channel.queueDeclare(queueName, false, false, false, null);
// 消息內容
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
// 關閉通道和連接
channel.close();
connection.close();
return false;
}
聲明隊列的時候,有四個參數,應爲後面經常出現,現在提前說明一下這四個參數的含義:
channel.queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
Map<String, Object> arguments);
- queue:我們使用的隊列名稱
- durable:是否將消息持久化;假如我們在創建queue的時候沒有指定其持久化,後期不可以在代碼中修改其爲持久化隊列,只能刪除重建。rabbitmq不允許重新定義(不同參數)一個已存在的隊列
- exclusive:是否聲明爲獨佔隊列
- autoDelete:消息投遞到消費者後,server上是否自動刪除消息
- arguments:隊列的配置信息
2.2消息消費者
消費者消費信息的時候,對服務器有兩種應答方式:自動應答,手動應答
自動應答:當隊列服務器將消息投遞給消費者後,不管消費者是否成功處理,都自行將隊列刪除。
手動應答:當隊列服務器將消息投遞給消費者後,需要消費者確認是否在服務器上刪除該消息。
2.2.1客戶端自動應答
/**
* 客戶端自動應答消費消息
*/
public void reciveByAutoMode(String queueName) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 從連接中創建通道
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(queueName, false, false, false, null);
// 定義消費者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("receiveMessage:" + message);
}
};
// 監聽隊列
channel.basicConsume(queueName, true, consumer);
}
代碼中監聽隊列的第二個參數設置爲true,表示自動應答。
basicConsume(String queue, boolean autoAck, Consumer callback)
2.2.2客戶端手動應答
/**
* 客戶端手動應答
*/
public void reciveByHandleMode(String queueName) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 從連接中創建通道
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(queueName, false, false, false, null);
// 定義消費者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("receiveMessage:" + message);
// 程序確認,而不是rabbit的自動確認(手動回執)
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 監聽隊列,改成false則表示手動應答
channel.basicConsume(queueName, false, consumer);
}
手動應答,首先需要將監聽隊列的第二個參數設置爲false,
basicConsume(String queue, boolean autoAck, Consumer callback)
同時,在consumer中,需要手動發送一個確認信息。
channel.basicAck(envelope.getDeliveryTag(), false);
3.工作隊列
消息生產者可以不斷的向服務器發送消息,但是消費者不一定能夠及時或者快速處理掉消息,這就會導致消息的積壓。
工作隊列的出現,在某種程度上降低了消息積壓發生的可能性。
工作隊列,通過多個消費者消費同一個隊列的數據,加快了消息的處理速度。
同時,隊列提供了兩種消息消費類型:公平消費,輪訓消費
公平消費:服務器根據消費者的能力來投遞消息,在消費者沒有完成對消息的處理時,不會再次發送一條消息至消費者。
輪訓消費:服務器將消息輪訓投遞給消費者,如果消費者有一個處理能力較差,會導致最後的壓力都集中在能力差的機器上。
生產中,我們毫無疑問的提倡公平分發消費。
3.1消息生產者
public boolean sendMessage2WorkQueue(String queueName, List<String> messages) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 從連接中創建通道
Channel channel = connection.createChannel();
// 聲明(創建)隊列
channel.queueDeclare(queueName, false, false, false, null);
for (String message : messages) {
//發送消息
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
}
// 此行代碼做公平分發用
// 每個消費者發送確認消息之前,消息隊列不發送下一個消息到消費者,一次只處理一個消息
// 限制發送給同一個消費者不得超過一條消息
int prefetchCount = 1;
channel.basicQos(prefetchCount);
// 關閉通道和連接
channel.close();
return true;
}
3.2消息消費者
/**
* 客戶端手動應答
* 當採用公平分發的時候,消費端要關閉自動應答,改爲手動應答
*
* @param queueName
* @throws Exception
*/
public void reciveByHandleMode(String queueName) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 從連接中創建通道
final Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(queueName, false, false, false, null);
// 定義消費者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("receiveMessage:" + message);
// 程序確認,而不是rabbit的自動確認(手動回執)
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 監聽隊列,改成false則表示手動應答
channel.basicConsume(queueName, false, consumer);
}
4.發佈訂閱
從這開始接觸一個新的概念:交換機
交換機相當於一個路由,對生產者發來的消息,根據配置進行路由,其本身並不存儲消息。在rabbitmq中,只有隊列能存儲消息。
路由的方式有三種:fanout(對應發佈訂閱模式),direct(對應路由模式),topic(對應主題模式)
- fanout:發佈訂閱模式,顧名思義,像扇子一樣(扇出),對綁定到該交換機的所有隊列,都能收到消息。此時routingKey不生效。
- direct:路由模式,對綁定到該交換機的隊列,通過routingKey匹配,只有完全匹配,交換機纔會將消息路由到該隊列。
- topic:主題模式,將路由和某個模式進行匹配(規則匹配, #:匹配一個或者多個; *:匹配一個)。匹配上的隊列,交換機纔會路由消息至該隊列
1.一個生產者,多個消費者 2. 每一個消費者都有自己的隊列 </br> 3. 生產者沒有直接把消息發送到隊列,而是發到了交換機(轉發器 exchange) 4. 每個隊列都要綁定到交換機上 5. 生產者發送的消息,經過交換機,到達隊列,就能實現一個消息被多個消費者消費
4.1消息生產者
public void sendMessage(String exchange, String message, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 從連接中創建通道
Channel channel = connection.createChannel();
// 聲明交換機(交換機沒有存儲的能力,在rabbitmq中只有隊列纔有存儲能力)
// fanout:不處理路由鍵,會將消息轉發給所有綁定該交換機的隊列,因此將routingKey設置爲""
// direct:處理路由鍵,將消息轉發給指定了routingKey的隊列上
channel.exchangeDeclare(exchange, "fanout");
// 發送消息
channel.basicPublish(exchange, "", null, message.getBytes());
// 關閉通道
channel.close();
connection.close();
}
4.2消息消費者
public void reciveByHandleMode(String exchange, String queueName, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 從連接中創建通道
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(queueName, false, false, false, null);
// 綁定到交換機
channel.queueBind(queueName, exchange, "");
// 可綁定多個
// channel.queueBind(queueName, exchange, routingKey);
// channel.queueBind(queueName, exchange, routingKey);
int prefetchCount = 1;
channel.basicQos(prefetchCount);
// 定義消費者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("receiveMessage:" + message);
// 程序確認,而不是rabbit的自動確認(手動回執)
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 監聽隊列,改成false則表示手動應答
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, consumer);
}
5.路由
5.1消息生產者
public void sendMessage(String exchange, String message, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 從連接中創建通道
Channel channel = connection.createChannel();
// 聲明交換機(交換機沒有存儲的能力,在rabbitmq中只有隊列纔有存儲能力)
// fanout:不處理路由鍵,會將消息轉發給所有綁定該交換機的隊列,因此將routingKey設置爲""
// direct:處理路由鍵,將消息轉發給指定了routingKey的隊列上
channel.exchangeDeclare(exchange, "direct");
// 發送消息
channel.basicPublish(exchange, routingKey, null, message.getBytes());
// 關閉通道
channel.close();
connection.close();
}
5.2消息消費者
/**
* 將隊列綁定到交換機
*
* @param exchange
* @param queueName
* @throws Exception
*/
public void reciveByHandleMode(String exchange, String queueName, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 從連接中創建通道
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(queueName, false, false, false, null);
// 綁定到交換機
channel.queueBind(queueName, exchange, routingKey);
// 可綁定多個
// channel.queueBind(queueName, exchange, routingKey);
// channel.queueBind(queueName, exchange, routingKey);
int prefetchCount = 1;
channel.basicQos(prefetchCount);
// 定義消費者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("receiveMessage:" + message);
// 程序確認,而不是rabbit的自動確認(手動回執)
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 監聽隊列,改成false則表示手動應答
channel.basicConsume(queueName, false, consumer);
}
6.話題
6.1消息生產者
public void sendMessage(String exchange, String message, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 從連接中創建通道
Channel channel = connection.createChannel();
// 聲明交換機(交換機沒有存儲的能力,在rabbitmq中只有隊列纔有存儲能力)
// fanout:不處理路由鍵,會將消息轉發給所有綁定該交換機的隊列,因此將routingKey設置爲""
// direct:處理路由鍵,將消息轉發給指定了routingKey的隊列上
//topic:將路由和某個模式進行匹配(規則匹配)。 #:匹配一個或者多個; *:匹配一個
channel.exchangeDeclare(exchange, "topic");
// 發送消息routingKey,such as :goods.add
channel.basicPublish(exchange, routingKey, null, message.getBytes());
// 關閉通道
channel.close();
}
6.2消息消費者
/**
* 將隊列綁定到交換機
*
* @param exchange
* @param queueName
* @throws Exception
*/
public void reciveByHandleMode(String exchange, String queueName, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 從連接中創建通道
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(queueName, false, false, false, null);
// 綁定到交換機,發送消息routingKey,such as :goods.#
channel.queueBind(queueName, exchange, routingKey);
int prefetchCount = 1;
channel.basicQos(prefetchCount);
// 定義消費者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("receiveMessage:" + message);
// 程序確認,而不是rabbit的自動確認(手動回執)
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 監聽隊列,改成false則表示手動應答
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, consumer);
}
7.RPC
8.發佈者確認
生產者將消息發送至broker以後,生產者並不知道消息是否真的正確達到了目標隊列,rabbitMQ當然不會這麼傻,它提供了兩種方式,來告知生產者,消息是否正確投遞出去了。他們分別是:事務機制,確認機制。
8.1事務機制
投遞前開啓事務,投遞結束後提交事務,若發送異常,則回滾事務。
public void createTransactionProducer(String exchange, String message, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 從連接中創建通道
Channel channel = connection.createChannel();
// 聲明交換機(交換機沒有存儲的能力,在rabbitmq中只有隊列纔有存儲能力)
// fanout:不處理路由鍵
// direct:處理路由鍵
channel.exchangeDeclare(exchange, "topic");
try {
// 發送消息routingKey,such as :goods.add
channel.txSelect();
channel.basicPublish(exchange, routingKey, null, message.getBytes());
channel.txCommit();
} catch (Exception e) {
channel.txRollback();
} finally {
// 關閉通道
channel.close();
}
}
8.2confirm機制
生產者將信道設置成confirm模式,一旦信道進入confirm模式,所有在該信道上發佈的消息都會被指派一個唯一的ID(默認從1開始), 一旦消息被投遞到所匹配的隊列之後,broker就會發送一個確認給生產者(包含消息的唯一ID),這就使得生產者知道消息已經正確的到達目標隊列了。 如果消息和隊列是可持久化的,那麼確認消息會在消息寫入磁盤之後發出,broker回傳給生產者的確認消息中deliver-tag域包含了確認消息的序列號。 此外,broker也可以設置basic.ack的multiple域,表示到這個序列號之前的所有消息都已經得到了處理。
8.2.1同步確認機制
/**
* @param exchange
* @param message
* @param routingKey
* @throws Exception
*/
public void createConfirmProducerSync(String exchange, String message, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 從連接中創建通道
Channel channel = connection.createChannel();
// 聲明交換機(交換機沒有存儲的能力,在rabbitmq中只有隊列纔有存儲能力)
// fanout:不處理路由鍵
// direct:處理路由鍵
channel.exchangeDeclare(exchange, "topic");
try {
// 發送消息routingKey,such as :goods.add
channel.confirmSelect();
channel.basicPublish(exchange, routingKey, null, message.getBytes());
if (!channel.waitForConfirms()) {
System.out.println("send message fail");
} else {
System.out.println("send message success");
}
} catch (Exception e) {
} finally {
// 關閉通道
channel.close();
}
}
8.2.2異步確認機制
public void createConfirmProducerAsync(String exchange, String message, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 從連接中創建通道
Channel channel = connection.createChannel();
// 聲明交換機(交換機沒有存儲的能力,在rabbitmq中只有隊列纔有存儲能力)
// fanout:不處理路由鍵
// direct:處理路由鍵
channel.exchangeDeclare(exchange, "topic");
// 發送消息routingKey,such as :goods.add
channel.confirmSelect();
final SortedSet<Long> confirmSets = Collections.synchronizedSortedSet(new TreeSet<Long>());
channel.addConfirmListener(new ConfirmListener() {
// 沒有問題的handleAck
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
confirmSets.headSet(deliveryTag + 1).clear();
} else {
confirmSets.remove(deliveryTag);
}
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
confirmSets.headSet(deliveryTag + 1).clear();
} else {
confirmSets.remove(deliveryTag);
}
}
});
channel.basicPublish(exchange, routingKey, null, message.getBytes());
// 關閉通道
channel.close();
}