為了一些初學習者更好理解我就從簡單的解釋一下Rabbitmq的原理吧?,首先你可以這樣想RabbitMq就是一個隊列,而這個隊列相當于一個消息投遞服務,用于應用程序可以發送和接收包裹,而數據所在的服務器也可以接收和發送。所以RabbitMq在應用程序和服務器之間扮演著路由器的角色,當應用程序連接到RabbitMq時,它就必須做個決定:我是發送者還是接收呢?
這里我們首先了解一下什么是生產者:
生產者就是創建消息,然后發送到代理服務器?。這里肯定有很多人不明白消息包括什么?很好解釋有效載體和標簽。有效載體就是你想傳輸的數據(它是可以是如何內容,一個json數組或者你喜歡的字符串RabbitMq根本不會在意這些)。標簽作用就是描述有效載荷(通俗點就是標記可以找到對應的消費者),并且RabbitMq用它來決定誰講獲得消息的拷貝。舉例來說:和tcp協議有點不同,當你明確指定發送方和接收方時,AMQP只會用標簽來表述這條消息(一個交換機的名稱和可選的主題標記),然后發到隊列,隊列會根據標簽發給感興趣的一方(如下圖)
?這里接著了解消費者:
消費者看字面意思我們就很清楚了,主要就是連接到代理服務器上,并訂閱到隊列上,為何訂閱,訂閱就像上面生產者會發送給感興趣的一方,所以訂閱了代表我們感興趣。這里有個特點就是消費者接收到消息時,它只得到消息的一部分:有效載荷(內容),在消息路由過程中,消息標簽并沒有隨有效載荷一同傳遞。RabbitMq甚至不會告訴你是哪個生產者發送的信息。就好比你拿到信件時,所有的信封都是空白,想要知道到底是誰發送的就看信里是否簽名了。同理,如果想知道是誰生成的AMQP消息的話,就要看生成者是否把發送方信息放入有效載荷中(內容)。
看了上面描述我們最基礎的過程應該懂了:生產者創建消息,消費者接收這些消息。你的應用程序可以做為生產者,像其他應用程序發送消息。也可以做為消費者接收消息。兩者可以互相裝換。不過在此之前,它必須依靠一條信道。( 字面理解就是一個通道 )
你想用信息隊列RabbitMq,就必須和它先建立連接吧,沒有連接就不可能完成消費或者發布消息。所以我們要在應用程序和RabbitMq代理服務器之間創建一條TCP連接,一旦TCP連接打開,應用程序就可以創建一條AMQP信道,信道必須建立在“真實的”TCP連接內的虛擬連接。AMQP命令都是通過信道發送出去的。每條信道都會指派唯一的ID(AMQP會幫我們做記錄,這里不用管知道就行)。不論是發布消息、訂閱隊列或是接收消息,這些都是通過信道完成的。這里如果學會思考的人肯定疑問有了TCP連接還要信道干嘛,不直接利用TCP完成AMQP命令呢?主要原因在于對操作系統來說建立和銷毀TCP會話是非常昂貴的的開銷。比如應用程序從隊列消費消息,并根據服務需求合理調度線程。假設你只進行TCP連接,那么每個線程都需要自行連接到RabbitMq上。也就是說稍微高峰期不說多每秒需要上百條連接。很容易對服務器造成不可避免的問題(瓶頸)。如果我們所有線程只使用一條TCP連接以滿足性能方面的要求,不僅能保證私密性,還能各自獨立,這就需要引入信道概念的原因。線程啟動時,會現在連接上建立一條信道,也就獲得連接到RabbitMq上私密通信路徑,而不會給系統的TCP造成額外負擔,因此,你可以每秒成百上千次地創建信道而不會影響操作系統。在一條TCP連接上創建多少條信道是沒有限制,把它想象成光光纖電纜就可以了。每條電纜中的光纖都可以傳輸(就像一條信道)。一條電纜有許多光纖束,允許所有連接的線程通過多條光纖束同時進行傳輸和接收。TCP連接就像電纜,而AMQP信道就像光纖束。
你會感覺前面內容很枯燥,但是沒辦法干貨都是無聊的!你以對消費者和生產者有了一定理解,首先你想要知道什么是隊列。從概念上來講,AMQP消息路由必須有三部分:交換器、隊列、和綁定。生產者把消息發布到交換器上;消息最終到達隊列,并被消費者接收;綁定決定了消息如何從路由器到特定的隊列。這里你還要先理解隊列的概念和工作原理。
消費者通過以下兩種方式從特定的隊列中接收消息:
1:通過AMQP的basic.consume命令訂閱。這樣做會將信道為接收模式,直到取消對隊列的訂閱為止。訂閱了消息后,消費者在消費最近接收的那條消息后,就能從隊列自動接收下一條消息。
2:還有一種情況是我們只想接收一條消息而不是持續訂閱。向隊列請求單條消息時通過AMQP的basic.get命令實現的。這樣做可以讓消費者接收隊列紅的下一條消息。如果想要更多消息,想要再次發送basic.get命令。你不能把basic.get放在一個循環里來替代basic.consume。雖然放在循環里面這種方法不可取,但是比較簡單易懂,只是這樣做會影響性能,所以不是不可以這樣做。
還有一點這里說明一下如果至少有一個消費者訂閱隊列的話,消息會立即發送給這些訂閱的消費者。如果消息到達看無人訂閱的隊列,它會一直在隊列等待,一旦有訂閱,就會立即發送消息。
大家應該迫不及待想看代碼沒問題首先我會演示生產者到消費者這一塊延遲會放在最后面解說:
這里同時解說一下吧:
依賴jar:
org.springframework.boot
spring-boot-starter-amqp
首先我會先寫一個配置類,創建兩個隊列,一個是消息隊列queue1和消息隊列queue2,并且配置連接TCP信息的,配置消息的交換機針對消費者配置,還要讓消息隊列持久化防止丟失,最后是配置綁定交換機和路由。
配置:
package com.didispace.delaymq;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
@Configuration
public class RabbitConfig {
public static final String EXCHANGE= "sb-exchange";//交換機
public static final String ROUTINGKEY1 = "sb-routingKey1";//路由
public static final String ROUTINGKEY2 = "sb-routingKey2";//路由
public static final String QUEUE1 = "queue1";//隊列1
public static final String QUEUE2 = "queue2";//隊列2
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactoryconnectionFactory = new CachingConnectionFactory("127.0.0.1",5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
connectionFactory.setPublisherConfirms(true); // 必須要設置這里是回調用的
return connectionFactory;
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
//必須是prototype類型
public RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(connectionFactory());
return template;
}
@Bean
public DirectExchange defaultExchange() {
return new DirectExchange(EXCHANGE, true, false);
}
@Bean(name = "queue1")
public Queue queue1() {
return new Queue(QUEUE1, true); //隊列持久
}
@Bean(name = "queue2")
public Queue queue2() {
return new Queue(QUEUE2, true); //隊列持久
}
@Bean
public Binding binding1() {
return BindingBuilder.bind(queue1()).to(defaultExchange()).with(ROUTINGKEY1);
}
@Bean
public Binding binding2() {
return BindingBuilder.bind(queue2()).to(defaultExchange()).with(ROUTINGKEY2);
}
}
上面都是對應配置接下來是生產者和對應消費者
生產者:
package com.didispace.delaymq;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@RestController
public class TestController implements RabbitTemplate.ConfirmCallback {
private RabbitTemplate rabbitTemplate;
@Autowired
public void setRabbitTemplate(RabbitTemplate rabbitTemplate) {
rabbitTemplate.setConfirmCallback(this);
this.rabbitTemplate = rabbitTemplate;
}
@RequestMapping("send1")
public String send1(String msg){
String uuid = UUID.randomUUID().toString();
CorrelationData correlationId = new CorrelationData(uuid);
rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE, RabbitConfig.ROUTINGKEY1, msg, correlationId);//交換機、路由、要發送的消息和對應的id值
return null;
}
@RequestMapping("send2")
public String send2(String msg){
String uuid = UUID.randomUUID().toString();
CorrelationData correlationId = new CorrelationData(uuid);
rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE, RabbitConfig.ROUTINGKEY2, msg, correlationId); //交換機、路由、要發送的消息和對應的id值
return null;
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println(" 回調id:" + correlationData);
if (ack) {
System.out.println("消息成功消費");
} else {
System.out.println("消息消費失敗:" + cause+"\n重新發送");
}
}
}
消費者:
package com.didispace.delaymq;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitmqListener {
@Autowired
private ConnectionFactory connectionFactory;
@Autowired
private Queue queue1;
@Autowired
private Queue queue2;
@Autowired
private TestController testController;
@Bean
public SimpleMessageListenerContainer messageContainer() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
container.setQueues(queue1);
container.setExposeListenerChannel(true);
container.setMaxConcurrentConsumers(1);
container.setConcurrentConsumers(1);
container.setAcknowledgeMode(AcknowledgeMode.MANUAL); //設置確認模式手工確認
container.setMessageListener(new ChannelAwareMessageListener() {
public void onMessage(Message message, com.rabbitmq.client.Channel channel) throws Exception {
byte[] body = message.getBody();
testController.send2("hahahahaahha");//這里做了實驗啟動了隊列2,說明在1個隊列可以啟動其他隊列。
System.out.println("queue1 收到消息 : " + new String(body));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); //確認消息成功消費
}
});
return container;
}
@Bean
public SimpleMessageListenerContainer messageContainer2() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
container.setQueues(queue2);
container.setExposeListenerChannel(true);
container.setMaxConcurrentConsumers(1);
container.setConcurrentConsumers(1);
container.setAcknowledgeMode(AcknowledgeMode.MANUAL); //設置確認模式手工確認
container.setMessageListener(new ChannelAwareMessageListener() {
public void onMessage(Message message, com.rabbitmq.client.Channel channel) throws Exception {
byte[] body = message.getBody();
System.out.println("queue2 收到消息 : " + new String(body));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); //確認消息成功消費
}
});
return container;
}
}
上面我多加了一個回調,這個意思就是我發送的消息成功發送至于消費者接收沒接收,消費沒消費我回調根本不知道。對啦我在上面還做了一個小實驗你認真看會發現我在queue1隊列消費者里面調用了隊列queue2嘿嘿個人愛好喜歡瞎搞!
好啦上面也可以測試,把項目啟動起來那個Spring啟動的那個類沒寫太簡單我相信你們都會,直接啟動,然后如下圖訪問頁面 :http://localhost:8080/send1
?好啦上面都是RabbitMq簡單操作沒進入正題,現在正式研究延遲隊列
關于延遲隊列先講一下原理:
創建一個交換機e1(廣播類型),一個交換機e2(類型看自己需求),e1綁定一個隊列q1,q1設置死信交換機為e2,e2可以綁定有需求的任何隊列,比如q2。當要發延遲消息的時候,直接將消息發送到交換機e1,消息會被廣播到q1隊列中,q1隊列因為沒有消費者,消息過期后會自動轉發先前綁定的e2交換機,e2會正確投遞到你指定的隊列中。
當然還有一種方法是直接對隊列操作的:
創建兩個隊列,一個隊列標記為死信e1,一個是延遲隊列設置延遲時間e2,在消費者那邊,要把e1死信隊列事先訂閱綁定到死信路由和交換機,把延遲隊列e2設置了死信的路由和交換器,e2延遲之后自然到對應的死信里面,根據死信路由和交換器找到對應綁定的隊列e
上面對延遲隊列應該有一定的理解,個人建議是自己畫圖理解比較好,畢竟我畫圖太丑!所以不獻丑了!直接上代碼:
生產者:
package com.didispace.delaymq;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
public class Producer {
public static String DIRECT_EXCHANGES = "amq.direct";//交換機
public static String DEAD_LETTER_ROUTING = "DEAD_LETTER_ROUTING";//死信路由
public static String DEAD_LETTER_QUEUE_NAME = "DEAD_LETTER_QUEUE";//死信隊列名
public static String DELAY_QUEUE = "DELAY_QUEUE";//延遲隊列
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
//設置MabbitMQ所在主機ip或者主機名
factory.setHost("localhost");
//創建一個連接
Connection connection = factory.newConnection();
//創建一個頻道
Channel channel = connection.createChannel();
//指定一個隊列
channel.queueDeclare(DEAD_LETTER_QUEUE_NAME, true, false, false, null);
//發送的消息延遲30秒并且持久化
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
AMQP.BasicProperties properties = builder.expiration("30000").deliveryMode(2).build();
//往隊列中發出一條消息這時候要發送的隊列不應該是QUEUE_NAME,這樣才能進行轉發的
for (int i=10; i > 0; i--) {
String message = "hello world!"+System.currentTimeMillis();
channel.basicPublish("", DELAY_QUEUE, properties, message.getBytes());
System.out.println("Producer Sent_DELAY_QUEUE_"+i+" : " + message + "" );
}
//關閉頻道和連接
channel.close();
connection.close();
}
}
消費者:
package com.didispace.delaymq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
import java.util.HashMap;
public class Consumer {
//private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//聲明隊列,主要為了防止消息接收者先運行此程序,隊列還不存在時創建隊列。
channel.queueDeclare(Producer.DEAD_LETTER_QUEUE_NAME, true, false, false, null);
// channel.queueBind(QUEUE_NAME, "amq.direct", QUEUE_NAME);
channel.queueBind(Producer.DEAD_LETTER_QUEUE_NAME, Producer.DIRECT_EXCHANGES, Producer.DEAD_LETTER_ROUTING);
//這里是創建死信并且綁定死信
HashMap arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", Producer.DIRECT_EXCHANGES);
arguments.put("x-dead-letter-routing-key", Producer.DEAD_LETTER_ROUTING);
channel.queueDeclare(Producer.DELAY_QUEUE, true, false, false, arguments);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
//創建隊列消費者
QueueingConsumer consumer = new QueueingConsumer(channel);
//指定消費隊列
channel.basicConsume(Producer.DEAD_LETTER_QUEUE_NAME, true, consumer);
while (true) {
//nextDelivery是一個阻塞方法(內部實現其實是阻塞隊列的take方法)
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'"+ "'[當前系統時間戳]" +System.currentTimeMillis());
}
}
}
這里需要運行main方法當然如果想用其他方法也是可以的改一下就ok啦,這里我還是截圖最后運行結果,肯定先讓生產者main方法先走,在走消費者main方法。這里建議延遲時間10秒最好不多不少,太長時間也不好。如圖結果:
這里開了4個消費者,因為我用循環發了是10個延遲隊列,阻塞的話一個消費者處理太慢,所以開了4個消費者處理會很快,分配任務是3,3,2,2的方式。?
好了基本上寫完了,肯定有很多瑕疵,所以還是希望可以互相討論,我研究這個也才2個星期,不可能寫的那么完美,多多體諒!有什么不懂的或者有什么瑕疵!轉載需聲明地址,謝謝!
####指導qq:179061434