文章目錄
消息中間件
消息中間件作用:異步解耦、流量消峯
一、RabbitMQ 工作模式
1.安裝
-
下載並安裝erlang,下載地址:http://www.erlang.org/download
-
配置erlang環境變量信息
- 新增環境變量ERLANG_HOME=erlang的安裝地址
- 將%ERLANG_HOME%\bin加入到path中
-
下載並安裝RabbitMQ,下載地址:http://www.rabbitmq.com/download.html
- 配置rabbitmq的環節變量:d:\rabbitmq_server-3.2.1/sbin
- 安裝可視化插件:rabbitmq-plugins.bat enable rabbitmq_management
- 使用命令rabbitmqctl status檢查是否正常
- 啓動或者停止rabbitmq: net start RabbitMQ / net stop RabbitMQ
- 添加用戶:
- rabbitmqctl.bat add_user admin admin
- rabbitmqctl.bat set_user_tags admin administrator
- rabbitmqctl.bat set_permissions -p / admn “.” “.” “.*”
- 需要修改:rabbitmq_server-3.7.8 /etc/rabbitmq.config.example,也可以複製一份修改名稱rabbitmq.config。然後再環境變量中配置他的路徑。修改內容如下
/* {tcp_listeners,[{"127.0.0.1",5672},{"::1",5672}]} {loopback_users,[admin]} */
注意: RabbitMQ 它依賴於Erlang,需要先安裝Erlang。
http://192.168.1.6:15672 默認賬號:guest / guest
2.RabbitMQ
RabbitMQ官方教程:https://www.rabbitmq.com/getstarted.html
3.核心概念
-
virtual Hosts 虛擬主機
- VirtualHost相當月一個相對獨立的RabbitMQ服務器,每個VirtualHost之間是相互隔離的。exchange、queue、message不能互通
-
生產者(Producer):發送消息的應用。
-
消費者(Consumer):接收消息的應用。
-
隊列(Queue):存儲消息的緩存。
-
消息(Message):由生產者通過RabbitMQ發送給消費者的信息。
-
連接(Connection):連接RabbitMQ和應用服務器的TCP連接。
-
通道(Channel):連接裏的一個虛擬通道。當你通過消息隊列發送或者接收消息時,這個操作都是通過通道進行的。
-
交換機(Exchange):交換機負責從生產者那裏接收消息,並根據交換類型分發到對應的消息列隊裏。要實現消息的接收,一個隊列必須到綁定一個交換機。
-
綁定(Binding):綁定是隊列和交換機的一個關聯連接。
-
路由鍵(Routing Key):路由鍵是供交換機查看並根據鍵來決定如何分發消息到列隊的一個鍵。路由鍵可以說是消息的目的地址
當生產者發送消息時,它並不是直接把消息發送到隊列裏的,而是使用交換機(Exchange)來發送
4.工作模式
-
簡單模式:一個生產者,一個消費者
-
work模式:一個生產者,多個消費者,消費者進行手動應答,誰應答快,誰消費消息多。
-
訂閱模式:一個生產者發送的消息會被多個消費者獲取。
-
路由模式:發送消息到交換機並且要指定路由key ,消費者將隊列綁定到交換機時需要指定路由key
-
topic模式:將路由鍵和某模式進行匹配,此時隊列需要綁定在一個模式上,“#”匹配一個詞或多個詞,“*”只匹配一個詞。
創建連接 ——> 獲取連接 -------->創建通道 -----> 聲明隊列 ---->發送消息
消費端,通過監聽方式,拉取隊列對應的消息。
queueDeclare
隊列聲明
channel
通道
應答模式ACK
5 簡單模式/work模式
5.1 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
5.2生產者
package com.zx.rabbitmqdemo.simpleQueue;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class ProductMessage {
//聲明一個隊列
private static final String QUEUE_NAME = "test_rabbitmq_simple";
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
//設置地址
connectionFactory.setPort(5672);
connectionFactory.setPassword("admin");
connectionFactory.setUsername("admin");
connectionFactory.setHost("192.168.1.6");
//設置虛擬主機
connectionFactory.setVirtualHost("/");
//獲取連接
Connection connection = connectionFactory.newConnection();
//創建一個通道
Channel channel = connection.createChannel();
//申明一個隊列
// durable: 如果爲true,消息持久化,服務器重啓後消息還存在
//exclusive:如果聲明獨佔隊列(僅限於此連接),則爲true
//autoDelete:如果聲明的是自動刪除隊列,則爲true(服務器將在不再使用時將其刪除)
//arguments:隊列的其他屬性(構造參數)
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String msg = "生產者發送消息發送到隊列:"+QUEUE_NAME+"中 ";
/*
發佈消息:
exchange:交換機
queue_name:隊列名
props:消息路由頭等的其他屬性
body:消息體,二進制數組
*/
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
channel.close();
connection.close();
}
}
5.3 消費者
package com.zx.rabbitmqdemo.simpleQueue;
import com.rabbitmq.client.*;
import com.rabbitmq.client.impl.recovery.ConsumerRecoveryListener;
import java.io.UnsupportedEncodingException;
public class ConsumerMessage {
//聲明一個隊列
private static final String QUEUE_NAME = "test_rabbitmq_simple";
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
//設置地址
connectionFactory.setPort(5672);
connectionFactory.setPassword("admin");
connectionFactory.setUsername("admin");
connectionFactory.setHost("192.168.1.6");
//設置虛擬主機
connectionFactory.setVirtualHost("/");
//獲取連接
Connection connection = connectionFactory.newConnection();
//創建一個通道
Channel channel = connection.createChannel();
//申明一個隊列
// durable: 如果爲true,消息持久化,服務器重啓後消息還存在
//exclusive:如果聲明獨佔隊列(僅限於此連接),則爲true
//autoDelete:如果聲明的是自動刪除隊列,則爲true(服務器將在不再使用時將其刪除)
//arguments:隊列的其他屬性(構造參數)
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
//TODO 工作模式,需要進行手動應答,誰應答塊,誰消費消息多
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
//自動應答 簡單隊列模式
// channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });
/*Consumer consumer = new DefaultConsumer(channel){
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws UnsupportedEncodingException {
String msg = new String(body,"utf-8");
System.out.println(msg);
}
} ;
// 3.監聽隊列
channel.basicConsume(QUEUE_NAME, true, consumer);*/
}
}
6.發佈訂閱模式[高級隊列]
6.1 消息投遞流程
-
.生產者投遞消息到
Exchange
交換機[消息管道聲明交換機,如果存在就創建一個交換機,並且把該管道綁定到該交換機] -
消費者通過管道聲明隊列,同時通過管道把該隊列和交換機進行綁定【沒有指定routingkey,相當於進行廣播.】
-
生產者投遞消息到交換機,該交換機會把消息投送到和交換機綁定的隊列,隊列在投遞或者消費者拉取消息
6.2 發佈訂閱原理
RoutingKey
路由key
、Exchange
交換機
p:product 、 x:exchange、多個隊列 ; queue 綁定 exchange
Fanout exchange(扇型交換機)將消息路由給綁定到它身上的所有隊列【默認交換機類型】
- 一個生產者,多個消費者
- 每一個消費者都有自己的一個隊列
- 生產者沒有直接發消息到隊列中,而是發送到交換機
- 每個消費者的隊列都綁定到交換機上
- 消息通過交換機到達每個消費者的隊列 【一對多,隊列只要綁定了該交換機,消息投遞到該交換機,隊列都會收到消息】
注意:交換機沒有存儲消息功能,如果消息發送到沒有綁定消費隊列的交換機,消息則丟失
6.3 演示代碼
public class RabbitmqUtil {
public static Connection getConnection() throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
//設置地址
connectionFactory.setPort(5672);
connectionFactory.setPassword("admin");
connectionFactory.setUsername("admin");
connectionFactory.setHost("192.168.1.6");
//設置虛擬主機
connectionFactory.setVirtualHost("/");
//獲取連接
Connection connection = connectionFactory.newConnection();
return connection;
}
}
/**
* 發佈/訂閱 publish / subscribe
*/
public class ProductMessage {
private static final String EXCHANGE_NAME = "my_fanout";
public static void main(String[] args) throws Exception{
Connection connection = RabbitmqUtil.getConnection();
//創建通道
Channel channel = connection.createChannel();
//聲明交換機,type = fanout,如果交換機不存在,聲明的同時也綁定交換機.
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
//創建消息
String msg = "exchange type fanout rabbitmq 發佈訂閱";
//發送消息
channel.basicPublish(EXCHANGE_NAME,"",null,msg.getBytes());
//關閉通道
channel.close();
connection.close();
}
}
public class ConsumerMessage01 {
private static final String EXCHANGE_NAME = "my_fanout";
private static final String QUEUE_SMS = "sms_queue_exchange_fanout";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitmqUtil.getConnection();
//創建通道
Channel channel = connection.createChannel();
//消費者申明隊列
//申明一個隊列
// durable: 如果爲true,消息持久化,服務器重啓後消息還存在
//exclusive:如果聲明獨佔隊列(僅限於此連接),則爲true
//autoDelete:如果聲明的是自動刪除隊列,則爲true(服務器將在不再使用時將其刪除)
//arguments:隊列的其他屬性(構造參數)
channel.queueDeclare(QUEUE_SMS, false, false, false, null);
//消費者綁定隊列
channel.queueBind(QUEUE_SMS,EXCHANGE_NAME,"");
//消費者監聽隊列
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
//TODO 工作模式,需要進行手動應答,誰應答塊,誰消費消息多
//channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
channel.basicConsume(QUEUE_SMS, true, deliverCallback, consumerTag -> { });
}
}
public class ProductMessage {
//聲明一個隊列
private static final String QUEUE_NAME = "test_rabbitmq_simple";
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
//設置地址
connectionFactory.setPort(5672);
connectionFactory.setPassword("admin");
connectionFactory.setUsername("admin");
connectionFactory.setHost("192.168.1.6");
//設置虛擬主機
connectionFactory.setVirtualHost("/");
//獲取連接
Connection connection = connectionFactory.newConnection();
//創建一個通道
Channel channel = connection.createChannel();
//申明一個隊列
// durable: 如果爲true,消息持久化,服務器重啓後消息還存在
//exclusive:如果聲明獨佔隊列(僅限於此連接),則爲true
//autoDelete:如果聲明的是自動刪除隊列,則爲true(服務器將在不再使用時將其刪除)
//arguments:隊列的其他屬性(構造參數)
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String msg = "生產者發送消息發送到隊列:"+QUEUE_NAME+"中 ";
/*
發佈消息:
exchange:交換機
queue_name:隊列名
props:消息路由頭等的其他屬性
body:消息體,二進制數組
*/
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
channel.close();
connection.close();
}
}
7.路由模式
路由模式:發送消息到交換機並且要指定路由key ,消費者將隊列綁定到交換機時需要指定路由key
Direct exchange(直連交換機)是根據消息攜帶的路由鍵(routing key)將消息投遞給對應隊列的
exchange_type = direct
路由模式,exchange 會根據routingkey 進行投遞消息到隊列中。
- 消息投遞, 管道申明交換機的時候指定routingkey
- 消費者通過管道聲明隊列,同時通過管道把該隊列和交換機通過routingkey 進行綁定
- 生產者投遞消息到交換機,交換機根據routingkey 進行投遞到指定的的隊列。
一個隊列可以綁定多個routingkey
7.1 演示代碼
/**
* 發佈/訂閱 publish / subscribe
*/
public class ProductMessage {
private static final String EXCHANGE_NAME = "my_direct";
private static final String ROUTING_KEY = "routing_key";
public static void main(String[] args) throws Exception{
Connection connection = RabbitmqUtil.getConnection();
//創建通道
Channel channel = connection.createChannel();
//聲明交換機,type = fanout,如果交換機不存在,聲明的同時也綁定交換機.
channel.exchangeDeclare(EXCHANGE_NAME,"direct");
//創建消息
String msg = "exchange type direct rabbitmq 路由策略routingkey";
//發送消息
channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY,null,msg.getBytes());
//關閉通道
channel.close();
connection.close();
}
}
public class ConsumerMessage01 {
private static final String EXCHANGE_NAME = "my_direct";
private static final String QUEUE_SMS = "sms_queue_exchange_direct";
private static final String ROUTING_KEY = "routing_key";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitmqUtil.getConnection();
//創建通道
Channel channel = connection.createChannel();
//消費者申明隊列
//申明一個隊列
// durable: 如果爲true,消息持久化,服務器重啓後消息還存在
//exclusive:如果聲明獨佔隊列(僅限於此連接),則爲true
//autoDelete:如果聲明的是自動刪除隊列,則爲true(服務器將在不再使用時將其刪除)
//arguments:隊列的其他屬性(構造參數)
channel.queueDeclare(QUEUE_SMS, false, false, false, null);
//消費者綁定隊列 綁定routingkey ,可以綁定多個
//channel.queueBind(QUEUE_SMS,EXCHANGE_NAME,ROUTING_KEY_SMS);
channel.queueBind(QUEUE_SMS,EXCHANGE_NAME,ROUTING_KEY);
//消費者監聽隊列
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
//TODO 工作模式,需要進行手動應答,誰應答塊,誰消費消息多
//channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
channel.basicConsume(QUEUE_SMS, true, deliverCallback, consumerTag -> { });
}
}
public class ConsumerMessage02 {
private static final String EXCHANGE_NAME = "my_direct";
private static final String ROUTING_KEY = "routing_key";
private static final String QUEUE_EMAIL = "email_queue_exchange_direct";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitmqUtil.getConnection();
//創建通道
Channel channel = connection.createChannel();
//消費者申明隊列
//申明一個隊列
// durable: 如果爲true,消息持久化,服務器重啓後消息還存在
//exclusive:如果聲明獨佔隊列(僅限於此連接),則爲true
//autoDelete:如果聲明的是自動刪除隊列,則爲true(服務器將在不再使用時將其刪除)
//arguments:隊列的其他屬性(構造參數)
channel.queueDeclare(QUEUE_EMAIL, false, false, false, null);
//消費者綁定隊列
//channel.queueBind(QUEUE_EMAIL,EXCHANGE_NAME,ROUTING_KEY);
channel.queueBind(QUEUE_EMAIL,EXCHANGE_NAME,"");
//消費者監聽隊列
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
//TODO 工作模式,需要進行手動應答,誰應答塊,誰消費消息多
// channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
channel.basicConsume(QUEUE_EMAIL, true, deliverCallback, consumerTag -> { });
}
}
consumer02沒有綁定routingkey,consumer01 綁定routingkey 。product申明交換機的同時指定routingkey
8.Topics模式
此模式實在路由key模式的基礎上,使用了通配符來管理消費者接收消息
生產者P發送消息到交換機X,type=topic,交換機根據綁定隊列的routing key的值進行通配符匹配;
符號#
:匹配一個或者多個詞lazy.# 可以匹配lazy.irs或者lazy.irs.cor
符號*
:只能匹配一個詞lazy. 可以匹配lazy.irs或者lazy.cor
9.交換機類型
- Direct exchange(直連交換機)是根據消息攜帶的路由鍵(routing key)將消息投遞給對應隊列的
- Fanout exchange(扇型交換機)將消息路由給綁定到它身上的所有隊列
- Topic exchange(主題交換機)隊列通過路由鍵綁定到交換機上,然後,交換機根據消息裏的路由值,將消息路由給一個或多個綁定隊列
- Headers exchange(頭交換機)類似主題交換機,但是頭交換機使用多個消息屬性來代替路由鍵建立路由規則。通過判斷消息頭的值能否與指定的綁定相匹配來確立路由規則。
**
* 發佈/訂閱 publish / subscribe
*/
public class ProductMessage {
private static final String EXCHANGE_NAME = "my_topic";
private static final String ROUTING_KEY = "routing_key_los.sms";
public static void main(String[] args) throws Exception{
Connection connection = RabbitmqUtil.getConnection();
//創建通道
Channel channel = connection.createChannel();
//聲明交換機,type = fanout, direct topic如果交換機不存在,聲明的同時也綁定交換機.
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
//創建消息
String msg = "exchange type topic rabbitmq routing_key_los.sms";
//發送消息
channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY,null,msg.getBytes());
//關閉通道
channel.close();
connection.close();
}
}
public class ConsumerMessage01 {
private static final String EXCHANGE_NAME = "my_topic";
private static final String QUEUE_SMS = "sms_queue_exchange_topic";
private static final String ROUTING_KEY = "routing_key_los.#";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitmqUtil.getConnection();
//創建通道
Channel channel = connection.createChannel();
//消費者申明隊列
//申明一個隊列
// durable: 如果爲true,消息持久化,服務器重啓後消息還存在
//exclusive:如果聲明獨佔隊列(僅限於此連接),則爲true
//autoDelete:如果聲明的是自動刪除隊列,則爲true(服務器將在不再使用時將其刪除)
//arguments:隊列的其他屬性(構造參數)
channel.queueDeclare(QUEUE_SMS, false, false, false, null);
//消費者綁定隊列 綁定routingkey ,可以綁定多個
channel.queueBind(QUEUE_SMS,EXCHANGE_NAME,ROUTING_KEY);
//消費者監聽隊列
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
//TODO 工作模式,需要進行手動應答,誰應答塊,誰消費消息多
//channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
channel.basicConsume(QUEUE_SMS, true, deliverCallback, consumerTag -> { });
}
}
public class ConsumerMessage02 {
private static final String EXCHANGE_NAME = "my_topic";
private static final String ROUTING_KEY = "routing_key_los.#";
private static final String QUEUE_EMAIL = "email_queue_exchange_topic";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitmqUtil.getConnection();
//創建通道
Channel channel = connection.createChannel();
//消費者申明隊列
//申明一個隊列
// durable: 如果爲true,消息持久化,服務器重啓後消息還存在
//exclusive:如果聲明獨佔隊列(僅限於此連接),則爲true
//autoDelete:如果聲明的是自動刪除隊列,則爲true(服務器將在不再使用時將其刪除)
//arguments:隊列的其他屬性(構造參數)
channel.queueDeclare(QUEUE_EMAIL, false, false, false, null);
//消費者綁定隊列
channel.queueBind(QUEUE_EMAIL,EXCHANGE_NAME,ROUTING_KEY);
//消費者監聽隊列
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
//TODO 工作模式,需要進行手動應答,誰應答塊,誰消費消息多
// channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
channel.basicConsume(QUEUE_EMAIL, true, deliverCallback, consumerTag -> { });
}
}
二、Rabbitmq消息確認機制
- 消息應答模式ACK,消費端默認是自動應答
- 生產者投遞消息,如何確保消息投遞成功?
- 如果Rabbitmq服務器宕機,消息會丟失嗎? RbbitMQ消息會進行持久化.
// durable: 如果爲true,消息持久化,服務器重啓後消息還存在
//exclusive:如果聲明獨佔隊列(僅限於此連接),則爲true
//autoDelete:如果聲明的是自動刪除隊列,則爲true(服務器將在不再使用時將其刪除)
//arguments:隊列的其他屬性(構造參數)
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
1.AMQP事物機制
- 事務模式:
- txSelect 將當前channel設置爲transaction模式
- txCommit 提交當前事務
- txRollback 事務回滾
public class ProductMessage {
//聲明一個隊列
private static final String QUEUE_NAME = "test_rabbitmq_simple_tx";
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
//設置地址
connectionFactory.setPort(5672);
connectionFactory.setPassword("admin");
connectionFactory.setUsername("admin");
connectionFactory.setHost("192.168.1.6");
//設置虛擬主機
connectionFactory.setVirtualHost("/");
//獲取連接
Connection connection = connectionFactory.newConnection();
//創建一個通道
Channel channel = connection.createChannel();
//申明一個隊列
// durable: 如果爲true,消息持久化,服務器重啓後消息還存在
//exclusive:如果聲明獨佔隊列(僅限於此連接),則爲true
//autoDelete:如果聲明的是自動刪除隊列,則爲true(服務器將在不再使用時將其刪除)
//arguments:隊列的其他屬性(構造參數)
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
try {
channel.txSelect();//開啓事物
String msg = "生產者發送消息發送到隊列:" + QUEUE_NAME + "中 ";
/*
發佈消息:
exchange:交換機
queue_name:隊列名
props:消息路由頭等的其他屬性
body:消息體,二進制數組
*/
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
//TODO 發送完消息之後 ,出現int i = 1/0的錯誤 ,消息會發送到rabbitmq服務器中
int i = 1 / 0;
channel.txCommit();//提交事物
} catch (Exception e) {
e.printStackTrace();
channel.txRollback();//回滾事物
} finally {
channel.close();
connection.close();
}
}
}
2.Confirm 模式
發送消息確認:用來確認生產者 producer
將消息發送到 broker
,broker
上的交換機 exchange
再投遞給隊列 queue
的過程中,消息是否成功投遞。
-
消息從
producer
到rabbitmq broker
有一個confirmCallback
確認模式。 -
消息從
exchange
到queue
投遞失敗有一個returnCallback
退回模式。 -
我們可以利用這兩個
Callback
來確保消的100%送達。
配置文件中配置開啓confirmcallback / return callback 模式
三、Springboot整合RabbitMQ
1.SpringBoot整合RabbitMQ
2.消費者消費消息拋出異常
消費端如果出現業務上的異常,比如int i = 1/0
,消費端默認會進行重試。RabbitMQ服務器上的消息沒有被消費端消費。補償機制是隊列服務器(RabbitMQ服務器)發送的。
@RabbitListener 註解.底層使用AOP進行攔截,只要該方法沒有拋出異常。會自動提交事物,RabbitMQ會刪除消息。如果被AOP異常通知攔截,補貨異常信息,會自動實現補償機制,一致補償到不拋出異常,該消息一致會緩存在RabbitMQ服務器上緩存。
修改補償機制,默認間隔5s重試.可以在配置文件中配置重試時間間隔和重試次數.
listener:
simple:
retry:
####開啓消費者重試
enabled: true
####最大重試次數
max-attempts: 5
####重試間隔次數
initial-interval: 3000
重試了配置的重試次數之後,就放棄消息重試,如果程序還在報異常,需要我們把消息轉入到死信隊列對,或者不用後續處理,RabbitMQ會把該消息刪除。
3.重試機制
- 場景:
- 消費者獲取到消息後,調用第三方接口,但接口暫時無法訪問,是否需要重試? (需要重試機制)
- 比如 郵件消費者接收到消息,調用第三方郵件接口(使用http協議,比如HttpUtils工具類發送請求)
- 消費者獲取到消息後,拋出數據轉換異常,是否需要重試?(不需要重試機制)需要發佈進行解決。
- 如果消費者代碼拋出異常是需要發佈新版本才能解決的問題,那麼不需要重試,重試也無濟於事。應該採用日誌記錄+定時任務job健康檢查+人工進行補償
- 消費者獲取到消息後,調用第三方接口,但接口暫時無法訪問,是否需要重試? (需要重試機制)
3.1 調用第三方接口自動實現補償機制.
利用RabbitMQ的重試機制,去重複調用第三方接口。比如:消費者消費消息拋出異常處理的原理.
3.2 消費端如何解決冪等性
- 生產者在發送消息的時候的需要設置一個全局唯一的ID放到消息頭中,作爲消息標識。同時存一份在redis中。消費端,從消息頭獲取消息ID,和緩存中取出該ID,並且刪除該ID,然後進行比較。如果相等,進行下一步操作。
- 使用業務狀態進行排除冪等性。比如訂單狀態、支付狀態
四、死信隊列
1.死信隊列場景
- 隊列長度已滿
- 消費者拒絕消費消息,或者消費值沒有手動應答
- 消息有過期時間,消息過期了。
在定義業務隊列的時候,可以考慮指定一個死信交換機,並綁定一個死信隊列,當消息變成死信時,該消息就會被髮送到該死信隊列上,這樣就方便我們查看消息失敗的原因了
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false); 丟棄消息
2.定義業務(普通)隊列的時候指定參數
x-dead-letter-exchange: 用來設置死信後發送的交換機
x-dead-letter-routing-key:用來設置死信的routingKey
3.死信隊列環境搭建
生產者
@Component
public class FanoutConfig {
/**
* 定義死信隊列相關信息
*/
public final static String deadQueueName = "dead_queue";
public final static String deadRoutingKey = "dead_routing_key";
public final static String deadExchangeName = "dead_exchange";
/**
* 死信隊列 交換機標識符
*/
public static final String DEAD_LETTER_QUEUE_KEY = "x-dead-letter-exchange";
/**
* 死信隊列交換機綁定鍵標識符
*/
public static final String DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";
// 郵件隊列
private String FANOUT_EMAIL_QUEUE = "fanout_email_queue";
// 短信隊列
private String FANOUT_SMS_QUEUE = "fanout_sms_queue";
// fanout 交換機
private String EXCHANGE_NAME = "fanoutExchange";
// 1.定義郵件隊列
@Bean
public Queue fanOutEamilQueue() {
// 將普通隊列綁定到死信隊列交換機上
Map<String, Object> args = new HashMap<>(2);
args.put(DEAD_LETTER_QUEUE_KEY, deadExchangeName);
args.put(DEAD_LETTER_ROUTING_KEY, deadRoutingKey);
Queue queue = new Queue(FANOUT_EMAIL_QUEUE, true, false, false, args);
return queue;
}
// 2.定義短信隊列
@Bean
public Queue fanOutSmsQueue() {
return new Queue(FANOUT_SMS_QUEUE);
}
// 2.定義交換機
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange(EXCHANGE_NAME);
}
// 3.隊列與交換機綁定郵件隊列
@Bean
Binding bindingExchangeEamil(Queue fanOutEamilQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanOutEamilQueue).to(fanoutExchange);
}
// 4.隊列與交換機綁定短信隊列
@Bean
Binding bindingExchangeSms(Queue fanOutSmsQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanOutSmsQueue).to(fanoutExchange);
}
/**
* 配置死信隊列
*
* @return
*/
@Bean
public Queue deadQueue() {
Queue queue = new Queue(deadQueueName, true);
return queue;
}
@Bean
public DirectExchange deadExchange() {
return new DirectExchange(deadExchangeName);
}
@Bean
public Binding bindingDeadExchange(Queue deadQueue, DirectExchange deadExchange) {
return BindingBuilder.bind(deadQueue).to(deadExchange).with(deadRoutingKey);
}
}
消費者配置
@RabbitListener(queues = "fanout_email_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
System.out.println("郵件消費者獲取生產者消息msg:" + msg + ",消息id:" + messageId);
JSONObject jsonObject = JSONObject.parseObject(msg);
Integer timestamp = jsonObject.getInteger("timestamp");
try {
int result = 1 / timestamp;
System.out.println("result:" + result);
// 通知mq服務器刪除該消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
// // 丟棄該消息
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
@Component
public class DeadConsumer {
@RabbitListener(queues = "dead_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
System.out.println("死信郵件消費者獲取生產者消息msg:" + msg + ",消息id:" + messageId);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
五、MQ解決分佈式事務
MQ解決分佈式事務三個重要概念
-
確保生產者消息一定要投遞到MQ服務器中 Confirm機制
-
確保消費者能夠正確的消費消息,採用手動ACK(注意冪等)
-
如何保證第一個事務一定要創建成功(在創建一個補單的隊列,綁定同一個交換機,檢查訂單數據是否已經創建在數據庫中 實現補償機制)
生產者 一定確保消息投遞到MQ服務器(使用)
補償隊列