消息中間件的優勢
UNIX的進程間通信就開始運用消息隊列技術,一個進程將數據寫入某個特定的隊列中,其它進程可以讀取隊列中的數據,從而實現異步通信。對於如今的分布式系統,消息隊列已經演變為獨立的消息中間件產品,相比於RPC同步通信的方式來說有幾個明顯的優勢:
- 低耦合,不管是程序還是模塊之間,使用消息中間件進行間接通信。
- 消息的順序性,消息隊列可以保證消息的先進先出。
- 消息可靠傳輸,持久化的存儲使得消息只有在被消費之后才會刪除。
- 異步通信能力,相對於RPC來說,異步通信使得生產者和消費者得以充分執行自己的邏輯而無需等待。
- 緩沖能力,消息中間件像是一個巨大的蓄水池,將高峰期大量的請求存儲下來慢慢交給后台進行處理,對於秒殺業務來說尤為重要。
但是異步通信也存在程序設計和編程方面的復雜,同時對於實時性要求較高的業務也不能采用異步通信,所以要根據業務具體分析。
J2EE和JEE是什么?
J2EE全稱是Java to Enterprise Edition,是一套企業級技術規范,包含:JMS Servlet JSP EJB JPA 等。J2EE發展到1.5版本改名為JEE5,所以JEE是J2EE規范的延續。
消息中間件的發展歷程
- J2EE時代,消息中間件強調企業級特性,比如消息持久化和事務性要求,全部遵循JMS規范。典型的ActiveMQ、HornetQ,后者現在已經發展成ActiveMQ Artemis子項目,這表明在Java/JEE領域ActiveMQ會繼續發揮不可替代的作用。
- 后Java時代,隨着消息中間件協議AMQP(Advanced Message Queuing Protocol)的誕生,最知名的AMQP消息中間件產品RabbitMQ也隨之誕生,RabbitMQ基於Erlang開發,2007年誕生至今已經稱為最流行的開源消息中間件產品,ActiveMQ於2013年也開始支持AMQP協議,后來Apache還推出了ActiveMQ Apollo的新項目,也實現了包括AMQP在內的多種協議。
- 互聯網時代,設計思路上采用分布式系統設計理念,以LinkedIn開源的Kafka為代表,Kafka使用Scala編寫,由於良好的水平擴展能力和高性能被廣泛采用,同時誕生了一些高仿的類似產品如搜狐的Jafka,阿里的RocketMQ等。
JMS是什么?
JMS Java Message Service Java消息服務是J2EE架構中針對消息中間件的一組規范。JMS規范定義了Java中訪問消息中間件的接口,但沒有給予實現,具體實現交給消息中間件,稱為JMS Provider,如ActiveMQ就是一個JMS Provider。簡單說,沒有實現這些接口的消息中間件就不能納入J2EE(JEE)架構,Java程序也無法與其進行交互。
JMS的兩種消息傳送模型
JMS支持兩種消息傳送模型:點對點消息通信模型和發布訂閱模型。
- 點對點(PTP)消息通信模型也可稱之為隊列模式,特定的一條消息只能被一個消費者消費。生產者將消息發送到指定的Queue當中,Broker(中間件)針對消息是否需要持久化進行持久化存儲后通知消費者進行處理,消費者處理完畢后發送一個回執(Acknowledge)給Broker,Broker認為該消息已被正常消費,於是從持久化存儲中刪除該條消息。回執的發送邏輯內嵌在MQ的API中,無需主動調用。消費者通常可以通過兩種方式獲取新消息:PUSH和PULL。PUSH方式,由MQ收到消息后主動調用消費者的新消息通知接口,需要消耗MQ寶貴的線程資源,同時消費者只能被動等待消息通知;PULL方式,由消費者輪詢調用MQ API去獲取新消息,對應於ActiveMQ中的方法為consumer.receive(),不消耗MQ線程,消費者更加主動,雖然消費者的處理邏輯變得稍稍復雜。兩種方式的根本區別在於線程消耗問題,由於MQ的線程資源相對客戶端更加寶貴,PUSH方式會占用MQ過多的線程從而難以適應高並發的消息場景。同時當某一消費者離線一段時間再次上線后,大量積壓消息處理會消耗大量MQ線程從而拖累其它消費者的消息處理,所以PULL方式相對來說更好。Kafka已經拋棄了PUSH模式,全面擁抱PULL模式。
- 發布/訂閱模式(Pub/Sub)也可稱之為主題模式,特定的一條消息可以被多個消費者所接收,只要消費者訂閱了某個主題。消息生產者(發布者)將消息發送到某個稱為主題(Topic)的虛擬通道中,Topic可以被多個消費者訂閱,因此該模式類似於廣播的方式。發布/訂閱模式采用PUSH的方式傳送消息,Subscriber只需保持在線即可。Subscriber分為臨時性的和持久性的,當Sub離線時,MQ會為持久性的Sub持久化消息,當Sub恢復時會重新收到消息。但是既然采用Pub/Sub模式就表明允許部分消費者接收不到消息,所以通常會采用臨時性的Subscriber而不是持久性的。