最近又要用到rabbitmq,業務上要求伺服器只發一次訊息,需要多個客戶端都去單獨消費。但我們知道rabbitmq的機制裡,每個佇列裡的訊息只能消費一次,所以客戶端要單獨消費資訊,就必須得每個客戶端單獨監聽一個queue。所以我最終想實現的是服務端只宣告exchange,客戶端來建立queue和繫結exchange。但是在看各種rabbitmq博文和討論的時候,我覺得對exchange的模式和queue間的關係講的都不是很清楚。所以我決定自己驗證一下
fanout模式和direct模式
本文主要驗證fanout模式和direct模式下以上猜想是否可行。fanout模式就是大名鼎鼎的廣播模式了,只要queue繫結了fanout的交換器,就可以直接的收到訊息,無需routingkey的參與。而direct模式就是通過routing key直接傳送到繫結了同樣routing key的佇列中。那麼,在這兩種exchange的模式下,是否都可以實現服務端僅建立exchange,客戶端建立queue並繫結exchange呢?
Direct模式驗證
我們先把交換器、routingkey、佇列的名稱定義好:
- 交換器為directTest
- routingkey為direct_routing_key
- 佇列測試3個,首先測試Direct_test_queue_1,再行測試Direct_test_queue_2,再行測試Direct_test_queue_3
程式碼使用spring boot框架快速搭建。我們先規劃好需要幾個類來完成這個事情:
- 針對生產者,需要RabbitmqConfig,用來配置exchange的
- 針對生產者,需要DirectRabbitSender,用來實現Direct模式的訊息傳送
- 針對消費者,需要DirectConsumerOne,來測試第一個佇列Direct_test_queue_1生成和訊息接收
- 針對消費者,需要DirectConsumerTwo,來測試第二個佇列Direct_test_queue_2生成和訊息接收
- 針對消費者,需要DirectConsumerThree,來測試第三個佇列Direct_test_queue_3生成和訊息接收
- 我們還需要一個測試類RabbitmqApplicationTests,用於測試訊息的傳送和接收
rabbitmq先配置一個DirectExchange
@Bean
DirectExchange directExchange(){
return new DirectExchange("directTest", true, false);
}
我們可以看到Direct交換器的名稱定義為了directTest,這時候還未繫結任何的佇列。啟動程式,若我們的設想沒錯,則rabbitmq中應該已經生成了directTest的exchange。
Bingo!directTest交換器成功建立。接下來,我們去編寫DirectRabbitSender的程式碼
@Component
public class DirectRabbitSender{
@Autowired
private RabbitTemplate rabbitTemplate;
private final String EXCHANGE_NAME = "directTest";
private final String ROUTING_KEY = "direct_routing_key";
public void send(Object message) {
rabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTING_KEY, message);
}
}
我們可以看到程式碼中,通過rabbitTemplate傳送訊息到了交換器為directTest,routingkey為direct_routing_key的地方。但這時候我們沒有任何佇列了,自然接不到訊息。現在我們去編寫第一個消費者DirectConsumerOne來接受訊息。
@Component
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "Direct_test_queue_1", durable = "true"),
exchange = @Exchange(value = "directTest"),
key = "direct_routing_key"
))
public class DirectConsumerOne {
@RabbitHandler
private void onMessage(String message){
System.out.println("監聽佇列Direct_test_queue_1接到訊息" + message);
}
}
通過程式碼可以看到,我們通過@QueueBinding把Direct_test_queue_1佇列繫結到了directTest和direct_routing_key上。Direct_test_queue_1並沒有在rabbitmq建立,這並沒有關係。一般來說,@RabbitListener會自動去建立佇列。啟動程式,我們去看一下rabbitmq裡佇列是不是建立了。
Bingo!再次驗證成功。我們去看看繫結關係是不是正確。這時候Direct_test_queue_1應該繫結到了名為directTest的交換器,而繫結的routingkey為direct_routing_key
biubiubiu!繫結關係完全正確。到了這裡,我們進行最後一步,寫了單元測試去傳送訊息,檢視控制檯中消費者是否成功收到訊息。RabbitmqApplicationTests的程式碼如下:
@SpringBootTest
class RabbitmqApplicationTests {
@Autowired
private DirectRabbitSender directRabbitSender;
@Test
void contextLoads() {
}
@Test
public void directSendTest(){
directRabbitSender.send("direct-sender");
directRabbitSender.send("direct-sender_test");
}
}
啟動測試類,然後去檢視控制檯。
沒錯,這就是我們想要達到的效果!基本可以宣佈Direct模式驗證成功。服務端生成exchange,客戶端去生成佇列繫結的方式在direct模式下完全可行。為了保險起見,再驗證一下生成多個消費者繫結到同一個佇列是否可行。
DirectConsumerTwo程式碼如下:
@Component
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "Direct_test_queue_2", durable = "true"),
exchange = @Exchange(value = "directTest"),
key = "direct_routing_key"
))
public class DirectConsumerTwo {
@RabbitHandler
private void onMessage(String message){
System.out.println("監聽佇列Direct_test_queue_2接到訊息" + message);
}
}
DirectConsumerThree程式碼如下:
@Component
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "Direct_test_queue_3", durable = "true"),
exchange = @Exchange(value = "directTest"),
key = "direct_routing_key"
))
public class DirectConsumerThree {
@RabbitHandler
private void onMessage(String message){
System.out.println("監聽佇列Direct_test_queue_3接到訊息" + message);
}
}
啟動測試類,我們去看兩個地方:
- rabbitmq是否建立了客戶端繫結的三個佇列Direct_test_queue_1、Direct_test_queue_2、Direct_test_queue_3
- 消費者應該各自收到2條訊息(Test中傳送了兩條,參看上面 RabbitmqApplicationTests 的程式碼)。那3個佇列,控制檯中應該列印了6條訊息。
hohohoho!建立成功,並且繫結關係我看了也全都正確。我們去看控制檯
6條!沒有任何毛病,至此,可以宣佈Direct模式下,完全支援我們最初的想法:服務端生成exchange,客戶端去生成佇列繫結的方式在direct模式下完全可行。
fanout模式驗證
接下來我們驗證一下fanout的方式,基本操作流程和Direct模式一致。程式碼的結構也差不多:
- 針對生產者,需要RabbitmqConfig,直接在Direct模式下的rabbitmqConfig裡直接新增Fanout的交換器配置
- 針對生產者,需要FanoutRabbitSender,用來實現Fanout模式的訊息傳送
- 針對消費者,需要FanoutConsumerOne,來測試第一個佇列Fanout_test_queue_1生成和訊息接收
- 針對消費者,需要FanoutConsumerTwo,來測試第二個佇列Fanout_test_queue_2生成和訊息接收
- 針對消費者,需要FanoutConsumerThree,來測試第三個佇列Fanout_test_queue_3生成和訊息接收
- 測試類RabbitmqApplicationTests也直接複用Direact模式下測試的類
我就不多BB,直接上程式碼了。
RabbitmqConfig程式碼如下
@Configuration
public class RabbitmqConfig {
@Bean
DirectExchange directExchange(){
return new DirectExchange("directTest", true, false);
}
@Bean
FanoutExchange fanoutExchange(){
return new FanoutExchange("fanoutTest", true, false);
}
}
FanoutRabbitSender的程式碼如下,此處和direct模式的區別是Fanout中沒有routingkey,所以程式碼裡也沒定義routingkey:
@Component
public class FanoutRabbitSender{
@Autowired
private RabbitTemplate rabbitTemplate;
private final String EXCHANGE_NAME = "fanoutTest";
public void send(Object message) {
rabbitTemplate.convertAndSend(EXCHANGE_NAME, null, message);
}
}
我們到這裡先啟動程式試試,看看fanoutTest的交換器在沒有繫結佇列的情況下是否生成了。
棒棒棒!和我們想的一樣,那接下來去寫完所有的消費者,這裡和Direct模式最重要的區別是@Exchange中必須要指定type為fanout。direct模式的程式碼裡沒指定是因為@Exchange的type預設值就是direct。我直接上程式碼了:
/**
* 監聽器主動去宣告queue=fanout_test_queue_1,並繫結到fanoutTest交換器
*/
@Component
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "fanout_test_queue_1", durable = "true"),
exchange = @Exchange(value = "fanoutTest", type = ExchangeTypes.FANOUT)
))
public class FanoutConsumerOne {
@RabbitHandler
private void onMessage(String message){
System.out.println("監聽佇列fanout_test_queue_1接到訊息" + message);
}
}
@Component
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "fanout_test_queue_2", durable = "true"),
exchange = @Exchange(value = "fanoutTest", type = ExchangeTypes.FANOUT)
))
public class FanoutConsumerTwo {
@RabbitHandler
private void onMessage(String message){
System.out.println("監聽佇列fanout_test_queue_2接到訊息" + message);
}
}
@Component
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "fanout_test_queue_3", durable = "true"),
exchange = @Exchange(value = "fanoutTest", type = ExchangeTypes.FANOUT)
))
public class FanoutConsumerThree {
@RabbitHandler
private void onMessage(String message){
System.out.println("監聽佇列fanout_test_queue_3接到訊息" + message);
}
}
接著去測試類RabbitmqApplicationTests中加上fanout的傳送測試,然後註釋掉direct的單元測試,以便一會造成干擾
@SpringBootTest
class RabbitmqApplicationTests {
@Autowired
private DirectRabbitSender directRabbitSender;
@Autowired
private FanoutRabbitSender fanoutRabbitSender;
@Test
void contextLoads() {
}
// @Test
// public void directSendTest(){
// directRabbitSender.send("direct-sender");
// directRabbitSender.send("direct-sender_test");
// }
@Test
public void fanoutSendTest(){
fanoutRabbitSender.send("fanout-sender_1");
fanoutRabbitSender.send("fanout-sender_2");
}
}
程式碼都完成了,現在我們啟動測試類,看看控制檯是否正常收到了訊息
看圖看圖,fanout模式下也完全認證成功!!!那我們可以宣佈,文章開頭的猜想完全可以實現。
總結
服務端只宣告exchange,客戶端來建立queue和繫結exchange的方式完全可行。並且在Direct和Fanout模式下都可行。
那我們可以推測在Header模式的交換器和Topic模式的交換器下應該也大差不差。具體各位可自行驗證,基本流程和上面direct和fanout的流程差不多。