前言
這個項目的github地址:extensible項目的github地址
extensible項目當前功能模塊如下:
java-web系列(一)—搭建一個基於SSM框架的java-web項目
java-web系列(二)—以dockerfile的方式發佈java-web項目
java-web系列(三)—(slf4j + logback)進行日誌分層
java-web系列(四)—幾種常見的加密算法
java-web系列(五)—SpringBoot整合Redis
java-web系列(六)—Mybatis動態多數據源配置
java-web系列(七)—SpringBoot整合Quartz實現多定時任務
java-web系列(八)—RabbitMQ在java-web中的簡單應用
如對該項目有疑問,可在我的博客/github下面留言,也可以以郵件的方式告知。
我的聯繫方式:[email protected]
RabbitMQ的使用場景
MQ,是Message Queue(消息隊列)的簡寫。簡而言之,RabbitMQ就是將消息儲存在隊列中。
在項目的實際開發過程中,可以將一些無需即時返回結果且耗時的操作提取出來,進行異步處理。這種處理方式能夠大大節省服務器的請求響應時間,從而提高系統的吞吐量。
比如:以去年雙十一淘寶成交額爲例
當天每秒下訂單筆數超過32.5萬筆,支付筆數超過25.6萬筆。
也就是說,需要阿里的服務器每秒進行32.5萬個“生成訂單”的操作,還要進行25.6萬個“支付訂單”的操作。
而這些與money相關的,都是一些耗時的操作。如果要求即時返回這些操作的處理結果,服務器的壓力太大。
實際能夠進行優化後的做法是:把兩種不同的操作,放到兩個不同的隊列中延後進行處理。
如:“剁手黨”之一的我在當天進行了一個下單操作,服務器會將這個操作的具體邏輯放到“order_queue”隊列中,就告知我下單成功,然後服務器會在空閒時處理並生成相應的訂單。這種做法,不僅提升了用戶的使用體驗(我覺得很快就下單成功了),還能夠緩解服務器的壓力。
常用的消息隊列還有:ZeroMQ,ActiveMQ,Kafka等等,有興趣的可以自行了解。這篇博客主要是學習RabbitMQ的簡單應用,這裏就不探討這幾種消息隊列的優劣勢了。
在Windows下安裝RabbitMQ
RabbitMQ是使用Erlang語言編寫的一個開源的消息隊列。
下面的這兩個安裝包也可以在我的網盤中下載,提取碼爲:bs7t。
- 首先,在Erlang官網下載對應版本的Erlang平臺。
- 然後運行可執行文件(otp_win64_21.1)。按默認配置進行安裝,更改一下文件的存放目錄即可。出現如下圖說明Erlang平臺安裝成功。
- 再然後在RabbitMQ官網下載最新版本的RabbitMQ Server。
- 繼續按默認配置進行安裝,出現如下圖說明RabbitMQ Server安裝成功。
- 這種做法,是將RabbitMQ暴露爲Window的一個服務。在這裏啓動RabbitMQ後,可以在15672端口用默認賬戶密碼進行登錄(guest/guest)。
- 出現如下圖說明登錄成功。
RabbitMQ中的幾個重要概念
- Producer
Producer,生產者。消息的發送方即爲生產者。
- Consumer
Consumer,消費者。消息的接收方即爲消費者。
- Connection
Connection,獲取RabbitMQ(消息隊列)服務的長連接。得到長連接實例的方式有兩種:
- 手動設置相關參數(host/port etc…)
/**
* 不設置的話,會使用默認的參數(userName:guest,password:guest,virtualHost:/,hostName:localhost,portNumber:5672)
*/
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername(userName);
factory.setPassword(password);
factory.setVirtualHost(virtualHost);
factory.setHost(hostName);
factory.setPort(portNumber);
Connection conn = factory.newConnection();
- URI設置相關參數
/**
* 不設置的話,會使用默認的參數(userName:guest,password:guest,virtualHost:/,hostName:localhost,portNumber:5672)
*/
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://userName:password@hostName:portNumber/virtualHost");
Connection connection = factory.newConnection();
- Channel
Channel,通道。通過通道來決定“生產者如何往隊列中發送消息”、“消費者如何從隊列中接收消息”。獲取通道實例的方式如下:
Channel channel = connection.createChannel();
- Exchange
Exchange,交換機。生產者往隊列中發送消息,實際不是直接發送給隊列,而是先發送給交換機,由交換機決定實際將消息發送給哪個隊列。
- Queue
Queue,隊列。存儲消息的地方。交換機和隊列都是通過通道實例來聲明,而且交換機與隊列直接是存在對應關係的。具體的源碼如下:
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT, true);
channel.queueDeclare(queueName,true,false,false,null);
channel.queueBind(queueName, exchangeName, routingKey);
具體的代碼解釋如下:
通過ChannelIN.exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable)
來聲明交換機實例,第一個參數是交換機的名稱,第二個參數是交換機的類型(direct/fanout/topic/headers),第三個參數是該交換機是否持久化。
通過ChannelIN.queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
來聲明隊列實例,第一個參數是隊列的名稱,第二個參數是該隊列是否持久化,第三個參數是是否只有自己能夠看到該對列(排他性),第四個參數是當沒有消費者佔用該隊列時是否刪除該隊列。
通過queueBind(String queue, String exchange, String routingKey)
綁定該交換機和隊列。routingKey是綁定隊列和交換機之間的路由規則。
這些概念之間的關係結構圖如下:
RabbitMQ在java-web中的應用
- 必須要導入RabbitMQ Server的POM依賴
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>4.0.3</version>
</dependency>
- 發送消息的生產者測試代碼如下:
/**
* RabbitMQ中生產者測試代碼
* @author zhenye 2018/9/29
*/
@Slf4j
public class ProducerTest {
private final static String EXCHANGE_NAME = "MY_EXCHANGE";
private final static String QUEUE_NAME = "MY_QUEUE";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT,true);
channel.queueDeclare(QUEUE_NAME,true,false,false,null);
String routingKey = "123";
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,routingKey);
String message = "Hello RabbitMQ, I will send some message to the consumer.";
channel.basicPublish(EXCHANGE_NAME,routingKey,null,message.getBytes("UTF-8"));
log.info("Producer send message, the content is :" + message);
channel.close();
connection.close();
}
}
- 接收消息的消費者測試代碼如下:
/**
* RabbitMQ中消費者測試代碼
* @author zhenye 2018/9/29
*/
@Slf4j
public class ConsumerTest {
private final static String QUEUE_NAME = "MY_QUEUE";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,true,false,false,null);
log.info("Consumer is waiting!");
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");
log.info("Consumer received message, the content is:" + message);
}
};
channel.basicConsume(QUEUE_NAME,true,consumer);
}
}
- 演示過程
連續運行5次ProducerTest.main()
,即有5個消費者往隊列(MY_QUEUE)中發送了消息。
RabbitMQ Server主頁顯示如下:
結果表明,在RabbitMQ的所有隊列中,還有5條待處理的消息。
接着運行ConsumerTest.main()
,效果圖如下:
結果表明,該消費者一次性處理完了5條消息。
- 流程說明
生產者是通過ChannelIN.basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)
來發送消息,第一個參數是交換機名稱,第二個參數是路由規則Key,第三個參數是全局屬性,第四個參數是消息實體。第一、二個參數是可以確定要保存消息的隊列的。
消費者是通過DefaultConsumer.handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
來接收並處理消息的。由於消費者是直接從隊列中獲取消息,只要保證生產者存入消息的隊列(交換機名稱、路由Key確定的隊列),與消費者接收消息的隊列(直接指定QUEUE_NAME)相同,就能正確地取出消息。