一、RabbitMQ高級
1. 過期時間TTL
過期時間TTL表示可以對消息設置預期的時間,在這個時間內都可以被消費者接收穫取;過了之後消息將自動被刪除。RabbitMQ可以對消息和隊列設置TTL。
目前有兩種方法可以設置:
- 第一種方法是通過隊列屬性設置,隊列中所有消息都有相同的過期時間。
- 第二種方法是對消息進行單獨設置,每條消息TTL可以不同。
如果上述兩種方法同時使用,則消息的過期時間以兩者之間TTL較小的那個數值爲準。消息在隊列的生存時間一旦超過設置的TTL值,就稱爲dead message被投遞到死信隊列, 消費者將無法再收到該消息。
1.1. 設置隊列TTL
在 spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml
文件中添加如下內容:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!--定義過期隊列及其屬性,不存在則自動創建-->
<rabbit:queue id="my_ttl_queue" name="my_ttl_queue" auto-declare="true">
<rabbit:queue-arguments>
<!--投遞到該隊列的消息如果沒有消費都將在6秒之後被刪除-->
<entry key="x-message-ttl" value-type="long" value="6000"/>
</rabbit:queue-arguments>
</rabbit:queue>
</beans>
然後在測試類 spring-rabbitmq-producer\src\test\java\top\onefine\rabbitmq\ProducerTest.java
中編寫如下方法發送消息到上述定義的隊列:
package top.onefine.rabbitmq;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author one fine<br/>
*/
@SpringBootTest
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 過期隊列消息
* 投遞到該隊列的消息如果沒有消費都將在6秒之後被刪除
*/
@Test
public void ttlQueueTest() {
//路由鍵與隊列同名
rabbitTemplate.convertAndSend("my_ttl_queue", "發送到過期隊列my_ttl_queue,6秒內不消費則不能再被消費。");
}
/**
* 過期消息
* 該消息投遞任何交換機或隊列中的時候;如果到了過期時間則將從該隊列中刪除
*/
@Test
public void ttlMessageTest() {
MessageProperties messageProperties = new MessageProperties();
//設置消息的過期時間,5秒
messageProperties.setExpiration("5000");
Message message = new Message("測試過期消息,5秒鐘過期".getBytes(), messageProperties);
//路由鍵與隊列同名
rabbitTemplate.convertAndSend("my_ttl_queue", message);
}
}
參數 x-message-ttl 的值 必須是非負 32 位整數 (0 <= n <= 2^32-1) ,以毫秒爲單位表示 TTL 的值。這樣,值 6000 表示存在於 隊列 中的當前 消息 將最多隻存活 6 秒鐘。
如果不設置TTL,則表示此消息不會過期。如果將TTL設置爲0,則表示除非此時可以直接將消息投遞到消費者,否則該消息會被立即丟棄。
1.2. 設置消息TTL
消息的過期時間;只需要在發送消息(可以發送到任何隊列,不管該隊列是否屬於某個交換機)的時候設置過期時間即可。在測試類中編寫如下方法發送消息並設置過期時間到隊列:
/**
* 過期消息
* 該消息投遞任何交換機或隊列中的時候;如果到了過期時間則將從該隊列中刪除
*/
@Test
public void ttlMessageTest(){
MessageProperties messageProperties = new MessageProperties();
//設置消息的過期時間,5秒
messageProperties.setExpiration("5000");
Message message = new Message("測試過期消息,5秒鐘過期".getBytes(), messageProperties);
//路由鍵與隊列同名
rabbitTemplate.convertAndSend("my_ttl_queue", message);
}
expiration 字段以微秒爲單位表示 TTL 值。且與 x-message-ttl 具有相同的約束條件。因爲 expiration 字段必須爲字符串類型,broker 將只會接受以字符串形式表達的數字。
當同時指定了 queue 和 message 的 TTL 值,則兩者中較小的那個纔會起作用。
注:項目中使用:@ImportResource("classpath:/spring/spring-rabbitmq.xml")
導入配置文件使rabbitmq配置生效
package top.onefine.rabbitmq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;
/**
* 生產者的啓動類
*/
@SpringBootApplication
@ImportResource("classpath:/spring/spring-rabbitmq.xml") // 導入配置文件使rabbitmq配置生效
public class ProducerApplication {
public static void main(String[] args) {
SpringApplication.run(ProducerApplication.class, args);
}
}
2. 死信隊列
DLX,全稱爲Dead-Letter-Exchange , 可以稱之爲死信交換機,也有人稱之爲死信郵箱。當消息在一個隊列中變成死信(dead message)之後,它能被重新發送到另一個交換機中,這個交換機就是DLX ,綁定DLX的隊列就稱之爲死信隊列。
消息變成死信,可能是由於以下的原因:
- 消息被拒絕
- 消息過期
- 隊列達到最大長度
DLX也是一個正常的交換機,和一般的交換機沒有區別,它能在任何的隊列上被指定,實際上就是設置某一個隊列的屬性。當這個隊列中存在死信時,Rabbitmq就會自動地將這個消息重新發布到設置的DLX上去,進而被路由到另一個隊列,即死信隊列。
要想使用死信隊列,只需要在定義隊列的時候設置隊列參數 x-dead-letter-exchange
指定交換機即可。
具體步驟如下面的章節。
2.1. 定義死信交換機
在 spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml
文件中添加如下內容:
<!--定義定向交換機中的持久化死信隊列,不存在則自動創建-->
<rabbit:queue id="my_dlx_queue" name="my_dlx_queue" auto-declare="true"/>
<!--定義廣播類型交換機;並綁定上述兩個隊列-->
<rabbit:direct-exchange id="my_dlx_exchange" name="my_dlx_exchange" auto-declare="true">
<rabbit:bindings>
<!--綁定路由鍵my_ttl_dlx、my_max_dlx,可以將過期的消息轉移到my_dlx_queue隊列-->
<rabbit:binding key="my_ttl_dlx" queue="my_dlx_queue"/>
<rabbit:binding key="my_max_dlx" queue="my_dlx_queue"/>
</rabbit:bindings>
</rabbit:direct-exchange>
2.2. 隊列設置死信交換機
爲了測試消息在過期、隊列達到最大長度後都將被投遞死信交換機上;所以添加配置如下:
在 spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml
文件中添加如下內容:
<!--定義過期隊列及其屬性,不存在則自動創建-->
<rabbit:queue id="my_ttl_dlx_queue" name="my_ttl_dlx_queue" auto-declare="true">
<rabbit:queue-arguments>
<!--投遞到該隊列的消息如果沒有消費都將在6秒之後被投遞到死信交換機-->
<entry key="x-message-ttl" value-type="long" value="6000"/>
<!--設置當消息過期後投遞到對應的死信交換機-->
<entry key="x-dead-letter-exchange" value="my_dlx_exchange"/>
</rabbit:queue-arguments>
</rabbit:queue>
<!--定義限制長度的隊列及其屬性,不存在則自動創建-->
<rabbit:queue id="my_max_dlx_queue" name="my_max_dlx_queue" auto-declare="true">
<rabbit:queue-arguments>
<!--投遞到該隊列的消息最多2個消息,如果超過則最早的消息被刪除投遞到死信交換機-->
<entry key="x-max-length" value-type="long" value="2"/>
<!--設置當消息過期後投遞到對應的死信交換機-->
<entry key="x-dead-letter-exchange" value="my_dlx_exchange"/>
</rabbit:queue-arguments>
</rabbit:queue>
<!--定義定向交換機 根據不同的路由key投遞消息-->
<rabbit:direct-exchange id="my_normal_exchange" name="my_normal_exchange" auto-declare="true">
<rabbit:bindings>
<rabbit:binding key="my_ttl_dlx" queue="my_ttl_dlx_queue"/>
<rabbit:binding key="my_max_dlx" queue="my_max_dlx_queue"/>
</rabbit:bindings>
</rabbit:direct-exchange>
2.3. 消息過期的死信隊列測試
1)發送消息代碼
添加 spring-rabbitmq-producer\src\test\java\top\onefine\rabbitmq\ProducerTest.java
方法
/**
* 過期消息投遞到死信隊列
* 投遞到一個正常的隊列,但是該隊列有設置過期時間,到過期時間之後消息會被投遞到死信交換機(隊列)
*/
@Test
public void dlxTTLMessageTest(){
rabbitTemplate.convertAndSend("my_normal_exchange", "my_ttl_dlx", "測試過期消息;6秒過期後會被投遞到死信交換機");
}
2)在rabbitMQ管理界面中結果
未過期:
略,自個寫代碼觀察去。
過期後:
略,自個寫代碼觀察去。
3)流程
具體因爲隊列消息過期而被投遞到死信隊列的流程:
2.4. 消息過長的死信隊列測試
1)發送消息代碼
添加 spring-rabbitmq-producer\src\test\java\top\onefine\rabbitmq\ProducerTest.java
方法
package top.onefine.rabbitmq;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author one fine<br/>
*/
@SpringBootTest
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 過期消息投遞到死信隊列
* 投遞到一個正常的隊列,但是該隊列有設置過期時間,到過期時間之後消息會被投遞到死信交換機(隊列)
*/
@Test
public void dlxTTLMessageTest() {
rabbitTemplate.convertAndSend("my_normal_exchange", "my_ttl_dlx", "測試過期消息;6秒過期後會被投遞到死信交換機");
}
/**
* 消息長度超過2,會投遞到死信隊列中
* 投遞到一個正常的隊列,但是該隊列有設置最大消息數,到最大消息數之後隊列中最早的消息會被投遞到死信交換機(隊列)
*/
@Test
public void dlxMaxMessageTest() {
for (int i = 1; i <= 3; i++) {
rabbitTemplate.convertAndSend("my_normal_exchange",
"my_max_dlx",
"發送消息" + i + ";消息長度超過2最早的消息會被投遞到死信隊列中");
}
}
}
2)在rabbitMQ管理界面中結果
上面發送的3條消息中的第1條消息會被投遞到死信隊列中(如果啓動了消費者,那麼隊列消息很快會被取走消費掉);
3)消費者接收死信隊列消息
與過期消息投遞到死信隊列的代碼和配置是共用的,並不需要重新編寫。
4)流程
消息超過隊列最大消息長度而被投遞到死信隊列的流程在前面的圖中已包含。
3. 延遲隊列
延遲隊列存儲的對象是對應的延遲消息;所謂“延遲消息” 是指當消息被髮送以後,並不想讓消費者立刻拿到消息,而是等待特定時間後,消費者才能拿到這個消息進行消費。
在RabbitMQ中延遲隊列可以通過 過期時間
+ 死信隊列
來實現;具體如下流程圖所示:
在上圖中;分別設置了兩個5秒、10秒的過期隊列,然後等到時間到了則會自動將這些消息轉移投遞到對應的死信隊列中,然後消費者再從這些死信隊列接收消息就可以實現消息的延遲接收。
延遲隊列的應用場景;如:
- 在電商項目中的支付場景;如果在用戶下單之後的幾十分鐘內沒有支付成功;那麼這個支付的訂單算是支付失敗,要進行支付失敗的異常處理(將庫存加回去),這時候可以通過使用延遲隊列來處理
- 在系統中如有需要在指定的某個時間之後執行的任務都可以通過延遲隊列處理
4. 消息確認機制
確認並且保證消息被送達,提供了兩種方式:發佈確認和事務。(兩者不可同時使用)在channel爲事務時,不可引入確認模式;同樣channel爲確認模式下,不可使用事務。
4.1 發佈確認
有兩種方式:消息發送成功確認和消息發送失敗回調。
- 消息發送成功確認
在spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml
connectionFactory 中啓用消息確認:
<!-- publisher-confirms="true" 表示:啓用了消息確認 -->
<rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"
publisher-confirms="true" />
配置消息確認回調方法如下:
<!-- 消息回調處理類 -->
<bean id="confirmCallback" class="top.onefine.rabbitmq.MsgSendConfirmCallBack"/>
<!--定義rabbitTemplate對象操作可以在代碼中方便發送消息-->
<!-- confirm-callback="confirmCallback" 表示:消息失敗回調 -->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"
confirm-callback="confirmCallback"/>
消息確認回調方法com.itheima.rabbitmq.MsgSendConfirmCallBack如下:
package top.onefine.rabbitmq;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
/**
* @author one fine<br/>
*
* 消息確認與回退
*/
public class MsgSendConfirmCallBack implements RabbitTemplate.ConfirmCallback {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
System.out.println("消息確認成功....");
} else {
//處理丟失的消息
System.out.println("消息確認失敗," + cause);
}
}
}
功能測試如下:
發送消息
com.itheima.rabbitmq.ProducerTest#queueTest
package top.onefine.rabbitmq;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author one fine<br/>
*/
@SpringBootTest
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 消息確認
*/
@Test
public void queueTest(){
//路由鍵與隊列同名
rabbitTemplate.convertAndSend("spring_queue", "只發隊列spring_queue的消息。");
}
}
管理界面確認消息發送成功
消息確認回調
- 消息發送失敗回調
在spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml
connectionFactory 中啓用回調:
<!-- publisher-returns="true" 表示:啓用了失敗回調 -->
<rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"
publisher-returns="true" />
配置消息失敗回調方法如下:
注意:同時需配置mandatory=“true”,否則消息則丟失
<!-- 消息失敗回調類 -->
<bean id="sendReturnCallback" class="top.onefine.rabbitmq.MsgSendReturnCallback"/>
<!-- return-callback="sendReturnCallback" 表示:消息失敗回調 ,同時需配置mandatory="true",否則消息則丟失-->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"
confirm-callback="confirmCallback" return-callback="sendReturnCallback"
mandatory="true"/>
消息失敗回調方法com.itheima.rabbitmq.MsgSendReturnCallback如下:
package top.onefine.rabbitmq;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
/**
* @author one fine<br/>
*/
public class MsgSendReturnCallback implements RabbitTemplate.ReturnCallback {
@Override
public void returnedMessage(Message message, int i, String s, String s1, String s2) {
String msgJson = new String(message.getBody());
System.out.println("Returned Message:" + msgJson);
}
}
功能測試如下:
模擬消息發送失敗
com.itheima.rabbitmq.ProducerTest#testFailQueueTest
package top.onefine.rabbitmq;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author one fine<br/>
*/
@SpringBootTest
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 由於queue錯誤,所以會確認失敗
*/
@Test
public void testFailQueueTest() {
//exchange 正確,queue 錯誤 ,confirm被回調, ack=true; return被回調 replyText:NO_ROUTE
rabbitTemplate.convertAndSend("test_fail_exchange", "", "測試消息發送失敗進行確認應答。");
}
}
失敗回調結果如下:
完整的配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!-- publisher-confirms="true" 表示:啓用了消息確認 -->
<rabbit:connection-factory id="connectionFactory" host="127.0.0.1"
port="5672"
username="guest"
password="123456"
virtual-host="/"
publisher-confirms="true"
publisher-returns="true"/>
<!-- 消息回調處理類 -->
<bean id="confirmCallback" class="top.onefine.rabbitmq.MsgSendConfirmCallBack"/>
<!-- 消息失敗回調類 -->
<bean id="sendReturnCallback" class="top.onefine.rabbitmq.MsgSendReturnCallback"/>
<!--定義rabbitTemplate對象操作可以在代碼中方便發送消息-->
<!-- confirm-callback="confirmCallback" 表示:消息失敗回調 -->
<!-- return-callback="sendReturnCallback" 表示:消息失敗回調 ,同時需配置mandatory="true",否則消息則丟失-->
<rabbit:template id="rabbitTemplate"
connection-factory="connectionFactory"
confirm-callback="confirmCallback"
return-callback="sendReturnCallback"
mandatory="true"/>
</beans>
4.2 事務支持
場景:業務處理伴隨消息的發送,業務處理失敗(事務回滾)後要求消息不發送。rabbitmq 使用調用者的外部事務,通常是首選,因爲它是非侵入性的(低耦合)。
外部事務的配置:spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml
<!-- channel-transacted="true" 表示:支持事務操作 -->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"
confirm-callback="confirmCallback" return-callback="sendReturnCallback"
channel-transacted="true" />
<!--平臺事務管理器-->
<bean id="transactionManager" class="org.springframework.amqp.rabbit.transaction.RabbitTransactionManager">
<property name="connectionFactory" ref="connectionFactory"/>
</bean>
- 模擬業務處理失敗的場景:
測試類或者測試方法上加入@Transactional註解
@Transactional
public class ProducerTest
package top.onefine.rabbitmq;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
/**
* @author one fine<br/>
*/
@SpringBootTest
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
@Transactional // 開啓事務
@SuppressWarnings("All")
// @Rollback(value = false) // spring中的測試方法執行後會自動回滾,所以在這裏需要手動指定回滾策略:關閉回滾操作
public void queueTest2(){
//路由鍵與隊列同名
rabbitTemplate.convertAndSend("spring_queue", "只發隊列spring_queue的消息--01。");
System.out.println("----------------dosoming:可以是數據庫的操作,也可以是其他業務類型的操作---------------");
//模擬業務處理失敗
System.out.println(1/0);
rabbitTemplate.convertAndSend("spring_queue", "只發隊列spring_queue的消息--02。");
}
}
完整的配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!-- publisher-confirms="true" 表示:啓用了消息確認 -->
<rabbit:connection-factory id="connectionFactory" host="127.0.0.1"
port="5672"
username="guest"
password="123456"
virtual-host="/"/>
<!-- 事務和確認機制不能共存 -->
<!-- publisher-confirms="true"-->
<!-- publisher-returns="true"/>-->
<!--定義rabbitTemplate對象操作可以在代碼中方便發送消息-->
<!-- channel-transacted="true" 表示:支持事務操作 -->
<rabbit:template id="rabbitTemplate"
connection-factory="connectionFactory"
mandatory="true"
channel-transacted="true"
/>
<!--平臺事務管理器-->
<bean id="transactionManager" class="org.springframework.amqp.rabbit.transaction.RabbitTransactionManager">
<property name="connectionFactory" ref="connectionFactory"/>
</bean>
</beans>
5. 消息追蹤
消息中心的消息追蹤需要使用Trace實現,Trace是Rabbitmq用於記錄每一次發送的消息,方便使用Rabbitmq的開發者調試、排錯。可通過插件形式提供可視化界面。Trace啓動後會自動創建系統Exchange:amq.rabbitmq.trace ,每個隊列會自動綁定該Exchange,綁定後發送到隊列的消息都會記錄到Trace日誌。
5.1 消息追蹤啓用與查看
以下是trace的相關命令和使用(要使用需要先rabbitmq啓用插件,再打開開關才能使用):
命令集 | 描述 |
---|---|
rabbitmq-plugins list | 查看插件列表 |
rabbitmq-plugins enable rabbitmq_tracing | rabbitmq啓用trace插件 |
rabbitmqctl trace_on | 打開trace的開關 |
rabbitmqctl trace_on -p one | 打開trace的開關(one爲需要日誌追蹤的vhost) |
rabbitmqctl trace_off | 關閉trace的開關 |
rabbitmq-plugins disable rabbitmq_tracing | rabbitmq關閉Trace插件 |
rabbitmqctl set_user_tags onefine administrator | 只有administrator的角色才能查看日誌界面 |
[root@onefine ~]# rabbitmq-plugins list
# ...
[root@onefine ~]# rabbitmq-plugins enable rabbitmq_tracing
# ...
[root@onefine ~]# rabbitmqctl trace_on
Starting tracing for vhost "/"
[root@onefine ~]# rabbitmqctl trace_on -p one
Starting tracing for vhost "one"
[root@onefine ~]#
安裝插件並開啓 trace_on 之後,會發現多個 exchange:amq.rabbitmq.trace ,類型爲:topic。
5.2 日誌追蹤
第一步:發送消息
package top.onefine.rabbitmq;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author one fine<br/>
*/
@SpringBootTest
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void queueTest2(){
//路由鍵與隊列同名
rabbitTemplate.convertAndSend("spring_queue", "消息追蹤...");
}
}
發送成功,web查看多了一條消息
第二步:查看trace
第三步:點擊Tracing查看Trace log files
第四步:點擊itcast-trace.log確認消息軌跡正確性
url:http://127.0.0.1:15672/api/trace-files/itcast-trace.log
瀏覽器截圖:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-9HbVKT8M-1592451812752)(assets/1572257077651.png)]
更新中…
二、RabbitMQ集羣
RabbitMQ這款消息隊列中間件產品本身是基於Erlang編寫,Erlang語言天生具備分佈式特性(通過同步Erlang集羣各節點的magic cookie來實現)。因此,RabbitMQ天然支持Clustering。這使得RabbitMQ本身不需要像ActiveMQ、Kafka那樣通過ZooKeeper分別來實現HA方案和保存集羣的元數據。集羣是保證可靠性的一種方式,同時可以通過水平擴展以達到增加消息吞吐量能力的目的。
在實際使用過程中多采取多機多實例部署方式,爲了便於同學們練習搭建,有時候你不得不在一臺機器上去搭建一個rabbitmq集羣,本章主要針對單機多實例這種方式來進行開展。
主要參考官方文檔:https://www.rabbitmq.com/clustering.html
1. 集羣搭建
1.1. 準備工作
配置的前提是你的rabbitmq可以運行起來,比如"ps aux|grep rabbitmq"你能看到相關進程,又比如運行“rabbitmqctl status”你可以看到類似如下信息,而不報錯:
執行"ps aux|grep rabbitmq"結果如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ZLNrh5S2-1592451875269)(assets/1565664545643.png)]
執行“rabbitmqctl status”結果如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-QwNnjwXQ-1592451875270)(assets/1565664577505.png)]
注意:確保RabbitMQ可以運行的,確保完成之後,把RabbitMQ停止,後臺看不到RabbitMQ的進程
搭建之前一定要把後臺的RabbitMQ的進程停止
1.2. 單機多實例搭建
目標:完成單機多實例的搭建
情景:假設有兩個rabbitmq節點,分別爲rabbit-1, rabbit-2,rabbit-1作爲主節點,rabbit-2作爲從節點。
啓動命令:RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit-1 rabbitmq-server -detached
結束命令:rabbitmqctl -n rabbit-1 stop
集羣啓動
第一步:啓動第一個節點rabbit-1,命令如下:
sudo RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit-1 rabbitmq-server start &
執行結果如下:0
itcast@Server-node:/$ sudo RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit-1 rabbitmq-server start
...............省略...................
########## Logs: /var/log/rabbitmq/rabbit-1.log
###### ## /var/log/rabbitmq/rabbit-1-sasl.log
##########
Starting broker...
completed with 7 plugins.
至此節點rabbit-1啓動完成。
第二步:啓動第二個節點rabbit-2,命令如下:
itcast@Server-node:/$ sudo RABBITMQ_NODE_PORT=5673 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15673}]" RABBITMQ_NODENAME=rabbit-2 rabbitmq-server start &
注意:web管理插件端口占用,所以還要指定其web插件佔用的端口號
RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15673}]"
執行結果如下:
itcast@Server-node:/$ sudo RABBITMQ_NODE_PORT=5673 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,1567
3}]" RABBITMQ_NODENAME=rabbit-2 rabbitmq-server start
..............省略..................
########## Logs: /var/log/rabbitmq/rabbit-2.log
###### ## /var/log/rabbitmq/rabbit-2-sasl.log
##########
Starting broker...
completed with 7 plugins.
至此節點rabbit-2啓動完成。
第三步:驗證啓動 “ps aux|grep rabbitmq”
rabbitmq 2022 2.7 0.4 5349380 77020 ? Sl 11:03 0:06 /usr/lib/erlang/erts-9.2/bin/beam.smp -W w -A 128 -P 1048576 -t 5000000 -stbt db -zdbbl 128000 -K true -B i -- -root /usr/lib/erlang -progname erl -- -home /var/lib/rabbitmq -- -pa /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.15/ebin -noshell -noinput -s rabbit boot -sname rabbit-1 -boot start_sasl -kernel inet_default_connect_options [{nodelay,true}] -rabbit tcp_listeners [{"auto",5672}] -sasl errlog_type error -sasl sasl_error_logger false -rabbit error_logger {file,"/var/log/rabbitmq/rabbit-1.log"} -rabbit sasl_error_logger {file,"/var/log/rabbitmq/rabbit-1-sasl.log"} -rabbit enabled_plugins_file "/etc/rabbitmq/enabled_plugins" -rabbit plugins_dir "/usr/lib/rabbitmq/plugins:/usr/lib/rabbitmq/lib/rabbitmq_server-3.6.15/plugins" -rabbit plugins_expand_dir "/var/lib/rabbitmq/mnesia/rabbit-1-plugins-expand" -os_mon start_cpu_sup false -os_mon start_disksup false -os_mon start_memsup false -mnesia dir "/var/lib/rabbitmq/mnesia/rabbit-1" -kernel inet_dist_listen_min 25672 -kernel inet_dist_listen_max 25672 start
rabbitmq 2402 4.2 0.4 5352196 77196 ? Sl 11:05 0:05 /usr/lib/erlang/erts-9.2/bin/beam.smp -W w -A 128 -P 1048576 -t 5000000 -stbt db -zdbbl 128000 -K true -B i -- -root /usr/lib/erlang -progname erl -- -home /var/lib/rabbitmq -- -pa /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.15/ebin -noshell -noinput -s rabbit boot -sname rabbit-2 -boot start_sasl -kernel inet_default_connect_options [{nodelay,true}] -rabbit tcp_listeners [{"auto",5673}] -sasl errlog_type error -sasl sasl_error_logger false -rabbit error_logger {file,"/var/log/rabbitmq/rabbit-2.log"} -rabbit sasl_error_logger {file,"/var/log/rabbitmq/rabbit-2-sasl.log"} -rabbit enabled_plugins_file "/etc/rabbitmq/enabled_plugins" -rabbit plugins_dir "/usr/lib/rabbitmq/plugins:/usr/lib/rabbitmq/lib/rabbitmq_server-3.6.15/plugins" -rabbit plugins_expand_dir "/var/lib/rabbitmq/mnesia/rabbit-2-plugins-expand" -os_mon start_cpu_sup false -os_mon start_disksup false -os_mon start_memsup false -mnesia dir "/var/lib/rabbitmq/mnesia/rabbit-2" -rabbitmq_management listener [{port,15673}] -kernel inet_dist_listen_min 25673 -kernel inet_dist_listen_max 25673 start
第四步:rabbit-1操作作爲主節點,命令集如下:
//停止應用
itcast@Server-node:/$ sudo rabbitmqctl -n rabbit-1 stop_app
Stopping rabbit application on node 'rabbit-1@Server-node'
//目的是清除節點上的歷史數據(如果不清除,無法將節點加入到集羣)
itcast@Server-node:/$ sudo rabbitmqctl -n rabbit-1 reset
Resetting node 'rabbit-1@Server-node'
//啓動應用
itcast@Server-node:/$ sudo rabbitmqctl -n rabbit-1 start_app
Starting node 'rabbit-1@Server-node'
第五步:rabbit2操作爲從節點,命令集如下:
//停止應用
itcast@Server-node:/$ sudo rabbitmqctl -n rabbit-2 stop_app
Stopping rabbit application on node 'rabbit-2@Server-node'
//目的是清除節點上的歷史數據(如果不清除,無法將節點加入到集羣)
itcast@Server-node:/$ sudo rabbitmqctl -n rabbit-2 reset
Resetting node 'rabbit-2@Server-node'
//將rabbit2節點加入到rabbit1(主節點)集羣當中【Server-node服務器的主機名】
itcast@Server-node:/$ sudo rabbitmqctl -n rabbit-2 join_cluster rabbit-1@'Server-node'
Clustering node 'rabbit-2@Server-node' with 'rabbit-1@Server-node'
//啓動應用
itcast@Server-node:/$ sudo rabbitmqctl -n rabbit-2 start_app
Starting node 'rabbit-2@Server-node'
第六步:驗證集羣狀態
itcast@Server-node:/$ sudo rabbitmqctl cluster_status -n rabbit-1
Cluster status of node 'rabbit-1@Server-node'
//集羣有兩個節點:rabbit-1@Server-node、rabbit-2@Server-node
[{nodes,[{disc,['rabbit-1@Server-node','rabbit-2@Server-node']}]},
{running_nodes,['rabbit-2@Server-node','rabbit-1@Server-node']},
{cluster_name,<<"[email protected]">>},
{partitions,[]},
{alarms,[{'rabbit-2@Server-node',[]},{'rabbit-1@Server-node',[]}]}]
第七步:Web監控
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-e3y5FmcD-1592451875273)(assets/1565666186483.png)]
總結:至此單機多實例集羣搭建完成
Tips:
如果採用多機部署方式,需讀取其中一個節點的cookie, 並複製到其他節點(節點之間通過cookie確定相互是否可通信)。cookie存放在/var/lib/rabbitmq/.erlang.cookie。
例如:主機名分別爲rabbit-1、rabbit-2
1、逐個啓動各節點
2、配置各節點的hosts文件( vim /etc/hosts)
ip1:rabbit-1
ip2:rabbit-2
其它步驟雷同單機部署方式
2. 集羣監控
在廣大的互聯網行業中RabbitMQ幾乎都會有集羣,那麼對於集羣的監控就成了企業生態中必不可少的一環。接下來我們來將講解主要的4種監控。
2.1. 管理界面監控
管理界面監控需要我們開啓對應的插件(rabbitmq-plugins enable rabbitmq_management)
然後訪問http://ip:15672
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-6F24drDA-1592451875273)(assets/1572492635332.png)]
在管理控制檯我們就可以直觀的看到集羣中的每一個節點是否正常,如果爲紅色則表示節點掛掉了,同時可以很方便的查看到各個節點的內存、磁盤等相關的信息,使用起來也是非常方便的。但是遺憾的該功能做的比較簡陋,沒有告警等一些列的個性化設置,同時如果想把他接入到公司其他的監控系統統一管理也是很難做到的,所以擴展性不強,一般在小型企業的小集羣中使用。
2.2. tracing日誌監控
對於企業級的應用開發來講,我們通常都會比較關注我們的消息,甚至很多的場景把消息的可靠性放在第一位,但是我們的MQ集羣難免會出現消息異常丟失或者客戶端無法發送消息等異常情況,此時爲了幫助開發人員快速的定位問題,我們就可以對消息的投遞和消費過程進行監控,而tracing日誌監控插件幫我們很好的實現了該功能,具體的實現參見7.5章節
2.3. 定製自己的監控系統
RabbitMQ提供了很豐富的restful風格的api接口,我們可以通過這些接口得到對應的集羣數據,此時我們就可以定製我們的監控系統。
HTTP API URL | HTTP 請求類型 | 接口含義 |
---|---|---|
/api/connections | GET | 獲取當前RabbitMQ集羣下所有打開的連接 |
/api/nodes | GET | 獲取當前RabbitMQ集羣下所有節點實例的狀態信息 |
/api/vhosts/{vhost}/connections | GET | 獲取某一個虛擬機主機下的所有打開的connection連接 |
/api/connections/{name}/channels | GET | 獲取某一個連接下所有的管道信息 |
/api/vhosts/{vhost}/channels | GET | 獲取某一個虛擬機主機下的管道信息 |
/api/consumers/{vhost} | GET | 獲取某一個虛擬機主機下的所有消費者信息 |
/api/exchanges/{vhost} | GET | 獲取某一個虛擬機主機下面的所有交換器信息 |
/api/queues/{vhost} | GET | 獲取某一個虛擬機主機下的所有隊列信息 |
/api/users | GET | 獲取集羣中所有的用戶信息 |
/api/users/{name} | GET/PUT/DELETE | 獲取/更新/刪除指定用戶信息 |
/api/users/{user}/permissions | GET | 獲取當前指定用戶的所有權限信息 |
/api/permissions/{vhost}/{user} | GET/PUT/DELETE | 獲取/更新/刪除指定虛擬主機下特定用戶的權限 |
/api/exchanges/{vhost}/{name}/publish | POST | 在指定的虛擬機主機和交換器上發佈一個消息 |
/api/queues/{vhost}/{name}/get | POST | 在指定虛擬機主機和隊列名中獲取消息,同時該動作會修改隊列狀態 |
/api/healthchecks/node/{node} | GET | 獲取指定節點的健康檢查狀態 |
更多API的相關信息和描述可以訪問http://ip:15672/api/
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-78WHw6EA-1592451875273)(assets/1572491850973.png)]
接下來我們使用RabbitMQ Http API接口來獲取集羣監控數據
- HttpClient以及Jackson的相關Jar
-
創建MonitorRabbitMQ類實現具體的代碼
package com.itheima.rabbitmq;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HttpEntity;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* RabbitMQ的監控
*/
public class MonitorRabbitMQ {
//RabbitMQ的HTTP API——獲取集羣各個實例的狀態信息,ip替換爲自己部署相應實例的
private static String RABBIT_NODES_STATUS_REST_URL = "http://192.168.13.111:15672/api/nodes";
//RabbitMQ的HTTP API——獲取集羣用戶信息,ip替換爲自己部署相應實例的
private static String RABBIT_USERS_REST_URL = "http://192.168.13.111:15672/api/users";
//rabbitmq的用戶名
private static String RABBIT_USER_NAME = "guest";
//rabbitmq的密碼
private static String RABBIT_USER_PWD = "guest";
public static void main(String[] args) {
try {
//step1.獲取rabbitmq集羣各個節點實例的狀態信息
Map<String, ClusterStatus> clusterMap =
fetchRabbtMQClusterStatus(RABBIT_NODES_STATUS_REST_URL, RABBIT_USER_NAME, RABBIT_USER_PWD);
//step2.打印輸出各個節點實例的狀態信息
for (Map.Entry entry : clusterMap.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
//step3.獲取rabbitmq集羣用戶信息
Map<String, User> userMap =
fetchRabbtMQUsers(RABBIT_USERS_REST_URL, RABBIT_USER_NAME, RABBIT_USER_PWD);
//step4.打印輸出rabbitmq集羣用戶信息
for (Map.Entry entry : userMap.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
} catch (IOException e) {
e.printStackTrace();
-
啓動測試
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-AR6TJu9F-1592451875275)(assets/1572492408012.png)]
2.4. Zabbix 監控RabbitMQ
Zabbix是一個基於WEB界面提供分佈式系統監視以及網絡監視功能的企業級開源解決方案,他也可以幫助我們搭建一個MQ集羣的監控系統,同時提供預警等功能,但是由於其搭建配置要求比較高一般都是由運維人員負責搭建,感興趣的同學可以訪問https://www.zabbix.com/ 官網進行了解學習。
三、RabbitMQ高可用集羣
1. RabbitMQ集羣架構模式
-
主備模式
用來實現RabbitMQ的高可用集羣,一般是在併發和數據不是特別多的時候使用,當主節點掛掉以後會從備份節點中選擇一個節點出來作爲主節點對外提供服務。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-gFSLRaTX-1592451926039)(assets/1572494240934.png)]
-
遠程模式
主要用來實現雙活,簡稱爲Shovel模式,所謂的Shovel模式就是讓我們可以把消息複製到不同的數據中心,讓兩個跨地域的集羣互聯。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-adAuPA4A-1592451926041)(assets/1572494294216.png)]
-
鏡像隊列模式
鏡像隊列也被稱爲Mirror隊列,主要是用來保證mq消息的可靠性的,他通過消息複製的方式能夠保證我們的消息100%不丟失,同時該集羣模式也是企業中使用最多的模式。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-QtT17fDn-1592451926043)(assets/1572494368606.png)]
-
多活模式
多活模式主要是用來實現異地數據複製,Shovel模式其實也可以實現,但是他的配置及其繁瑣同時還要受到版本的限制,所以如果做異地多活我們更加推薦使用多活模式,使用多活模式我們需要藉助federation插件來實現集羣與集羣之間或者節點與節點之前的消息複製,該模式被廣泛應用於餓了麼、美團、滴滴等企業。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Zdj8fF9j-1592451926045)(assets/1572494419652.png)]
-
集羣模式總結
主備模式下主節點提供讀寫,從節點不提供讀寫服務,只是負責提供備份服務,備份節點的主要功能是在主節點宕機時,完成自動切換 從–>主,同時因爲主備模式下始終只有一個對外提供服務那麼對於高併發的情況下該模式並不合適.
遠程模式可以讓我們實現異地多活的mq,但是現在已經有了更好的異地多活解決方案,所以在實際的項目中已經不推薦使用了
鏡像隊列模式可以讓我們的消息100%不丟失,同時可以結合HAProxy來實現高併發的業務場景所以在項目中使用得最多
2. 鏡像隊列集羣搭建
-
集羣節點規劃
ip地址 用途 主機名 192.168.13.101 mq 主節點 server1 192.168.13.102 mq 從節點 server2 192.168.13.103 mq 從節點 server3 192.168.13.104 HAProxy KeepAlive server4 192.168.13.105 HAProxy KeepAlive server5 特別注意: 每個同學的ip可能都是不一樣的,各個學員根據自己的ip進行規劃,同時特別注意主機名稱,因爲我們後面的命令中會使用到所以需要提前設置好。
需要配置主機名的映射:
vi /etc/hosts
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hQgvfYSN-1592451926046)(assets/1572507661664.png)]
-
複製主節點的.erlang.cookie文件到其他所有的從節點
-
停掉所有的MQ節點然後使用集羣的方式啓動
主節點:
- 將從節點添加到主節點的集羣中
- 查看集羣的狀態
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-O0vNHkrc-1592451926052)(assets/05查看集羣的狀態.png)]
-
訪問集羣中的任何一個節點的控制檯查看集羣情況
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-DULyIKf2-1592451926055)(assets/06管理控制檯查看集羣信息.png)]
-
設置鏡像隊列策略
-
在管控臺創建一個隊列然後發送一條消息查看其他節點是否接收到
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-J9Ev0Px4-1592451926057)(assets/07.png)]
-
修改集羣信息
-
總結
到此爲止我們的鏡像隊列的集羣就搭建完成了,主要注意.erlang.cookie文件的同步和集羣命令的書寫
3. HAProxy 實現鏡像隊列的負載均衡
-
HAProxy 簡介
HAProxy是一款提供高可用性、負載均衡以及基於TCP和HTTP應用的代理軟件,HAProxy是完全免費的、藉助HAProxy可以快速並且可靠的提供基於TCP和HTTP應用的代理解決方案。
HAProxy適用於那些負載較大的web站點,這些站點通常又需要會話保持或七層處理。
HAProxy可以支持數以萬計的併發連接,並且HAProxy的運行模式使得它可以很簡單安全的整合進架構中,同時可以保護web服務器不被暴露到網絡上。
-
HAProxy 的規劃(搭建2臺是爲後面做HAProxy高可用做準備)
ip 用途 主機名 192.168.13.104 HAProxy server4 192.168.13.105 HAProxy server5 -
HAProxy 的安裝
-
HAProxy 的配置
104機器ha配置
105機器ha配置
- 啓動HAproxy
-
訪問HAproxy( 如果不能訪問看下防火牆是否關閉)
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-vQkMySbO-1592451926059)(assets/1572509988524.png)]
關閉集羣中的某一個節點:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8CdmcfvR-1592451926061)(assets/1572510114961.png)]
再次查看HAProxy的管理控制檯:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-eeiJKoM6-1592451926062)(assets/1572510210880.png)]
-
編寫程序測試負載均衡
- 生產者一端申明交換機和隊列,並完成綁定
- application.yml配置文件(重要)
- 在生產者一端編寫測試方法發送消息
- 測試結果(連續發送了2條消息一條被server1接收一條被server2接收,)
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ulnDBrxD-1592451926063)(assets/09.png)]
4. KeepAlived 搭建高可用的HAProxy集羣
-
KeepAlived 簡介
Keepalived,它是一個高性能的服務器高可用或熱備解決方案,Keepalived主要來防止服務器單點故障的發生問題,可以通過其與Nginx、Haproxy等反向代理的負載均衡服務器配合實現web服務端的高可用。Keepalived以VRRP協議爲實現基礎,用VRRP協議來實現高可用性(HA).VRRP(Virtual Router Redundancy Protocol)協議是用於實現路由器冗餘的協議,VRRP協議將兩臺或多臺路由器設備虛擬成一個設備,對外提供虛擬路由器IP(一個或多個)。
-
KeepAlived 的安裝
- KeepAlived 的配置
- 編寫執行腳本(一定要賦權否則不能執行)
- 執行腳本賦權
- 啓動keepalived
-
高可用測試
-
查看keepalived啓動狀態
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GuPUvEMs-1592451926065)(assets/12.png)]
-
停止server4節點的keepalived查看我們的虛擬ip是否漂移到了server5
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-HIzx3AWZ-1592451926066)(assets/13.png)]
-
編寫程序連接虛擬ip查看我們的mq是否成功
- 修改application.yml文件
-
2. 啓動測試
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-firu3oY2-1592451926069)(assets/14.png)]
四、RabbitMQ應用
1. 消息堆積
當消息生產的速度長時間,遠遠大於消費的速度時。就會造成消息堆積。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-w160K4ZG-1592451967287)(assets/消息堆積02.png)]
- 消息堆積的影響
- 可能導致新消息無法進入隊列
- 可能導致舊消息無法丟失
- 消息等待消費的時間過長,超出了業務容忍範圍。
- 產生堆積的情況
- 生產者突然大量發佈消息
- 消費者消費失敗
- 消費者出現性能瓶頸。
- 消費者掛掉
- 解決辦法
- 排查消費者的消費性能瓶頸
- 增加消費者的多線程處理
- 部署增加多個消費者
- 場景介紹
在用戶登錄成功之後,會向rabbitmq發送一個登錄成功的消息。這個消息可以被多類業務訂閱。
登錄成功,記錄登錄日誌;登錄成功,根據規則送積分。其中登錄送積分可以模擬成較爲耗時的處理
場景重現:讓消息產生堆積
-
生產者大量發送消息:使用Jmeter開啓多線程,循環發送消息大量進入隊列。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-pBJyJ8Z9-1592451967289)(assets/消息堆積03.png)]
模擬堆積10萬條數據
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-NxLyNmGF-1592451967290)(assets/消息堆積04.png)]
-
消費者消費失敗:隨機拋出異常,模擬消費者消費失敗,沒有ack(手動ack的時候)。
-
設置消費者的性能瓶頸:在消費方法中設置休眠時間,模擬性能瓶頸
-
關閉消費者:停掉消費者,模擬消費者掛掉
-
消費者端示例核心代碼:
public class LoginIntegralComsumer implements MessageListener {
public void onMessage(Message message) {
String jsonString = null;
try {
jsonString = new String(message.getBody(),"UTF8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
if(new Random().nextInt(5)==2){
//模擬發生異常
throw new RuntimeException("模擬處理異常");
}
try {
//模擬耗時的處理過程
TimeUnit.MILLISECONDS.sleep(1000);
System.out.println(Thread.currentThread().getName()+"處理消息:"+jsonString);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 如果每1秒鐘處理一條消息
1小時處理 60*60=3600條
處理完10萬條數據 100000/3600=27.7小時
問題解決:消息已經堆積如何解決
消息隊列堆積,想辦法把消息轉移到一個新的隊列,增加服務器慢慢來消費這個消息可以
生產環境的隊列可用狀態
1、解決消費者消費異常問題
2、解決消費者的性能瓶頸:改短休眠時間
5.4小時。
3、增加消費線程,增加多臺服務器部署消費者。快速消費。
增加10個線程
concurrency="10" prefetch="10"
1小時
增加一臺服務器
0.5小時
2. 消息丟失
在實際的生產環境中有可能出現一條消息因爲一些原因丟失,導致消息沒有消費成功,從而造成數據不一致等問題,造成嚴重的影響,比如:在一個商城的下單業務中,需要生成訂單信息和扣減庫存兩個動作,如果使用RabbitMQ來實現該業務,那麼在訂單服務下單成功後需要發送一條消息到庫存服務進行扣減庫存,如果在此過程中,一條消息因爲某些原因丟失,那麼就會出現下單成功但是庫存沒有扣減,從而導致超賣的情況,也就是庫存已經沒有了,但是用戶還能下單,這個問題對於商城系統來說是致命的。
消息丟失的場景主要分爲:消息在生產者丟失,消息在RabbitMQ丟失,消息在消費者丟失。
2.1. 消息在生產者丟失
場景介紹
消息生產者發送消息成功,但是MQ沒有收到該消息,消息在從生產者傳輸到MQ的過程中丟失,一般是由於網絡不穩定的原因。
解決方案
採用RabbitMQ 發送方消息確認機制,當消息成功被MQ接收到時,會給生產者發送一個確認消息,表示接收成功。RabbitMQ 發送方消息確認模式有以下三種:普通確認模式,批量確認模式,異步監聽確認模式。spring整合RabbitMQ後只使用了異步監聽確認模式。
*說明*
異步監聽模式,可以實現邊發送消息邊進行確認,不影響主線程任務執行。
*步驟*
-
生產者發送3000條消息
-
在發送消息前開啓開啓發送方確認模式
- 在發送消息前添加異步確認監聽器
2.2. 消息在RabbitMQ丟失
場景介紹
消息成功發送到MQ,消息還沒被消費卻在MQ中丟失,比如MQ服務器宕機或者重啓會出現這種情況
解決方案
持久化交換機,隊列,消息,確保MQ服務器重啓時依然能從磁盤恢復對應的交換機,隊列和消息。
spring整合後默認開啓了交換機,隊列,消息的持久化,所以不修改任何設置就可以保證消息不在RabbitMQ丟失。但是爲了以防萬一,還是可以申明下。
2.3. 消息在消費者丟失
場景介紹
消息費者消費消息時,如果設置爲自動回覆MQ,消息者端收到消息後會自動回覆MQ服務器,MQ則會刪除該條消息,如果消息已經在MQ被刪除但是消費者的業務處理出現異常或者消費者服務宕機,那麼就會導致該消息沒有處理成功從而導致該條消息丟失。
解決方案
設置爲手動回覆MQ服務器,當消費者出現異常或者服務宕機時,MQ服務器不會刪除該消息,而是會把消息重發給綁定該隊列的消費者,如果該隊列只綁定了一個消費者,那麼該消息會一直保存在MQ服務器,直到消息者能正常消費爲止。本解決方案以一個隊列綁定多個消費者爲例來說明,一般在生產環境上也會讓一個隊列綁定多個消費者也就是工作隊列模式來減輕壓力,提高消息處理效率
MQ重發消息場景:
1.消費者未響應ACK,主動關閉頻道或者連接
2.消費者未響應ACK,消費者服務掛掉
3. 有序消費消息
3.1. 場景介紹
場景1
當RabbitMQ採用work Queue模式,此時只會有一個Queue但是會有多個Consumer,同時多個Consumer直接是競爭關係,此時就會出現MQ消息亂序的問題。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-j7FlaQsD-1592451967293)(assets/MQ亂序1.png)]
場景2
當RabbitMQ採用簡單隊列模式的時候,如果消費者採用多線程的方式來加速消息的處理,此時也會出現消息亂序的問題。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-WkhpmR4q-1592451967295)(assets/MQ亂序2.png)]
3.2. 場景1解決
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hpmneC9M-1592451967295)(assets/場景1解決方案.png)]
3.3. 場景2解決
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Y28fRVLu-1592451967296)(assets/場景2解決方案.png)]
4. 重複消費
4.1. 場景介紹
爲了防止消息在消費者端丟失,會採用手動回覆MQ的方式來解決,同時也引出了一個問題,消費者處理消息成功,手動回覆MQ時由於網絡不穩定,連接斷開,導致MQ沒有收到消費者回復的消息,那麼該條消息還會保存在MQ的消息隊列,由於MQ的消息重發機制,會重新把該條消息發給和該隊列綁定的消息者處理,這樣就會導致消息重複消費。而有些操作是不允許重複消費的,比如下單,減庫存,扣款等操作。
MQ重發消息場景:
1.消費者未響應ACK,主動關閉頻道或者連接
2.消費者未響應ACK,消費者服務掛掉
4.2. 解決方案
如果消費消息的業務是冪等性操作(同一個操作執行多次,結果不變)就算重複消費也沒問題,可以不做處理,如果不支持冪等性操作,如:下單,減庫存,扣款等,那麼可以在消費者端每次消費成功後將該條消息id保存到數據庫,每次消費前查詢該消息id,如果該條消息id已經存在那麼表示已經消費過就不再消費否則就消費。本方案採用redis存儲消息id,因爲redis是單線程的,並且性能也非常好,提供了很多原子性的命令,本方案使用setnx命令存儲消息id。
setnx(key,value):如果key不存在則插入成功且返回1,如果key存在,則不進行任何操作,返回0