1. 搭建demo工程
首先創建一個工程rabbitmq_demo(開發工具idea),之後的消息模型demo都基於此工程
用的springboot版本比較高,2.2.2的,根據需要選擇吧。
創建完成後,會開始下載所需要的依賴,網速慢的話得等一小會。
pom文件如下(自動生成的,無需改動):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>rabbitmq_demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rabbitmq_demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
spring的配置文件的話暫時用不到,這裏rabbitmq的連接信息配置在了一個工具類裏:
public class ConnectionUtil {
/**
* 建立與RabbitMQ的連接
* @return
* @throws Exception
*/
public static Connection getConnection() throws Exception {
//定義連接工廠
ConnectionFactory factory = new ConnectionFactory();
//設置服務地址
factory.setHost("192.168.33.128");
//端口
factory.setPort(5672);
//設置賬號信息,用戶名、密碼、vhost
factory.setVirtualHost("/");
factory.setUsername("guest");
factory.setPassword("guest");
factory.setConnectionTimeout(1000000);
factory.setHandshakeTimeout(1000000);
// 通過工程獲取連接
Connection connection = factory.newConnection();
return connection;
}
}
注意端口默認時5672,在瀏覽其裏訪問默認時15672
2. 基本消息模型概述
官網介紹如下:
RabbitMQ message broker:它接受和轉發消息。 你可以考慮一下郵局:當你把你想要發佈的郵件信箱, 可以肯定的是,先生或女士Mailperson最終會交付收件人的郵件。 在這個比喻中,RabbitMQ信箱,一個郵局和一個郵差。
P(producer/ publisher):生產者,一個發送消息的用戶應用程序。
C(consumer):消費者,消費和接收有類似的意思,消費者是一個主要用來等待接收消息的用戶應用程序
隊列(紅色區域):rabbitmq內部類似於郵箱的一個概念。雖然消息流經rabbitmq和你的應用程序,但是它們只能存儲在隊列中。隊列只受主機的內存和磁盤限制,實質上是一個大的消息緩衝區。許多生產者可以發送消息到一個隊列,許多消費者可以嘗試從一個隊列接收數據。
總之:
生產者將消息發送到隊列,消費者從隊列中獲取消息,隊列是存儲消息的緩衝區。
3. 代碼實現
3.1 生產者
/**
* 生產者
*/
public class Send {
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws Exception {
// 1. 獲取到連接
Connection connection = ConnectionUtil.getConnection();
// 2. 從連接中創建通道,使用通道才能完成消息相關的操作
Channel channel = connection.createChannel();
// 3. 聲明(創建)隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 消息內容
String message = "Hello 大家好啦啦啦!";
// 向指定的隊列中發送消息 (需要轉換爲字節)
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
//關閉通道和連接
channel.close();
connection.close();
}
}
執行這段代碼後,瀏覽器登錄服務端管理界面如下:
http://192.168.33.128:15672/#/queues -網址ip+端口 (默認15672)
生成了通道和隊列
(看到隊列裏邊已經有了一條消息)
控制檯:
3.2 消費者
代碼如下:
public class Recv {
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws Exception {
// 獲取到連接
Connection connection = ConnectionUtil.getConnection();
// 創建通道
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 定義隊列的消費者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 獲取消息,並且處理,這個方法類似事件監聽,如果有消息的時候,會被自動調用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息體
String msg = new String(body);
System.out.println(" [x] received : " + msg + "!");
}
};
// 監聽隊列,第二個參數:是否自動進行消息確認。
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
運行如下:
消費者已經獲取了消息,但是程序沒有停止,一直在監聽隊列中是否有新的消息。一旦有新的消息進入隊列,就會立即打印.
3.3 消息確認機制(ACK)
通過剛纔的案例可以看出,消息一旦被消費者接收,隊列中的消息就會被刪除。
那麼問題來了:RabbitMQ怎麼知道消息被接收了呢?
如果消費者領取消息後,還沒執行操作就掛掉了呢?或者拋出了異常?消息消費失敗,但是RabbitMQ無從得知,這樣消息就丟失了!
因此,RabbitMQ有一個ACK機制。當消費者獲取消息後,會向RabbitMQ發送回執ACK,告知消息已經被接收。不過這種回執ACK分兩種情況:
- 自動ACK:消息一旦被接收,消費者自動發送ACK
- 手動ACK:消息接收後,不會發送ACK,需要手動調用
那麼哪種更好呢?
這需要看消息的重要性:
- 如果消息不太重要,丟失也沒有影響,那麼自動ACK會比較方便
- 如果消息非常重要,不容丟失。那麼最好在消費完成後手動ACK,否則接收消息後就自動ACK,RabbitMQ就會把消息從隊列中刪除。如果此時消費者宕機,那麼消息就丟失了。
之前的測試都是自動ACK的,如果要手動ACK,需要改動我們的代碼:
public class Recv2 {
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws Exception {
// 獲取到連接
Connection connection = ConnectionUtil.getConnection();
// 創建通道
final Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 定義隊列的消費者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 獲取消息,並且處理,這個方法類似事件監聽,如果有消息的時候,會被自動調用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息體
String msg = new String(body);
System.out.println(" [x] received : " + msg + "!");
// 手動進行ACK
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 監聽隊列,第二個參數false,手動進行ACK
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
注意到最後一行代碼:
channel.basicConsume(QUEUE_NAME, false, consumer);
如果第二個參數爲true,則會自動進行ACK;如果爲false,則需要手動ACK。方法的聲明:
3.4 自動ACK存在的問題
修改消費者,添加異常,如下:
生產者不做任何修改,直接運行,消息發送成功:
運行消費者,程序拋出異常。但是消息依然被消費
管理界面:
3.5 演示手動ACK
修改消費者,把自動改成手動(去掉之前製造的異常)
生產者不變,再次運行:
運行消費者
但是,查看管理界面,發現:
停掉消費者的程序,發現:
這是因爲雖然我們設置了手動ACK,但是代碼中並沒有進行消息確認!所以消息並未被真正消費掉。
當我們關掉這個消費者,消息的狀態再次稱爲Ready
修改代碼手動ACK:
執行: