消息中間件之RabbitMQ
基礎
一、RabbirMQ
介紹
RabbitMQ
使用Erlang
語言開發,支持的併發量不大,適用於中小企業使用,併發量不是很大。
RabbitMQ
是一個在AMQP
基礎上實現的,可複用的企業消息系統。它可以用於大型軟件系統各個模塊之間的高效通信,支持高併發,支持可擴展。
支持多種開發語言支持,Java
、Python
、Ruby
、PHP
、C/C++
等。
RabbitMQ
支持的工作隊列
二、RabbitMQ
安裝
想吐槽,這個RabbitMQ
和Erlang
安裝實在是不好用,可能還是自己太菜,反正挺麻煩的。但是我們還是用Docker
這個好用的,哈哈!
官方也是用的:https://www.rabbitmq.com/download.html
docker pull rabbitmq:3-management # 拉取rabbitmq
docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:3-management # 啓動容器
如果開啓的防火牆,需要先開放端口5672
和15672
,然後在更新規則。
[root@localhost ~]# firewall-cmd --zone=public --add-port=15672/tcp --permanent
[root@localhost ~]# firewall-cmd --zone=public --add-port=5672/tcp --permanent
[root@localhost ~]# firewall-cmd --reload
訪問:http://IP:15672/#/
用戶名和密碼:guest
和guest
,輸入登錄之後就可以進入RabbitMQ
管理頁面。
RabbitMQ
支持這幾種端口號:
- 5672:消息中間內部通訊的端口
- 15672:管理平臺端口號
- 25672:集羣的端口號
三、管理界面的使用
3.1. 管理界面介紹
3.2. 創建用戶
首先看一下用戶角色
角色 | 代碼 | 描述 |
---|---|---|
超級管理員 | administrator | 可登陸管理控制檯,可查看所有的信息,並且可以對用戶,策略(policy )進行操作 |
監控者 | monitoring | 可登陸管理控制檯,同時可以查看rabbitmq 節點的相關信息(進程數,內存使用情況,磁盤使用情況等) |
策略制定者 | policymaker | 可登陸管理控制檯, 同時可以對policy 進行管理。但無法查看節點的相關信息(上圖紅框標識的部分) |
普通管理者 | management | 僅可登陸管理控制檯,無法看到節點信息,也無法對策略進行管理 |
其他 | 無法登陸管理控制檯,通常就是普通的生產者和消費者 |
創建用戶
3.3. 添加Virtual Hosts
3.4. 爲用戶添加Virtual Hosts
四、五種隊列
常用的只有:點對點的簡單隊列、工作隊列、發佈訂閱、路由、通配符這五種,下面我們詳細介紹下
4.1. 點對點模式(簡單隊列)
4.1.1. 介紹
一個生產者P發送消息到隊列Q,一個消費者C接收。有多個消費者會使用輪詢方法進行消費隊列中信息。
4.1.2. 代碼演示
引入依賴
<!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
工具類:
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author 墨龍吟
* @version 1.0.0
* @ClassName RabitConnection.java
* @Description tabbitmq 連接工具
* @createTime 2020年01月29日 - 18:53
*/
public class RabbitConnection {
public static Connection connection(){
Connection connection = null;
try {
ConnectionFactory connectionFactory = new ConnectionFactory();
// step 設置IP
connectionFactory.setHost("192.168.252.132");
// step 設置端口
connectionFactory.setPort(5672);
// step 設置用戶名和密碼
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
// step 設置 Virtual Host
connectionFactory.setVirtualHost("/long");
connection = connectionFactory.newConnection();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
return connection;
}
}
生產者
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author 墨龍吟
* @version 1.0.0
* @ClassName MQProducer.java
* @Description 生產者
* @createTime 2020年01月29日 - 18:58
*/
public class MQProducer {
public static void main(String[] args) {
try {
Connection connection = RabbitConnection.connection();
// step 創建通道
Channel channel = connection.createChannel();
String msg = "中國加油!";
channel.basicPublish("", "hello", null, msg.getBytes());
System.out.println("生產者發送消息:" + msg);
channel.close();
connection.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
消費者
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* @author 墨龍吟
* @version 1.0.0
* @ClassName MQConsumer.java
* @Email [email protected]
* @Description 消費者
* @createTime 2020年01月29日 - 19:02
*/
public class MQConsumer {
public static void main(String[] args) {
try {
Connection connection = RabbitConnection.connection();
Channel channel = connection.createChannel();
channel.basicConsume("hello", true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消費者接受的消息:" + msg);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.2. 工作隊列
4.2.1. 介紹
默認的傳統隊列是爲均攤消費,存在不公平性;如果每個消費者速度不一樣的情況下,均攤消費是不公平的,應該是能者多勞。
4.2.2. 圖例
採用工作隊列,在通道中只需要設置basicQos
爲1即可,表示MQ
服務器每次只會給消費者推送1條消息必須手動ack
確認之後纔會繼續發送。channel.basicQos(1)
。
4.2.3. 實例
生產者:
public class MQProducer {
public static void main(String[] args) {
try {
Connection connection = RabbitConnection.connection();
// step 創建通道
Channel channel = connection.createChannel();
for (int i = 0; i < 10; i++) {
String msg = "第" + i + "條,中國加油!";
channel.basicPublish("", "hello", null, msg.getBytes());
System.out.println("生產者發送消息:" + msg);
}
channel.close();
connection.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
消費者:
public class MQConsumer {
public static void main(String[] args) {
final int time = 2000;
System.out.println("消費者:" + time);
try {
Connection connection = RabbitConnection.connection();
final Channel channel = connection.createChannel();
// MQ每次只能給消費者發送一條消息,必須返回ack之後纔可以繼續發送消息給消費者
channel.basicQos(1);
// auto Ack 默認自動簽收(true), false(必須手動Ack)
channel.basicConsume("hello", false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消費者接受的消息:" + msg);
// 手動告訴MQ從隊列中刪除這條消息
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
結果:
4.2.4. 隊列存在的問題:消失丟失
消息丟失可能存在三方面:
- 生產者丟失數據:生產者將數據發送到
RabbitMQ
的時候,可能數據就在半路丟失; RabbitMQ
丟失數據:MQ
還沒有持久化就自己丟失了 (MQ
掛了、MQ
拒絕接受消息 (隊列滿了));- 消費端丟失數據:剛消費到,還沒處理,結果進程掛了(重啓消費端)
- 其他情況下: 硬盤壞了、持久化的過程斷電了 ; 最好通過表記錄每次生產者投遞消息,如果長期沒有被消費,手動的補償消費。
4.2.5. 消息不丟失解決方法:
-
生產者方面:
-
方案一: 開啓
RabbitMQ
事務(同步方法,不推薦)// 開啓事務 channel.txSelect try { // 這裏發送消息 } catch (Exception e) { channel.txRollback // 這裏再次重發這條消息 } // 提交事務 channel.txCommit
-
方案二:開啓
confirm
模式(異步,推薦)channel.confirmSelect(); String msg = "第" + i + "條,中國加油!"; channel.basicPublish("", "hello", null, msg.getBytes()); if (channel.waitForConfirms()) { System.out.println("生產者發送消息:" + msg + "成功"); } else { System.out.println("生產者發送消息:" + msg + "失敗"); }
-
-
MQ
方面:-
開啓
RabbitMQ
的持久化(默認的情況下MQ
服務器端創建隊列和交換機都是持久化的) -
通過代碼設置持久化。
-
-
消費者方面:
- 關閉
RabbitMQ
的自動ACK
- 關閉
4.3. 發佈訂閱模式
RabbitMQ
支持的後面幾種模式都是依賴於交換機。交換機支持一下幾種模式:
- Direct exchange(直連交換機)
- Fanout exchange(扇型交換機)
- Topic exchange(主題交換機)
- Headers exchange(頭交換機)
4.3.1. 原理介紹
簡單解釋就是,可以將消息發送給不同類型的消費者。做到發佈一次,消費多個。(使用扇形交換機)
4.3.2. 創建交換機
4.3.3. 實例
先創建對應virtual host
的交換機。
生產者:
public class PsProducer {
private final static String EXCHANGE_NAME = "test_long";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("發佈訂閱模式中生產者啓動...");
// 創建新的連接
Connection connection = RabbitConnection.connection();
// 創建通道
Channel channel = connection.createChannel();
// 綁定的交換機 參數1交互機名稱 參數2 exchange類型
channel.exchangeDeclare(EXCHANGE_NAME, "fanout", true);
String msg = "這是生產者發送的一個消息...";
// 發送消息
channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes());
channel.close();
connection.close();
}
}
短信消費者:
public class SmsConsumer {
private final static String EXCHANGE_NAME = "test_long";
private final static String QUEUE_NAME = "sms_queue";
public static void main(String[] args) throws IOException {
System.out.println("短信消費者啓動...");
// 創建新的連接
Connection connection = RabbitConnection.connection();
// 創建通道
Channel channel = connection.createChannel();
// 消費者關聯隊列
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
channel.basicConsume(QUEUE_NAME, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("短信消費者接受的消息:" + msg);
}
});
}
}
郵件消費者:
public class EmailConsumer {
private final static String EXCHANGE_NAME = "test_long";
private final static String QUEUE_NAME = "email_queue";
public static void main(String[] args) throws IOException {
System.out.println("郵件消費者啓動...");
Connection connection = RabbitConnection.connection();
Channel channel = connection.createChannel();
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
channel.basicConsume(QUEUE_NAME, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("郵件消費者接受的消息:" + msg);
}
});
}
}
結果:
4.4. 路由模式
4.4.1. 簡介
當交換機類型爲direct類型時,根據隊列綁定的路由建轉發到具體的隊列中存放消息。
4.4.2. 實例代碼
生產者
public class Producer {
private final static String EXCHANGE_NAME = "long_direct_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("發佈訂閱模式中生產者啓動...");
Connection connection = RabbitConnection.connection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct", true);
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
String msg = "這是生產者" + i + "發送郵件的一個消息...";
System.out.println("郵件消息: " + msg);
channel.basicPublish(EXCHANGE_NAME, "email", null, msg.getBytes());
} else {
String msg = "這是生產者" + i + "發送短信的一個消息...";
System.out.println("短信消息: " + msg);
channel.basicPublish(EXCHANGE_NAME, "sms", null, msg.getBytes());
}
}
channel.close();
connection.close();
}
}
短信消費者
public class SmsConsumer {
private final static String EXCHANGE_NAME = "long_direct_exchange";
private final static String QUEUE_NAME = "sms_queue";
public static void main(String[] args) throws IOException {
System.out.println("短信消費者啓動...");
Connection connection = RabbitConnection.connection();
Channel channel = connection.createChannel();
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "sms");
channel.basicConsume(QUEUE_NAME, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("短信消費者接受的消息:" + msg);
}
});
}
}
郵件消費者
public class EmailConsumer {
private final static String EXCHANGE_NAME = "long_direct_exchange";
private final static String QUEUE_NAME = "email_queue";
public static void main(String[] args) throws IOException {
System.out.println("郵件消費者啓動...");
Connection connection = RabbitConnection.connection();
Channel channel = connection.createChannel();
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "email");
channel.basicConsume(QUEUE_NAME, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("郵件消費者接受的消息:" + msg);
}
});
}
}
消費結果:
4.5. 通配符模式
4.5.1. 簡介
當交換機類型爲topic類型時,根據隊列綁定的路由鍵模糊轉發到具體的隊列中存放。
#
號表示支持匹配多個詞;*
號表示只能匹配一個詞。
4.5.2. 實例代碼
生產者:
public class Producer {
private final static String EXCHANGE_NAME = "long_topic_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("發佈訂閱模式中生產者啓動...");
Connection connection = RabbitConnection.connection();
Channel channel = connection.createChannel();
// 修改爲topic類型
channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
String msg = "這是生產者" + i + "發送郵件的一個消息...";
System.out.println("郵件消息: " + msg);
// 路由鍵 爲 topic.email.long
channel.basicPublish(EXCHANGE_NAME, "topic.email.long", null, msg.getBytes());
} else {
String msg = "這是生產者" + i + "發送短信的一個消息...";
System.out.println("短信消息: " + msg);
// 路由鍵 爲 topic.sms
channel.basicPublish(EXCHANGE_NAME, "topic.sms", null, msg.getBytes());
}
}
channel.close();
connection.close();
}
}
短信消費者:使用#
可以匹配所有topic開頭
的消息。
public class SmsConsumer {
private final static String EXCHANGE_NAME = "long_topic_exchange";
private final static String QUEUE_NAME = "sms_queue";
public static void main(String[] args) throws IOException {
System.out.println("短信消費者啓動...");
Connection connection = RabbitConnection.connection();
Channel channel = connection.createChannel();
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "topic.#");
channel.basicConsume(QUEUE_NAME, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("短信消費者接受的消息:" + msg);
}
});
}
}
郵件消費者:使用*
可以匹配所有topic.*.*
(*爲替代字符)的消息。
public class EmailConsumer {
private final static String EXCHANGE_NAME = "long_topic_exchange";
private final static String QUEUE_NAME = "email_queue";
public static void main(String[] args) throws IOException {
System.out.println("郵件消費者啓動...");
Connection connection = RabbitConnection.connection();
Channel channel = connection.createChannel();
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "topic.email.*");
channel.basicConsume(QUEUE_NAME, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("郵件消費者接受的消息:" + msg);
}
});
}
}
運行結果
五、Sptingboot
整合RabbitMQ
5.1. 添加依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
5.2. 配置文件
spring:
rabbitmq:
# 連接地址
host: 192.168.252.132
# 端口號
port: 5672
# 賬號
username: admin
# 密碼
password: admin
# 地址
virtual-host: /long
5.3. 配置類:註冊隊列和交換機
主要三步:
- 創建隊列
- 創建交換機
- 將隊列綁定到交換機
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
/**
* @author 墨龍吟
* @version 1.0.0
* @ClassName RabbitConfig.java
* @Description RabbitMQ 配置類
* @createTime 2020年02月17日 - 22:10
*/
@Component
public class RabbitConfig {
/** 交換機名稱 */
private final static String EXCHANGE_NAME = "spring_boot_exchange";
/** 短信隊列名稱 */
private final static String FANOUT_SMS_QUEUE = "fanout_sms_queue";
/** 郵件隊列名稱 */
private final static String FANOUT_EMAIL_QUEUE = "fanout_email_queue";
/** 創建短信隊列 */
@Bean
public Queue smsQueue() {
return new Queue(FANOUT_SMS_QUEUE);
}
/** 創建郵件隊列 */
@Bean
public Queue emailQueue() {
return new Queue(FANOUT_EMAIL_QUEUE);
}
/** 創建交換機 */
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange(EXCHANGE_NAME);
}
/** 將短信隊列綁定到交換機 */
@Bean
public Binding smsBindingExchange(Queue smsQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(smsQueue).to(fanoutExchange);
}
/** 將郵件隊列綁定到交換機 */
@Bean
public Binding emailBindingExchange(Queue emailQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(emailQueue).to(fanoutExchange);
}
}
5.4. 創建生產者和消費者
生產者:
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 墨龍吟
* @version 1.0.0
* @ClassName HomeController.java
* @Description 消費者
* @createTime 2020年02月17日 - 22:47
*/
@RestController
public class HomeController {
private final static String EXCHANGE_NAME = "spring_boot_exchange";
@Autowired
private AmqpTemplate amqpTemplate;
/**
* 投遞消息,客戶端不會馬上知道消費者是否被消費,但是能夠確認知道我們是否投遞消息到中間件
* @return
*/
@GetMapping("/send_msg")
public String sendMsg() {
// 參數1 交換機名稱 、參數2路由key 參數3 消息
amqpTemplate.convertAndSend(EXCHANGE_NAME, "", "這個是一條消息");
return "success";
}
}
消費者:
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @author 墨龍吟
* @version 1.0.0
* @ClassName FanoutEmailConsumer.java
* @Description 郵件消費者
* @createTime 2020年02月17日 - 22:52
*/
@Component
@RabbitListener(queues = "fanout_email_queue")
public class FanoutEmailConsumer {
@RabbitHandler
public void process(String msg) {
System.out.println("郵件消費者收到消息:" + msg);
}
}
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @author 墨龍吟
* @version 1.0.0
* @ClassName FanoutEmailConsumer.java
* @Description 郵件消費者
* @createTime 2020年02月17日 - 22:52
*/
@Component
@RabbitListener(queues = "fanout_sms_queue")
public class FanoutSmsConsumer {
@RabbitHandler
public void process(String msg) {
System.out.println("短信消費者收到消息:" + msg);
}
}
springboot
會自動創建交換機和隊列,不需要我們手動創建。