參考:
導入RabbitMQ的依賴:
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>3.4.1</version>
</dependency>
一、簡單隊列模式
圖示:
P:代表生產者
紅色方塊:代表隊列
C:代表消費者
創建RabbitMQ連接工具類:
public class ConnectionUtil {
public static void main(String[] args) throws Exception {
System.out.println(getConnection());
}
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");
return factory.newConnection();
}
}
1.發送消息
public class Send {
private final static String QUEUE_NAME = "q_test_01";
public static void main(String[] argv) throws Exception {
// 獲取到連接以及mq通道
Connection connection = ConnectionUtil.getConnection();
// 從連接中創建通道
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//消息內容
String message = "Hello World";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("[x] send " + message + "");
//關閉通道
channel.close();
//關閉連接
connection.close();
}
}
2.接收消息
public class Recv {
private final static String QUEUE_NAME = "q_test_01";
public static void main(String[] argv) throws Exception {
// 獲取到連接以及mq通道
Connection connection = ConnectionUtil.getConnection();
// 從連接中創建通道
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 定義隊列的消費者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 監聽隊列
channel.basicConsume(QUEUE_NAME, true, consumer);
// 獲取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
}
}
}
二、Work模式
圖示:
1個生產者,1個隊列,2個消費者
1.發送消息
public class Send {
private final static String QUEUE_NAME = "test_queue_work";
public static void main(String[] args) throws Exception {
//獲取到連接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
for (int i = 0; i < 100; i++) {
//消息內容
String message = "" + i;
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
Thread.sleep(10);
}
channel.close();
connection.close();
}
}
2.接受消息
消費者1:
public class Recv1 {
private final static String QUEUE_NAME = "test_queue_work";
public static void main(String[] args) throws Exception {
//獲取到連接
Connection connection = ConnectionUtil.getConnection();
//獲取到mq通道
Channel channel = connection.createChannel();
//聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//同一時刻服務器只會發送一條消息給消費者
channel.basicQos(1);
//定義隊列的消費者
QueueingConsumer consumer = new QueueingConsumer(channel);
//監聽隊列,false表示手動返回完成狀態,true表示自動
channel.basicConsume(QUEUE_NAME, true, consumer);
//獲取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println("[y] Received" + message);
//休眠
Thread.sleep(10);
//返回確認狀態,註釋掉表示使用自動確認模式
// channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
消費者2:
public class Recv2 {
private final static String QUEUE_NAME = "test_queue_work";
public static void main(String[] argv) throws Exception {
// 獲取到連接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 同一時刻服務器只會發一條消息給消費者
//隊列只有在收到消費者發回的上一條消息 ack 確認後,纔會向該消費者發送下一條消息。
channel.basicQos(1);
// 定義隊列的消費者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 監聽隊列,false表示手動返回完成狀態,true表示自動
channel.basicConsume(QUEUE_NAME, false, consumer);
// 獲取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
//休眠1秒
Thread.sleep(10000);
//下面這行註釋掉表示使用自動確認模式,只有手動ack確認後,隊列纔會推送下一條消息到消費者
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
備註:
1.消費者1開啓了ack自動確認,
2.消費者2關閉了ack自動確認,使用的是手動ack確認,只有隊列收到了上N(N爲設置的Qos值)條消息的ack確認後纔會發送下N條消息。
3.怎樣才能做到按照每個消費者的能力分配消息呢?聯合使用 Qos 和 Acknowledge 就可以做到。
basicQos 方法設置了當前信道最大預獲取(prefetch)消息數量爲1。消息從隊列異步推送給消費者,消費者的 ack 也是異步發送給隊列,從隊列的視角去看,總是會有一批消息已推送但尚未獲得 ack 確認,Qos 的 prefetchCount 參數就是用來限制這批未確認消息數量的。設爲1時,隊列只有在收到消費者發回的上一條消息 ack 確認後,纔會向該消費者發送下一條消息。prefetchCount 的默認值爲0,即沒有限制,隊列會將所有消息儘快發給消費者。
4.消息的確認模式
消費者從隊列中獲取消息,服務端如何知道消息已經被消費呢?
4.1 模式1:自動確認
只要消息從隊列中獲取,無論消費者獲取到消息後是否成功消息,都認爲是消息已經成功消費。
4.2 模式2:手動確認
消費者從隊列中獲取消息後,服務器會將該消息標記爲不可用狀態,等待消費者的反饋,如果消費者一直沒有反饋,那麼該消息將一直處於不可用狀態。
開啓手動ack確認:
5.兩種分發方式
5.1 輪詢分發 :
使用任務隊列的優點之一就是可以輕易的並行工作。如果我們積壓了好多工作,我們可以通過增加工作者(消費者)來解決這一問題,使得系統的伸縮性更加容易。在默認情況下,RabbitMQ將逐個發送消息到在序列中的下一個消費者(而不考慮每個任務的時長等等,且是提前一次性分配,並非一個一個分配)。平均每個消費者獲得相同數量的消息。這種方式分發消息機制稱爲Round-Robin(輪詢)。
使用輪詢分發:兩個消費者的Qos值設爲相同的,關閉手動應答,改爲自動應答。
5.2 公平分發 :
雖然上面的分配法方式也還行,但是有個問題就是:比如:現在有2個消費者,所有的奇數的消息都是繁忙的,而偶數則是輕鬆的。按照輪詢的方式,奇數的任務交給了第一個消費者,所以一直在忙個不停。偶數的任務交給另一個消費者,則立即完成任務,然後閒得不行。而RabbitMQ則是不瞭解這些的。這是因爲當消息進入隊列,RabbitMQ就會分派消息。它不看消費者爲應答的數目,只是盲目的將消息發給輪詢指定的消費者。
使用公平分發:關閉自動應答,改爲手動應答。
測試數據:
消費者1:休眠10ms(能力強)
消費者2:休眠100ms(能力弱)
消費者1和消費者2都開啓了手動ack確認,消費者1消費了81條消息,消費者2消費了19條數據,消息順序沒規律。(公平分發)
消費者1和消費者2都開啓了自動ack確認,消費者1消費了50條消息,消費者2消費了50條數據,奇偶輪詢。(輪詢分發)
三、訂閱模式
特點:
1.1個生產者,多個消費者
2.每個消費者都有自己的一個隊列
3.生產者沒有將消息直接發送到隊列,而是發送到了交換機
4.每個隊列都要綁定到交換機
5.生產者發送的消息,經過交換機,到達隊列,實現一個消息被多個消費者獲取
發送消息:
public class Send {
private final static String EXCHANGE_NAME = "test_exchange_fanout";
public static void main(String[] args) throws Exception {
//獲取到連接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//聲明exchange,exchange一旦創建就不能改變,而服務器端創建了exchange,客戶端也創建一遍就會報錯
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//消息內容
String message = "Hello World";
//注意:消息發送到沒有隊列綁定的交換機時,消息將丟失,因爲,交換機沒有存儲消息的能力,消息只能存在在隊列中。
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println("sent " + message);
channel.close();
connection.close();
}
}
接受消息:
消費者1:
public class Recv1 {
private final static String QUEUE_NAME = "test_queue_work1";
private final static String EXCHANGE_NAME = "test_exchange_fanout";
public static void main(String[] args) throws Exception {
//獲取到連接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//綁定隊列到交換機
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
//同一時刻服務器只會發送一條消息給消費者
channel.basicQos(1);
//定義隊列的消費者
QueueingConsumer consumer = new QueueingConsumer(channel);
//監聽隊列,手動返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
//獲取消息
while (true) {
//消費者接受隊列推送過來的消息
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
消費者2:
public class Recv2 {
private final static String QUEUE_NAME = "test_queue_work2";
private final static String EXCHANGE_NAME = "test_exchange_fanout";
public static void main(String[] argv) throws Exception {
// 獲取到連接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 綁定隊列到交換機
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
// 同一時刻服務器只會發一條消息給消費者
channel.basicQos(1);
// 定義隊列的消費者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 監聽隊列,手動返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
// 獲取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv2] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
備註:
1.一個消費者隊列可以有多個消費者實例,只有其中一個消費者實例會消費
2.在創建exchange時,一旦創建就不能改變,而服務端創建了exchange,客戶端也創建一遍就會報錯。
3.消息發送到沒有隊列綁定的交換機時,消息將丟失,因爲,交換機沒有存儲消息的能力,消息只能存在在隊列中。
在管理工具中查看隊列和交換機的綁定關係:
交換機類型爲:fanout(廣播式交換機)
1.這種模式不需要指定Routing key路由鍵,一個交換機可以綁定多個隊列queue,一個queue可同時與多個exchange交換機進行綁定;
2.如果消息發送到交換機上,但是這個交換機上面沒有綁定的隊列,那麼這些消息將會被丟棄;
四、路由模式
交換機類型爲:direct(直連交換機)
說明:
1.任何發送到Direct Exchange的消息都會被轉發到指定RouteKey中指定的隊列Queue;
2.生產者生產消息的時候需要執行Routing Key路由鍵;
3.隊列綁定交換機的時候需要指定Binding Key,只有路由鍵與綁定鍵相同的話,才能將消息發送到綁定這個隊列的消費者;
4.如果vhost中不存在RouteKey中指定的隊列名,則該消息會被丟棄。
發送消息:
public class Send {
private final static String EXCHANGE_NAME = "test_exchange_direct";
public static void main(String[] args) throws Exception {
//獲取到連接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//聲明exchange,交換機類型爲:direct
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
//消息內容
String message = "新增商品";
// 消息的key爲:delete
channel.basicPublish(EXCHANGE_NAME, "insert", null, message.getBytes());
System.out.println("[x] sent " + message);
channel.close();
connection.close();
}
}
接受消息:
消費者1:
public class Recv1 {
private final static String QUEUE_NAME = "test_queue_direct_1";
private final static String EXCHANGE_NAME= "test_exchange_direct";
public static void main(String[] args) throws Exception {
//獲取到連接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//聲明隊列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//綁定隊列到交換機
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"delete");
//同一時刻服務器只會發送一條消息給消費者
channel.basicQos(1);
//定義隊列的消費者
QueueingConsumer consumer = new QueueingConsumer(channel);
//監聽隊列,手動返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
//獲取消息
while (true) {
//消費者接受隊列推送過來的消息
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
消費者2:
public class Recv2 {
private final static String QUEUE_NAME = "test_queue_direct_2";
private final static String EXCHANGE_NAME = "test_exchange_direct";
public static void main(String[] args) throws Exception {
//獲取到連接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//綁定隊列到交換機
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
//同一時刻服務器只會發送一條消息給消費者
channel.basicQos(1);
//定義隊列的消費者
QueueingConsumer consumer = new QueueingConsumer(channel);
//監聽隊列,手動返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
//獲取消息
while (true) {
//消費者接受隊列推送過來的消息
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
五、主題模式(通配符模式)
交換機類型爲:topic
1.任何發送到Topic Exchange的消息都會被轉發到所有滿足Route Key與Binding Key模糊匹配的隊列Queue上;
2.生產者發送消息的時候需要指定Route Key,同時綁定Exchange與Queue的時候也需要指定Binding Key;
3.#” 表示0個或多個關鍵字,“*”表示匹配一個關鍵字;
4.如果Exchange沒有發現能夠與RouteKey模糊匹配的隊列Queue,則會拋棄此消息;
5.如果Binding中的Routing key *,#都沒有,則路由鍵跟綁定鍵相等的時候才轉發消息,類似Direct Exchange;
6.如果Binding中的Routing key爲#或者#.#,則全部轉發,類似Fanout Exchange;
發送消息:
public class Send {
private final static String EXCHANGE_NAME = "test_exchange_topic";
public static void main(String[] argv) throws Exception {
// 獲取到連接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 聲明exchange
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
// 消息內容
String message = "Hello World!!";
channel.basicPublish(EXCHANGE_NAME, "routekey.1", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
channel.close();
connection.close();
}
}
接受消息:
消費者1:
public class Recv1 {
private final static String QUEUE_NAME = "test_queue_topic_work_1";
private final static String EXCHANGE_NAME = "test_exchange_topic";
public static void main(String[] argv) throws Exception {
// 獲取到連接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 綁定隊列到交換機
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "routekey.*");
// 同一時刻服務器只會發一條消息給消費者
channel.basicQos(1);
// 定義隊列的消費者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 監聽隊列,手動返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
// 獲取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv_x] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
消費者2:
public class Recv2 {
private final static String QUEUE_NAME = "test_queue_topic_work_2";
private final static String EXCHANGE_NAME = "test_exchange_topic";
public static void main(String[] argv) throws Exception {
// 獲取到連接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 綁定隊列到交換機
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.*");
// 同一時刻服務器只會發一條消息給消費者
channel.basicQos(1);
// 定義隊列的消費者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 監聽隊列,手動返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
// 獲取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv2_x] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
備註:
同一條消息可以發送到多個隊列上,一個隊列上只有一個消費者實例會消費消息。
六:RPC模式
RPC的處理流程:
1.當客戶端啓動時,創建一個匿名的回調隊列。
2.客戶端爲RPC請求設置2個屬性:replyTo,設置回調隊列名字;correlationId,標記request。
3.請求被髮送到rpc_queue隊列中。
4.RPC服務器端監聽rpc_queue隊列中的請求,當請求到來時,服務器端會處理並且把帶有結果的消息發送給客戶端。接收的隊列就是replyTo設定的回調隊列。
5.客戶端監聽回調隊列,當有消息時,檢查correlationId屬性,如果與request中匹配,那就是結果了。
RPC服務端;
public class RPCServer {
private static final String RPC_QUEUE_NAME = "rpc_queue";
//具體處理方法
private static int fib(int n) {
if (n == 0)
return 0;
if (n == 1)
return 1;
return fib(n - 1) + fib(n - 2);
}
public static void main(String[] argv) throws Exception {
//從工廠中獲取連接
Connection connection = ConnectionUtil.getConnection();
//從連接中獲取信道
final Channel channel = connection.createChannel();
//name:隊列名字
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
channel.basicQos(1);
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
System.out.println(" [x] Awaiting RPC requests");
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
BasicProperties props = delivery.getProperties();
// BasicProperties replyProps = new BasicProperties.Builder().correlationId(props.getCorrelationId()).build();
AMQP.BasicProperties replyProps = new AMQP.BasicProperties.Builder()
.correlationId(props.getCorrelationId()).build();
String message = new String(delivery.getBody());
int n = Integer.parseInt(message);
System.out.println(" [.] fib(" + message + ")");
String repsonse = "" + fib(n);
channel.basicPublish("", props.getReplyTo(), replyProps, repsonse.getBytes());
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
RPC客戶端:
public class RPCClient {
private Connection connection;
private Channel channel;
private String requestQueueName = "rpc_queue";
private String replyQueueName;
public RPCClient() throws Exception {
//建立一個連接和一個通道,併爲回調聲明一個唯一的'回調'隊列
connection = ConnectionUtil.getConnection();
channel = connection.createChannel();
//定義一個臨時變量的接受隊列名
replyQueueName = channel.queueDeclare().getQueue();
}
//發送RPC請求
public String call(String message) throws IOException, InterruptedException {
//生成一個唯一的字符串作爲回調隊列的編號
String corrId = UUID.randomUUID().toString();
//發送請求消息,消息使用了兩個屬性:replyto和correlationId
//服務端根據replyto返回結果,客戶端根據correlationId判斷響應是不是給自己的
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder().correlationId(corrId).replyTo(replyQueueName)
.build();
//發佈一個消息,requestQueueName路由規則
channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));
//由於我們的消費者交易處理是在單獨的線程中進行的,因此我們需要在響應到達之前暫停主線程。
//這裏我們創建的 容量爲1的阻塞隊列ArrayBlockingQueue,因爲我們只需要等待一個響應。
final BlockingQueue<String> response = new ArrayBlockingQueue<String>(1);
// String basicConsume(String queue, boolean autoAck, Consumer callback)
channel.basicConsume(replyQueueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
//檢查它的correlationId是否是我們所要找的那個
if (properties.getCorrelationId().equals(corrId)) {
//如果是,則響應BlockingQueue
response.offer(new String(body, "UTF-8"));
}
}
});
return response.take();
}
public void close() throws IOException {
connection.close();
}
public static void main(String[] argv) {
RPCClient fibonacciRpc = null;
String response = null;
try {
fibonacciRpc = new RPCClient();
System.out.println(" [x] Requesting fib(30)");
response = fibonacciRpc.call("30");
System.out.println(" [.] Got '" + response + "'");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fibonacciRpc != null) {
try {
fibonacciRpc.close();
} catch (IOException _ignore) {
}
}
}
}
}