RabbitMQ如何保證訊息的可達性
一、RabbitMQ簡介
AMQP,即Advanced Message Queuing Protocol,一個提供統一訊息服務的應用層標準高階訊息佇列協議,是應用層協議的一個開放標準,為面向訊息的中介軟體設計。
RabbitMQ,又稱為高效能分散式訊息佇列,它實現了AMQP標準協議。
分散式訊息佇列有很多應用場景,比如非同步處理、應用解耦、流量削峰等。
1、非同步處理
使用者註冊後需要傳送簡訊和郵件,傳統做法是先將使用者資訊寫入資料庫,然後傳送簡訊、傳送郵件,都完成後返回。
如果用到訊息佇列,可以先將使用者資訊寫入資料庫,然後將註冊資訊寫入訊息佇列,傳送簡訊、傳送郵件或者還有其他的業務邏輯都訂閱此訊息,完成傳送。
2、應用解耦
還是上面的例子,如果在一個大型分散式網站中,使用者系統、簡訊系統、郵件系統可能都是獨立的系統服務。
這時候,在使用者註冊成功後,你可以通過RPC遠端呼叫不同的服務介面,但更好的做法還是通過訊息佇列,訂閱自己感興趣的資料,日後就算增加或者刪減功能,主業務都不用變動。
3、流量削峰
一般在秒殺或者團購活動中,流量激增,應用面臨壓力過大。可以在應用前端加入訊息佇列,通過設定佇列最大長度來限制活動人數。這時候,後端伺服器就可以遊刃有餘的處理資料了。
二、訊息通訊
在AMQP協議中,有幾個基本概念,我們必須先搞明白。
1、Virtual host
虛擬主機,每一個虛擬主機中包含所有的AMQP基本元件,使用者、隊裡、交換器等都是在虛擬主機裡面建立。典型的用法是,如果公司的多個產品只想用一個伺服器,就可以把他們劃分到不同的虛擬主機中,裡面的任何資訊都是獨立存在,互不干擾。
2、Connection
連線,應用程式和伺服器之間的TCP連線。
3、Channel
通道,當你的應用程式和伺服器連線之後,就會建立TCP連線。一旦開啟了TCP連線,就可以建立一個Channel通道,所以說Channel通道是一個TCP連線的內部邏輯單元。
這是因為,建立和銷燬TCP連線是比較昂貴的開銷,每一次訪問都建立新的TCP連線的話,不僅是巨大浪費,而且還容易造成系統效能瓶頸。
4、Queue
佇列,所有的訊息最終都會被送到這裡,等待著被感興趣的人取走。
5、Exchange
交換器,訊息到達服務的第一站就是交換器,然後根據分發規則,匹配路由鍵,將訊息放到對應佇列中。值得注意的是,交換器的型別不止一種。
Direct
直連交換器,只有在訊息中的路由鍵和繫結關係中的鍵一致時,交換器才把訊息發到相應佇列Fanout
廣播交換器,只要訊息被髮送到廣播交換器,它會將訊息發到所有的佇列Topic
主題交換器,根據路由鍵,通配規則(*和#),將訊息發到相應佇列
6、Binding
繫結,交換器和佇列之間的繫結關係,繫結中就包含路由鍵,繫結資訊被儲存到交換器的查詢表中,交換器根據它分發訊息。
瞭解到這些元件相關概念後,我們總結一下來看看,一條訊息在RabbitMQ中是如何流轉的。
三、持久化和傳送方確認
1、持久化
事實上,上圖所示只是一個最基本的訊息流轉過程,交換器和佇列這些元件還有一個比較重要的屬性:持久化。
預設情況下,重啟RabbitMQ伺服器之後,我們建立的交換器和佇列都會消失不見,當然了,如果裡面還有未來得及消費的資料,也將難於倖免。
持久化交換器和佇列,為的是在AMQP伺服器重啟之後,重新建立它們並繫結關係,在RabbitMQ中,設定durable屬性為true即可。
不過,除了這些還不夠。雖然保證了交換器和佇列是安全的,但那些還未來得及消費的資料就變得岌岌可危。所以,我們還要設定訊息的投遞模式為持久的。
這樣,如果RabbitMQ伺服器重啟的話,我們的策略和相關資料才會確保無憂。所以,我們說能從AMQP伺服器崩潰中恢復的訊息,稱之為持久化訊息。那麼,它必須保證以下三點:
- 設定投遞模式為持久的
- 交換器為持久的
- 佇列為持久的
2、傳送方確認
到目前為止,我們已經保證了訊息的安全性。但是,還有另外一個問題。由於釋出操作是不返回任何資訊給生產者的,我們怎麼知道伺服器是否正確接收訊息並持久化到硬碟上了呢?
為此,我們可以將通道設定為事務模式。事務是AMQP標準中的一部分,但RabbitMQ有更好的做法,那就是傳送方確認模式,publisher confirm。如果設定了confirm模式,釋出的訊息會被分配一個唯一的ID號,等訊息被投遞給匹配的佇列後,通道會傳送一個傳送方確認模式給生產者(包含訊息的唯一ID)。
四、與Spring整合例項
廢話了這麼多,只是為了下面的程式碼部分做下鋪墊。畢竟,瞭解到上面內容之後,程式碼其實已經快要躍然紙上了。
1、配置檔案
配置檔案中我們首先要宣告RabbitMQ伺服器的資訊,IP地址、埠號、使用者名稱密碼等,但尤為重要的是,設定釋出確認模式。
<bean id="rabbitConnectionFactory"
class="org.springframework.amqp.rabbit.connection.CachingConnectionFactory">
<constructor-arg value="127.0.0.1"/>
<property name="username" value="shiqizhen"/>
<property name="password" value="shiqizhen"/>
<property name="port" value="5672"></property>
<property name="virtualHost" value="shiqizhen"></property>
<property name="publisherConfirms" value="true"></property>
<property name="publisherReturns" value="true"></property>
</bean>
接著,還要宣告交換器和佇列,記得它們是持久化的哦,durable為true。
<rabbit:admin connection-factory="rabbitConnectionFactory"/>
//佇列的名字、持久化、不要自動刪除、不是獨享佇列
<rabbit:queue name="userInfoQueue" durable="true" auto-delete="false" exclusive="false"/>
//交換器,型別為direct。並繫結交換器和佇列的關係,路由鍵為10086
<rabbit:direct-exchange name="user-exchange" durable="true" auto-delete="false">
<rabbit:bindings>
<rabbit:binding queue="userInfoQueue" key="10086"/>
</rabbit:bindings>
</rabbit:direct-exchange>
最後,配置消費者和訊息模板
//配置消費者 ref為bean的引用 queues指明瞭消費者與佇列的關係
//重要的是acknowledge 確認模式為手動確認
<rabbit:listener-container connection-factory="rabbitConnectionFactory" acknowledge="manual">
<rabbit:listener ref="consumerListener" queues="userInfoQueue" method="onMessage" />
</rabbit:listener-container>
//配置Spring RabbitMQ訊息模板
<bean id="rabbitTemplate" class="org.springframework.amqp.rabbit.core.RabbitTemplate">
<constructor-arg ref="rabbitConnectionFactory"></constructor-arg>
<property name="confirmCallback" ref="publisherConfirm"></property>
<property name="returnCallback" ref="returnMsgCallBack"></property>
<property name="mandatory" value="true"></property>
</bean>
2、生產者
上面我們宣告瞭rabbitTemplate,直接用它的send方法傳送訊息即可。不過它有幾個引數必須先要了解下。
- exchange
交換器名稱,訊息發到哪個交換器上 - routingKey
路由鍵,交換器怎樣分發訊息到對應佇列 - Message
訊息體物件,它包含訊息的主體和訊息屬性。訊息屬性包含很多附屬資訊,比如訊息內容型別、訊息ID、使用者ID等。 - CorrelationData
訊息相關資料,實際它只有一個ID的屬性。不過很重要,在釋出方確認的回撥方法裡,會帶有這個引數。我們可以根據它很直觀的看到哪條訊息傳送成功或失敗。
@Controller
public class IndexController {
@Autowired
RabbitTemplate rabbitTemplate;
@RequestMapping("/send_msg")
@ResponseBody
public User send_msg() {
String exchange = "user-exchange";
String routingKey = "10086";
User user = new User();
String id = IdUtil.getId();
user.setUid(id);
user.setUsername("小小沙彌");
user.setPassword("1234");
user.setCreatetime(DateUtil.getDateTime(new Date()));
CorrelationData correlation = new CorrelationData(id);
Message message = new Message(JSONObject.toJSONBytes(user, SerializerFeature.WriteNullStringAsEmpty), new MessageProperties());
logger.info("已傳送訊息到RabbitMQ伺服器:{}",JSONObject.toJSONString(user));
rabbitTemplate.send(exchange, routingKey,message,correlation);
return user;
}
}
3、消費者
消費者就是上面我們配置的listener ref引用的Bean。還記得我們把確認模式設定了手動確認,所以在消費者端有個很重要的動作,就是確認訊息。
- channel.basicAck(deliveryTag, false)
第一個引數是RabbitMQ內部產生的訊息ID,第二個引數代表是否批量確認訊息。通過這個指令我們告訴生產者端,訊息已經被正確消費了,RabbitMQ就會將此訊息在磁碟上刪除。 - channel.basicReject(deliveryTag, false)
拒絕訊息。如果消費到的訊息不是我們想要的,或者處理的時候報錯,我們可以將訊息拒絕。但值得注意的是第二個引數。如果設定為false,說明拒絕訊息並將訊息從伺服器上刪除;如果設定為true,說明拒絕訊息並將訊息重新放回佇列。如果你的消費者只有一個,最好不要把它設定為true,否則訊息會一直重試,直到把消費者端伺服器搞死。如果因為處理失敗而拒絕的話,最好將訊息刪除,同時將訊息記錄到日誌檔案或者資料庫中。
@Service
public class ConsumerListener implements ChannelAwareMessageListener{
Logger logger = LoggerFactory.getLogger(this.getClass());
public void onMessage(Message message, Channel channel) throws Exception {
logger.info("消費者監聽到RabbitMQ訊息...");
MessageProperties properties = message.getMessageProperties();
String msg = new String(message.getBody(),"utf-8");
logger.info("交換器:{},路由鍵:{}",properties.getReceivedExchange(),properties.getReceivedRoutingKey());
logger.info("訊息內容:{}",msg);
long deliveryTag = properties.getDeliveryTag();
channel.basicAck(deliveryTag, false);//確認資訊,false為不批量確認
//channel.basicReject(deliveryTag, true);//true為重入佇列 false為刪除訊息
}
}
4、傳送方確認
我們傳送訊息給RabbitMQ,第一站就是交換器。RabbitMQ是否能正確接收訊息,我們就靠它來反饋。這裡的CorrelationData就是在生產者端設定的,我們可以將它當成訊息ID,也可以直接把訊息寫入這裡。
@Component
public class PublisherConfirm implements ConfirmCallback{
Logger logger = LoggerFactory.getLogger(this.getClass());
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
logger.info("訊息投遞成功!");
}else {
logger.warn("訊息投遞失敗,原因:{},訊息ID:{}",cause,correlationData.getId());
}
}
}
如果我們把交換器的名字寫錯,那麼在這裡,你就會得到以下資訊:
22:57:51,635 WARN PublisherConfirm:19 - 訊息投遞失敗,原因:
channel error; protocol method: #method<channel.close>
(reply-code=404, reply-text=NOT_FOUND - no exchange 'user-exchange_xxx' in vhost 'shiqizhen', class-id=60, method-id=40),
訊息ID:516387069669408768
22:57:51,638 ERROR CachingConnectionFactory:1278 - Channel shutdown:
channel error; protocol method:
#method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'user-exchange_xxx' in vhost 'shiqizhen', class-id=60, method-id=40)
5、返回回撥
除了設定RabbitMQ的傳送方確認,在Spring中還有一個publisherReturns值的我們注意。雖然我們將訊息傳送到了交換器,但交換器是否能正確將訊息分發到對應佇列,還要打個問號。如果訊息無法傳送到指定的佇列,那麼publisherReturns就會發揮作用。記住,如果想應用這個特性,需要將mandatory設定為true。
@Component
public class ReturnMsgCallBack implements ReturnCallback{
Logger logger = LoggerFactory.getLogger(this.getClass());
public void returnedMessage(Message message, int replyCode,
String replyText, String exchange, String routingKey) {
logger.info("訊息內容:{}",new String(message.getBody()));
logger.info("回覆文字:{},回覆程式碼:{}",replyText,replyCode);
logger.info("交換器名稱:{},路由鍵:{}",exchange,routingKey);
}
}
如果我們不小心寫錯了路由鍵的名字,那就會呼叫到這裡。
23:24:27,813 INFO ReturnMsgCallBack:16 - 訊息內容:{"createtime":"2018-11-25 23:24:24","password":"1234","role":null,"uid":"516393749815754752","username":"小小沙彌"}
23:24:27,814 INFO ReturnMsgCallBack:17 - 回覆文字:NO_ROUTE,回覆程式碼:312
23:24:27,814 INFO ReturnMsgCallBack:18 - 交換器名稱:user-exchange,路由鍵:10086_xxx
//這裡是傳送方確認列印的資訊 說投遞到交換器成功
23:24:27,814 INFO PublisherConfirm:17 - 訊息投遞成功!
有個問題,如同第一個例子,如果寫錯了路由鍵的名稱,傳送方確認會列印ack為false的異常資訊,但為什麼不會呼叫到publisherReturns呢?
如果路由鍵錯誤,說明訊息壓根就沒有被接收到。這肯定是一個嚴重錯誤,所以RabbitMQ直接把當前通道關閉了。
Channel shutdown:
channel error; protocol method:
reply-code=404, reply-text=NOT_FOUND - no exchange 'user-exchange_xxx' in vhost ...
五、監聽RabbitMQ伺服器狀態
如果你的RabbitMQ服務不是一個叢集,那麼當網路故障或其他原因導致RabbitMQ服務停掉的時候,我們怎麼做呢?當然,你可以在Send方法中加入try/catch,根據catch資訊返回你的狀態。但有個更好的思路,可以結合使用。
在建立RabbitMQ服務連線的時候,我們要配置一個Bean,CachingConnectionFactory
它有個方法addConnectionListener
,我們可以利用它來監聽伺服器的連線狀態。
public class RabbitMQConnectionListener implements ConnectionListener{
public void onCreate(Connection connection) {
System.out.println("伺服器已啟動...");
}
public void onClose(Connection connection) {
System.out.println("伺服器已關閉...");
}
}
並在合適的位置,比如Spring容器初始化方法裡,加入這麼一句rabbitConnectionFactory.addConnectionListener(new RabbitMQConnectionListener());
這樣,我們就可以掌握RabbitMQ伺服器的連線狀態了,那麼我們就可以根據此狀態,在生產者方呼叫send方法的時候,判斷此狀態。如果未連線,可以先將訊息儲存到資料庫或者快取中。當連線到RabbitMQ,我們先把快取的訊息拿出來傳送,再將此狀態重置為已連線。
六、總結
本文簡單介紹了AMQP協議標準中的相關概念,以及RabbitMQ在Spring中如何正確配置使用持久化訊息、傳送方模式和返回回撥等機制。並在最後,介紹了在Spring中如何監聽RabbitMQ的伺服器連線狀態。總而言之一句話,我們將要怎樣使用RabbitMQ,才能保證訊息不會丟失。希望本文對你使用RabbitMQ有所幫助!
相關文章
- 《RabbitMQ》如何保證訊息的可靠性MQ
- RabbitMQ高階之如何保證訊息可靠性?MQ
- RabbitMQ-如何保證訊息不丟失MQ
- 《RabbitMQ》如何保證訊息不被重複消費MQ
- 訊息佇列-如何保證訊息的不被重複消費(如何保證訊息消費的冪等性)佇列
- 分散式訊息佇列:如何保證訊息的順序性分散式佇列
- 如何保證訊息佇列的順序性?佇列
- Kafka 如何保證訊息消費的全域性順序性Kafka
- RabbitMq如何確保訊息不丟失MQ
- 如何保證訊息佇列的可靠性傳輸?佇列
- RabbitMQ-如何保證訊息在99.99%的情況下不丟失MQ
- RabbitMQ使用教程(三)如何保證訊息99.99%被髮送成功?MQ
- MQ系列10:如何保證訊息冪等性消費MQ
- 訊息佇列之如何保證訊息的可靠傳輸佇列
- kafka-如何保證訊息的可靠性與一致性Kafka
- RabbitMQ使用教程(五)如何保證佇列裡的訊息99.99%被消費?MQ佇列
- RabbitMQ的訊息可靠性(五)MQ
- 分散式訊息佇列:如何保證訊息不被重複消費?(訊息佇列消費的冪等性)分散式佇列
- 二、如何保證訊息佇列的高可用?佇列
- MQ系列11:如何保證訊息可靠性傳輸(除夕奉上)MQ
- Kafka如何保證訊息不丟之無訊息丟失配置Kafka
- RabbitMQ使用教程(四)如何通過持久化保證訊息99.99%不丟失?MQ持久化
- Storm保證訊息處理ORM
- 面試題剖析,如何保證訊息佇列的高可用?面試題佇列
- RabbitMQ訊息模式MQ模式
- 如何選擇RabbitMQ的訊息儲存方式?MQ
- 消費端如何保證訊息佇列MQ的有序消費佇列MQ
- 阿里二面:Kafka中如何保證訊息的順序性?這周被問到兩次了阿里Kafka
- 使用訊息中介軟體時,如何保證訊息僅僅被消費一次?
- 面試官:volatile如何保證可見性的,具體如何實現?面試
- 如何透過 SpringBoot+RabbitMQ 保證訊息100%投遞成功並被消費?(附原始碼)Spring BootMQ原始碼
- 關於MQ的幾件小事(四)如何保證訊息不丟失MQ
- 如何保證MongoDB的安全性?MongoDB
- Storm基礎(四)保證訊息處理ORM
- 如何處理RabbitMQ 訊息堆積和訊息丟失問題MQ
- RabbitMQ訊息佇列MQ佇列
- [訊息佇列]RabbitMQ佇列MQ
- java從SQS訂閱訊息 的demo, 要求保證訊息可靠投遞的例子Java