RabbitMQ技術學習與應用

RabbitMQ是一個 異步通信中間件,可以在高併發下實現消峯限流,能實現消息的解耦,引入消息隊列可以不用等待消息處理完成可以繼續往下執行,不影響主要步驟的同步執行。在高併發下可以將任務放進隊列中,讓程序從隊列中取出執行,而不是崩潰或者直接拒絕,起到消峯限流

重要組件

隊列分消費者、生產者、以及隊列,生產者將消息發送到隊列中,消費者從隊列中取出消息消費,生產者也就是客戶端支持不同的工作而模式

  • connection:connection底層長連接,:一個對象對應一個進程
  • channel:可以頻繁的銷燬和創建,對應一次使用;
  • queue:存放消息的容器,需要綁定交換機,每生成一個隊列默認情況下都會綁定一個AMQP default的路由類型交換機“”也就是空字符,並且以隊列的名稱綁定路由;

服務器界面介紹

開啓服務並打開連接http://127.0.0.1:15672/通過默認賬戶 guest/guest 登錄
在這裏插入圖片描述

  • overview:看到當前運行狀態,加載使用的各種文件(日誌,數據)
  • connections:顯示程序連接,顯示來源的ip地址
  • channels:基於某個或某幾個connection創建的短連接
  • exchanges:交換機的對象
    • 默認每個用戶一旦綁定了virTualHost就會創建7個默認的交
      換機對象,2個路由,2個topic 2個headers 1 fanout;默認的
      路由交換機AMQP default:所有的隊列生成是自動綁定
  • queue:隊列內容,包括顯示消息數量,處理數量,可處理數量,未處理數量,總數量,可以拿到不同的隊列中的消息內容;
  • admin:對操作rabbitmq的用戶權限做管理,自定義用戶密碼登錄

添加用戶
在這裏插入圖片描述
用戶角色

  • 超級管理員(administrator)
    可登陸管理控制檯,可查看所有的信息,並且可以對用戶,策略(policy)進行操作。
  • 監控者(monitoring)
    可登陸管理控制檯,同時可以查看rabbitmq節點的相關信息(進程數,內存使用情況,磁盤使用情況等)
  • 策略制定者(policymaker)
    可登陸管理控制檯, 同時可以對policy進行管理。但無法查看節點的相關信息(上圖紅框標識的部分)。
  • 普通管理者(management)
    僅可登陸管理控制檯,無法看到節點信息,也無法對策略進行管理。
  • 其他
    無法登陸管理控制檯,通常就是普通的生產者和消費者。

創建Virtual Hosts
rabbitmq中每一個用戶如果登錄後想要操作資源(交換機,隊列等)需要綁定一個有權限的虛擬主機virtualHost,每個虛擬主機都會管理一批不同的資源,之間是互相隔離的;

在這裏插入圖片描述
在這裏插入圖片描述
看到權限已加:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

五種工作模式

在這裏插入圖片描述

簡單模式

在這裏插入圖片描述
生產端:綁定rabbitmq默認交換機,交換機名稱"",接收消息後根據消息中攜帶的路由key(隊列名稱) 找到綁定隊列發送消息,如果交
換機找不到對應名稱的隊列,以垃圾數據處理,扔到垃圾桶,強調的是一發一收
導入依賴

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>

定義連接工廠

 public class ConnectionUtil {
	public static Connection getConnection() throws Exception {
        //定義連接工廠
        ConnectionFactory factory = new ConnectionFactory();
        //設置服務地址
        factory.setHost("IP地址");
        //端口
        factory.setPort(5672);
        //設置賬號信息,用戶名、密碼、vhost
        factory.setVirtualHost("配置的host");
        factory.setUsername("用戶名");
        factory.setPassword("密碼");
        // 通過工程獲取連接
        Connection connection = factory.newConnection();
        return connection;
    }

}

控制層

@ResponseBody
	@GetMapping(value = "/mqsimple")
	public String simpleMq() {
		
		return rabbitService.simpleMq();
	}

服務層

服務層

AtomicInteger integer = new AtomicInteger();
	// 隊列名
	private final static String QUEUE_NAME = "hello";
	// 創建一個連接
//	@Autowired

	@Autowired
	ThreadPoolExecutor executor;
//假裝同時有一百次處理
	public String simpleMq() {
		for (int i = 0; i < 100; i++) {
			executor.execute(new RunnableRabbit());
		}
		//不要executor.shutdown(),因爲提交了任務,線程池關閉了,實際上任務放在隊列中未執行完畢,正真執行會報錯
//		executor.shutdown();
		return "發送成功,不用等到結果就能繼續執行";
	}

	private class RunnableRabbit implements Runnable {
		Channel channel;

//發送消息
		@Override
		public void run() {
			// 創建一個通道
			try {
				Connection connection = ConnectionUtil.getConnection();
				Channel channel = connection.createChannel();
				// 指定一個隊列
				// queueDeclare(String queue, boolean durable, boolean exclusive, boolean
				// autoDelete, Map<String, Object> arguments)
				// 參數1 queue :隊列名
				// 參數2 durable :是否持久化
				// 參數3 exclusive :僅創建者可以使用的私有隊列,斷開後自動刪除
				// 參數4 autoDelete : 當所有消費客戶端連接斷開後,是否自動刪除隊列
				// 參數5 arguments
				channel.queueDeclare(QUEUE_NAME, false, false, false, null);
				// 發送消息
				String message = "Hello World!";

				// basicPublish(String exchange, String routingKey, BasicProperties props,
				// byte[] body)
				// 參數1 exchange :交換器
				// 參數2 routingKey : 路由鍵
				// 參數3 props : 消息的其他參數
				// 參數4 body : 消息體
				// 簡單模式交換機爲“”,路由鍵默認是隊列名稱
				channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
				//每次休眠0.5秒
				Thread.sleep(500);
				integer.incrementAndGet();
				System.out.println(" [x] Sent '" + message + integer);
				//工具類用來關閉通道和連接
				RabbitUtil.closeRabbit(channel, connection);
			} catch (Exception e) {
				throw new RuntimeException(e.getMessage());
			}
		}

	}
	
//關閉通道和長連接
public class RabbitUtil {

	public static void closeRabbit(Channel channel, Connection connection) {
		try {
			channel.close();
		} catch (IOException | TimeoutException e) {
			channel = null;
		} finally {
			try {
				connection.close();
			} catch (IOException e) {
				connection = null;
			}
		}
	}
}

執行http://localhost:8093/mqsimple
會立即返回頁面信息
在這裏插入圖片描述
-----------------------------------------------------——————美麗的分割線————————---------------------------------------------------------------------
在這裏插入圖片描述
聲明隊列是冪等的, 隊列只會在它不存在時纔會被創建,多次聲明並不會重複創建。消息內容是一個字節數組,也就意味着可以傳遞任何數據。所以有300個消息放進了隊列且只有一個隊列無論發送多少條信息
點擊隊列名
在這裏插入圖片描述
可以看到每一條發送的信息
在這裏插入圖片描述
隊列狀態
在這裏插入圖片描述
使用的默認交換機,路由鍵爲隊列名
在這裏插入圖片描述
目前隊列有三百個信息等待處理

消費方
控制層


	@GetMapping(value = "/recv")
	public void	 recv() {
		rabbitService.recv();
		
	}

服務層

public void recv() {
		AtomicInteger integer = new AtomicInteger();
		try {
			Connection connection = ConnectionUtil.getConnection();
			Channel channel = connection.createChannel();
			// 指定一個隊列
			// queueDeclare(String queue, boolean durable, boolean exclusive, boolean
			// autoDelete, Map<String, Object> arguments)
			// 參數1 queue :隊列名
			// 參數2 durable :是否持久化
			// 參數3 exclusive :僅創建者可以使用的私有隊列,斷開後自動刪除
			// 參數4 autoDelete : 當所有消費客戶端連接斷開後,是否自動刪除隊列
			// 參數5 arguments
			channel.queueDeclare(QUEUE_NAME, false, false, false, null);
			System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

			
			while (true) {

				// 創建隊列消費者
				Consumer consumer = new DefaultConsumer(channel) {
					@Override
					public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
							byte[] body) throws UnsupportedEncodingException {
						String message = new String(body, "UTF-8");
						integer.incrementAndGet();
						System.out.println(" [x] Received '" + message+integer);

					}
				};
				// basicConsume(String queue, boolean autoAck, Consumer callback)
				// 參數1 queue :隊列名
				// 參數2 autoAck : 是否自動ACK
				// 參數3 callback : 消費者對象的一個接口,用來配置回調
				channel.basicConsume(QUEUE_NAME, true, consumer);

			}

		} catch (Exception e) {
			e.printStackTrace();
		}

		
	}

執行http://localhost:8093/recv

在這裏插入圖片描述
控制檯顯示300個信息已經被消費
在這裏插入圖片描述
消息已經被處理完畢

工作隊列

在這裏插入圖片描述
工作隊列,又稱任務隊列,主要思想是避免立即執行資源密集型任務,並且必須等待完成。相反地,我們進行任務調度,我們將一個任務封裝成一個消息,並將其發送到隊列。工作進行在後臺運行不斷的從隊列中取出任務然後執行。當你運行了多個工作進程時,這些任務隊列中的任務將會被工作進程共享執行。 這個概念在 Web 應用程序中特別有用,在短時間 HTTP 請求內需要執行復雜的任務。

編寫兩個消費者

//工作模式
	public void work1() throws Exception {
		
			Connection connection = ConnectionUtil.getConnection();
			Channel channel = connection.createChannel();
			// 指定一個隊列
			channel.queueDeclare(QUEUE_NAME, false, false, false, null);
			// 創建隊列消費者
			final Consumer consumer = new DefaultConsumer(channel) {
				@Override
				public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
						byte[] body) throws UnsupportedEncodingException {
					String message = new String(body, "UTF-8");
					integer2.incrementAndGet();
					System.out.println(" [x] Received '" + message + "工人one" + integer2);
			
				}
				
			};
			// 消息應答關閉
			boolean autoAck = true;
			channel.basicConsume(QUEUE_NAME, autoAck, consumer);


	}
public void work2() throws Exception {

		
			Connection connection = ConnectionUtil.getConnection();
			Channel channel = connection.createChannel();
			// 指定一個隊列
			channel.queueDeclare(QUEUE_NAME, false, false, false, null);
			// 創建隊列消費者
			final Consumer consumer = new DefaultConsumer(channel) {

				@Override
				public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
						byte[] body) throws UnsupportedEncodingException {
					String message = new String(body, "UTF-8");
					integer2.incrementAndGet();
					System.out.println(" [x] Received '" + message + "工人two" + integer2);
				
				}
			};
			// 消息應答關閉
			boolean autoAck = true;
			channel.basicConsume(QUEUE_NAME, autoAck, consumer);

		}

控制器代碼略,通過前臺啓動兩個消費者
在啓動發送者
在這裏插入圖片描述
結果是兩個消費者會交替一個一個執行,因爲在一個控制檯所以會有錯亂(反正是我的筆記博客),通過增加更多的工作程序就可以進行並行工作,並且接受的消息平均分配。注意的是,這種分配過程是一次性分配,並非一個一個分配。 默認情況下,RabbitMQ 將會發送的每一條消息按照順序給下一個消費者。平均每一個消費者將獲得相同數量的消息。這種分發消息的方式叫做輪詢調度

消息應答(Message acknowledgment)

當前代碼會存在一個問題,一旦 RabbitMQ 向客戶發送消息,它立即將其從內存中刪除。在這種情況下,如果某個工作者還沒執行完工作就死掉了,那麼就會丟失信息, 但是,我們不想失去任何消息。如果某個工作進程被殺死時,我們希望把這個任務交給另一個工作進程。 爲了確保消息永遠不會丟失,RabbitMQ 支持消息應答。從消費者發送一個確認信息告訴 RabbitMQ 已經收到消息並已經被接收和處理,然後RabbitMQ 可以自由刪除它。 如果消費者被殺死而沒有發送應答,RabbitMQ 會認爲該信息沒有被完全的處理,然後將會重新轉發給別的消費者。如果同時有其他消費者在線,則會迅速將其重新提供給另一個消費者。這樣就可以確保沒有消息丟失,即使工作進程偶爾也會死亡。 默認情況下,消息應答是開啓的。在前面的例子中,通過 autoAck = true 標誌明確地將它們關閉。現在是一旦完成任務,將此標誌設置爲false ,並向工作進程發送正確的確認。
發送端

修改消費者代碼

public void work1() throws Exception {

		Connection connection = ConnectionUtil.getConnection();
		Channel channel = connection.createChannel();
		// 指定一個隊列
		channel.queueDeclare(QUEUE_NAME, false, false, false, null);
		// 創建隊列消費者
		final Consumer consumer = new DefaultConsumer(channel) {
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
					byte[] body) throws UnsupportedEncodingException {
				String message = new String(body, "UTF-8");
				integer2.incrementAndGet();
				System.out.println(" [x] Received '" + message + "工人one" + integer2);
				// 每次處理完成一個消息後,手動發送一次應答。
				try {
					channel.basicAck(envelope.getDeliveryTag(), false);
				} catch (IOException e) {
					e.printStackTrace();
				}
			}

		};
		// 消息應答關閉
//		boolean autoAck = true;
		// 消息應答關開啓
		boolean autoAck = false;
		channel.basicConsume(QUEUE_NAME, autoAck, consumer);

	}

其中,首先關閉自動應答機制。

	// 消息應答關閉
//		boolean autoAck = true;
		// 消息應答關開啓
		boolean autoAck = false;
		channel.basicConsume(QUEUE_NAME, autoAck, consumer);

然後,每次處理完成一個消息後,手動發送一次應答。

channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

這樣消費者每處理一條信息就會應答給生成者,生產者就會刪除信息,如果沒有應答,生成者就會重新發送給可用的消費者

公平轉發(Fair dispatch)

但是目前的代碼還是有問題,調度仍然無法正常工作。例如在兩個工作線程的情況下,一個工作線程將不斷忙碌,另一個工作人員幾乎不會做任何工作。那麼,RabbitMQ 不知道什麼情況,還會平均分配消息。 這是因爲當消息進入隊列時,RabbitMQ 只會分派消息。它不看消費者的未確認消息的數量。它只是盲目地向第 n 個消費者發送每個第 n 個消息。 爲了解決這樣的問題,我們可以使用 basicQos 方法,並將傳遞參數爲 prefetchCount = 1。 這樣告訴 RabbitMQ 不要一次給一個工作線程多個消息。換句話說,在處理並確認前一個消息之前,不要向工作線程發送新消息。相反,它將發送到下一個還不忙的工作線程。

修改生產者代碼如下:

try {
				Connection connection = ConnectionUtil.getConnection();
				Channel channel = connection.createChannel();
				// 指定一個隊列
				// queueDeclare(String queue, boolean durable, boolean exclusive, boolean
				// autoDelete, Map<String, Object> arguments)
				// 參數1 queue :隊列名
				// 參數2 durable :是否持久化
				// 參數3 exclusive :僅創建者可以使用的私有隊列,斷開後自動刪除
				// 參數4 autoDelete : 當所有消費客戶端連接斷開後,是否自動刪除隊列
				// 參數5 arguments
				channel.queueDeclare(QUEUE_NAME, false, false, false, null);
				 // 公平轉發 
				int prefetchCount = 1;
				channel.basicQos(prefetchCount) ;
				// 發送消息
				String message = "Hello World!";

				// basicPublish(String exchange, String routingKey, BasicProperties props,
				// byte[] body)
				// 參數1 exchange :交換器
				// 參數2 routingKey : 路由鍵
				// 參數3 props : 消息的其他參數
				// 參數4 body : 消息體
				// 簡單模式交換機爲“”,路由鍵默認是隊列名稱
				channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
				integer.incrementAndGet();
				System.out.println(" [x] Sent '" + message + integer);
				// 工具類用來關閉通道和連接
				RabbitUtil.closeRabbit(channel, connection);
			} catch (Exception e) {
				e.printStackTrace();
			}

其中,使用 basicQos 方法,並將傳遞參數爲 prefetchCount = 1。

int prefetchCount = 1;
channel.basicQos(prefetchCount) ;
消息持久化(Message durability)

現在確保即使消費者死了,任務也不會丟失。但是如果 RabbitMQ 服務器停止,任務仍然會丟失。 當 RabbitMQ 退出或崩潰時,將會丟失所有的隊列和信息,除非告訴它不要丟失。需要兩件事來確保消息不丟失:需要分別將隊列和消息標記爲持久化。 首先,需要確保 RabbitMQ 永遠不會失去隊列。爲了這樣做,需要將其聲明爲持久化的。

再次修改生產者代碼:

// 創建一個通道
			try {
				Connection connection = ConnectionUtil.getConnection();
				Channel channel = connection.createChannel();
				// 指定一個隊列
				// queueDeclare(String queue, boolean durable, boolean exclusive, boolean
				// autoDelete, Map<String, Object> arguments)
				// 參數1 queue :隊列名
				// 參數2 durable :是否持久化
				// 參數3 exclusive :僅創建者可以使用的私有隊列,斷開後自動刪除
				// 參數4 autoDelete : 當所有消費客戶端連接斷開後,是否自動刪除隊列
				// 參數5 arguments
				//持久化隊列參數
				boolean durable = true;
				channel.queueDeclare(QUEUE_NAME, durable, false, false, null);
				 // 公平轉發 
				int prefetchCount = 1;
				channel.basicQos(prefetchCount) ;
				// 發送消息
				String message = "Hello World!";

				// basicPublish(String exchange, String routingKey, BasicProperties props,
				// byte[] body)
				// 參數1 exchange :交換器
				// 參數2 routingKey : 路由鍵
				// 參數3 props : 消息的其他參數
				// 參數4 body : 消息體
				// 簡單模式交換機爲“”,路由鍵默認是隊列名稱
				
				//MessageProperties.PERSISTENT_TEXT_PLAIN----消息持久化
				channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
				integer.incrementAndGet();
				System.out.println(" [x] Sent '" + message + integer);
				// 工具類用來關閉通道和連接
				RabbitUtil.closeRabbit(channel, connection);
			} catch (Exception e) {
				e.printStackTrace();
			}

消費端

// 指定一個隊列,第二個參數爲持久化隊列
		channel.queueDeclare(QUEUE_NAME, true, false, false, null);

在這裏插入圖片描述

現在我們先啓動生成者然後關閉rabbitmq服務器記得之前要換個隊列名,換個隊列玩,不然會報錯
在這裏插入圖片描述
現在我們重啓服務器
在這裏插入圖片描述
啓動一個消費者後:
在這裏插入圖片描述
9個小時後隊列還在
在這裏插入圖片描述

發佈訂閱模式

在這裏插入圖片描述
解讀:

  • 1個生產者,多個消費者
  • 每一個消費者都有自己的一個隊列
  • 生產者沒有將消息直接發送到隊列,而是發送到了交換機
  • 每個隊列都要綁定到交換機
  • 生產者發送的消息,經過交換機,到達隊列,實現,一個消息被多個消費者獲取的目的注意:一個消費者隊列可以有多個消費者實例,只有其中一個消費者實例會消費

交換器一共四種類型:direct、topic、headers、fanout。目前,先關注 fanout 類型的交換器。

channel.exchangeDeclare(“logs”, “fanout”);

fanout 類型的交換器非常簡單,它只是將所有收到的消息廣播到所有它所知道的隊列。不知道交換器,但仍然能夠發送消息到隊列。這是因爲使用了一個默認的交換器,用空的字符串(“”)。

// 參數1 exchange :交換器
// 參數2 routingKey : 路由鍵
// 參數3 props : 消息的其他參數
// 參數4 body : 消息體
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());

其中,第一個參數表示交換器的名稱,我們設置爲””,第二個參數表示消息由路由鍵決定發送到哪個隊列。 現在,我們可以發佈消息到我們命名的交換器。

channel.basicPublish("logs", "", null, message.getBytes());

	```

**臨時隊列(Temporary queues)**
之前,都是使用的隊列指定了一個特定的名稱。不過現在並不關心隊列的名稱。想要接收到所有的消息,而且也只對當前正在傳遞的消息感興趣。要解決需求,需要做兩件事。 首先,每當連接到 RabbitMQ,我們需要一個新的空的隊列。爲此,可以創建一個具有隨機名稱的隊列,或者甚至更好 - 讓服務器或者,讓服務器爲選擇一個隨機隊列名稱。 其次,一旦消費者與 RabbitMQ 斷開,消費者所接收的那個隊列應該被自動刪除。 在 Java 客戶端中,可以使用 queueDeclare() 方法來創建一個非持久的、唯一的、自動刪除的隊列,且隊列名稱由服務器隨機產生。

```java
String queueName = channel.queueDeclare().getQueue();

此時,queueName 包含一個隨機隊列名稱。
綁定(Bindings)
在這裏插入圖片描述
已經創建了一個 fanout 類型的交換器和隊列。現在,需要告訴交換器發送消息到我們的隊列。交換器和隊列之間的關係稱爲綁定

// 綁定交換器和隊列
// 參數1 queue :隊列名
// 參數2 exchange :交換器名
// 參數3 routingKey :路由鍵名
channel.queueBind(queueName, "logs", "");

代碼演示
發送端


	public String pubor() throws Exception {
		Connection connection = ConnectionUtil.getConnection();
		Channel channel = connection.createChannel();
	    // 指定一個交換器
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        // 發送消息  
        String message = "Liang-MSG log.";  
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());  
        System.out.println(" [x] Sent '" + message + "'");  
		return "發佈成功請訂閱";
	}

接收端

public String puborw1() throws Exception {
		Connection connection = ConnectionUtil.getConnection();
		Channel channel = connection.createChannel();
		 // 指定一個交換器
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        // 創建一個非持久的、唯一的、自動刪除的隊列
        String queueName = channel.queueDeclare().getQueue();
        // 綁定交換器和隊列
        // queueBind(String queue, String exchange, String routingKey)
        // 參數1 queue :隊列名
        // 參數2 exchange :交換器名
        // 參數3 routingKey :路由鍵名
        channel.queueBind(queueName, EXCHANGE_NAME, "");
        // 創建隊列消費者
        final 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(" [x] Received work1 '" + message + "'");
            }
        };
        channel.basicConsume(queueName, true, consumer);

		return "訂閱成功";
	}

	

	public String puborw2() throws Exception {
		Connection connection = ConnectionUtil.getConnection();
		Channel channel = connection.createChannel();
		 // 指定一個交換器
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        // 創建一個非持久的、唯一的、自動刪除的隊列
        String queueName = channel.queueDeclare().getQueue();
        // 綁定交換器和隊列
        // queueBind(String queue, String exchange, String routingKey)
        // 參數1 queue :隊列名
        // 參數2 exchange :交換器名
        // 參數3 routingKey :路由鍵名
        channel.queueBind(queueName, EXCHANGE_NAME, "");
        // 創建隊列消費者
        final 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(" [x] Received work2'" + message + "'");
            }
        };
        channel.basicConsume(queueName, true, consumer);

		return "訂閱成功";
	}

先啓動兩個接收端,然後啓動發送端
在這裏插入圖片描述
幾乎所有接收端同時獲得並處理相同的消息,直就是發佈與訂閱,發佈給已經訂閱的隊列,交換機通過路由鍵確定發送到哪個隊列

路由模式

直接交換(Direct exchange)是通過直接交換機實現的,發送端將消息發給交換機,通過路由鍵路要到相應的隊列,在這裏插入圖片描述
其中,第一個隊列與綁定鍵 orange 綁定,第二個隊列有兩個綁定,一個綁定鍵爲 black,另一個綁定爲 green。在這樣的設置中,具有 orange 的交換器的消息將被路由到隊列 Q1。具有 black 或 green 的交換器的消息將轉到 Q2。所有其他消息將被丟棄。

多重綁定(Multiple bindings)
在這裏插入圖片描述
此外,使用相同的綁定鍵綁定多個隊列是完全合法的。在我們的示例中,可以在 X 和 Q1 之間添加綁定鍵 black。在這種情況下,direct 類型的交換器將消息廣播到所有匹配的隊列 Q1 和 Q2。

代碼演示:
發送端

public class EmitLogDirect {
    private static final String EXCHANGE_NAME = "direct_logs";
    private static final String[] LOG_LEVEL_ARR = {"debug", "info", "error"};  

    public static void main(String[] args) throws IOException, TimeoutException {
        // 創建連接
        ConnectionFactory factory = new ConnectionFactory();
        // 設置 RabbitMQ 的主機名
        factory.setHost("localhost");
        // 創建一個連接
        Connection connection = factory.newConnection();
        // 創建一個通道
        Channel channel = connection.createChannel();    
        // 指定一個交換器
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        // 發送消息  
        for (int i = 0; i < 10; i++)  {  
            int rand = new Random().nextInt(3);
            String severity  = LOG_LEVEL_ARR[rand];
            String message = "Liang-MSG log : [" +severity+ "]" + UUID.randomUUID().toString();  
            // 發佈消息至交換器
            channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes());  
            System.out.println(" [x] Sent '" + message + "'");  
        }  
        // 關閉頻道和連接  
        channel.close();
        connection.close();
    }
}

接收端

public class ReceiveLogsDirect {
    private static final String EXCHANGE_NAME = "direct_logs";
    private static final String[] LOG_LEVEL_ARR = {"debug", "info", "error"};  

    public static void main(String[] args) throws IOException, TimeoutException {
        // 創建連接
        ConnectionFactory factory = new ConnectionFactory();
        // 設置 RabbitMQ 的主機名
        factory.setHost("localhost");
        // 創建一個連接
        Connection connection = factory.newConnection();
        // 創建一個通道
        Channel channel = connection.createChannel();
        // 指定一個交換器
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        // 設置日誌級別
        int rand = new Random().nextInt(3);
        String severity  = LOG_LEVEL_ARR[rand];
        // 創建一個非持久的、唯一的、自動刪除的隊列
        String queueName = channel.queueDeclare().getQueue();
        // 綁定交換器和隊列
        // queueBind(String queue, String exchange, String routingKey)
        // 參數1 queue :隊列名
        // 參數2 exchange :交換器名
        // 參數3 routingKey :路由鍵名
        channel.queueBind(queueName, EXCHANGE_NAME, severity);
        // 創建隊列消費者
        final 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(" [x] Received '" + message + "'");
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}

現在,做一個實驗,開啓三個 ReceiveLogsDirect 工作程序:ReceiveLogsDirect1 、ReceiveLogsDirect2 與 ReceiveLogsDirect3。
ReceiveLogsDirect1

[*] Waiting for messages. To exit press CTRL+C
[*] LOG LEVEL : info
[x] Received 'Liang-MSG log : [info]0dd0ae0c-bf74-4aa9-9852-394e65fbf939'
[x] Received 'Liang-MSG log : [info]b2b032f6-e907-4c95-b676-1790204c5f73'
[x] Received 'Liang-MSG log : [info]14482461-e432-4866-9eb5-a28f4edeb47f'

ReceiveLogsDirect2

[*] Waiting for messages. To exit press CTRL+C
[*] LOG LEVEL : error
[x] Received 'Liang-MSG log : [error]493dce2a-7ce1-4111-953c-99ab2564a2d0'
[x] Received 'Liang-MSG log : [error]2446dd80-d5f0-4d39-888f-31579b9d2724'
[x] Received 'Liang-MSG log : [error]fe8219e0-5548-40ba-9810-d922d1b03dd8'
[x] Received 'Liang-MSG log : [error]797b6d0e-9928-4505-9c76-56043322b1f0'

ReceiveLogsDirect3

[*] Waiting for messages. To exit press CTRL+C
[*] LOG LEVEL : debug
[x] Received 'Liang-MSG log : [debug]c05eee3e-b820-4b69-9c3f-c2bbded85195'
[x] Received 'Liang-MSG log : [debug]4645c9ba-4070-41d7-adc9-7f8b2df1e3c8'
[x] Received 'Liang-MSG log : [debug]d3d3ad5c-8f97-49ea-8fd6-c434790e40eb'

此時,ReceiveLogsDirect1 、ReceiveLogsDirect2 與 ReceiveLogsDirect3 同時收到了屬於自己級別的消息。

主題模式(通配符模式)

主題交換(Topic exchange)
使用 topic 類型的交換器,不能有任意的綁定鍵,它必須是由點隔開的一系列的標識符組成。標識符可以是任何東西,但通常它們指定與消息相關聯的一些功能。其中,有幾個有效的綁定鍵,例如 “stock.usd.nyse”,“nyse.vmw”, “quick.orange.rabbit”。可以有任何數量的標識符,最多可達 255 個字節。 topic 類型的交換器和 direct 類型的交換器很類似,一個特定路由的消息將被傳遞到與匹配的綁定鍵綁定的匹配的所有隊列。關於綁定鍵有兩種有兩個重要的特殊情況:

* 可以匹配一個標識符。
# 可以匹配零個或多個標識符。

在這裏插入圖片描述
消息將使用由三個字(兩個點)組成的綁定鍵發送。綁定鍵中的第一個字將描述速度,第二個顏色和第三個種類:“…”。其中, Q1 對所有的橙色動物感興趣。而 Q2 想聽聽有關兔子的一切,以及關於懶惰動物的一切。 如果我們違反合同併發送一個或四個字的消息,如 “quick.orange.male.rabbit” 會發生什麼?那麼,這些消息將不會匹配任何綁定,並將被丟失。 topic 類型的交換器是強大的,可以實現其他類型的交換器。 當一個隊列與“#”綁定綁定鍵時,它將接收所有消息,類似 fanout 類型的交換器。 當一個隊列與 “*” 和 “#” 在綁定中不被使用時,類似 direct 類型的交換器。

同一個消息被多個消費者獲取。一個消費者隊列可以有多個消費者實例,只有其中一個消費者實例會消費到消息。

代碼演示
發送端

public class EmitLogTopic {
    private static final String EXCHANGE_NAME = "topic_logs";
    private static final String[] LOG_LEVEL_ARR = {"dao.debug", "dao.info", "dao.error",
            "service.debug", "service.info", "service.error",
            "controller.debug", "controller.info", "controller.error"};

    public static void main(String[] args) throws IOException, TimeoutException {
        // 創建連接
        ConnectionFactory factory = new ConnectionFactory();
        // 設置 RabbitMQ 的主機名
        factory.setHost("localhost");
        // 創建一個連接
        Connection connection = factory.newConnection();
        // 創建一個通道
        Channel channel = connection.createChannel();    
        // 指定一個交換器
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        // 發送消息  
        for (String severity : LOG_LEVEL_ARR) {
            String message = "Liang-MSG log : [" +severity+ "]" + UUID.randomUUID().toString();  
            // 發佈消息至交換器
            channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes());  
            System.out.println(" [x] Sent '" + message + "'");  
        }  
        // 關閉頻道和連接  
        channel.close();
        connection.close();
    }
}

接收端

public class ReceiveLogsTopic {
    private static final String EXCHANGE_NAME = "topic_logs";
    private static final String[] LOG_LEVEL_ARR = {"#", "dao.error", "*.error", "dao.*", "service.#", "*.controller.#"};  

    public static void main(String[] args) throws IOException, TimeoutException {
        // 創建連接
        ConnectionFactory factory = new ConnectionFactory();
        // 設置 RabbitMQ 的主機名
        factory.setHost("localhost");
        // 創建一個連接
        Connection connection = factory.newConnection();
        // 創建一個通道
        Channel channel = connection.createChannel();
        // 指定一個交換器
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        // 設置日誌級別
        int rand = new Random().nextInt(5);
        String severity  = LOG_LEVEL_ARR[rand];
        // 創建一個非持久的、唯一的、自動刪除的隊列
        String queueName = channel.queueDeclare().getQueue();
        // 綁定交換器和隊列
        channel.queueBind(queueName, EXCHANGE_NAME, severity);
        // 打印
        System.out.println(" [*] LOG INFO : " + severity);
        // 創建隊列消費者
        final 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(" [x] Received '" + message + "'");
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}

接收端只會匹配到相應的鍵纔會消費,沒有被匹配的鍵的消息將會被丟棄

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章