分佈式專題-分佈式消息通信之ActiveMQ01-初識ActiveMQ

前言

關於分佈式消息通信,我們主要講三個中間件:

  • ActiveMQ
  • RabbitMQ
  • Kafka

從這節開始,我們先講ActiveMQ,關於ActiveMQ,我們主要講兩點

  1. 初識ActiveMQ
  2. ActiveMQ的原理分析(上)
  3. ActiveMQ的原理分析(下)

這一節,我們就先來認識一下ActiveMQ

Tips:本節代碼演示的下載地址請見本文末~

消息中間件的初步認識

什麼是消息中間件

消息中間件是值利用高效可靠的消息傳遞機制進行平臺無關的數據交流,並基於數據通信來進行分佈式系統的集成。通過提供消息傳遞和消息排隊模型,可以在分佈式架構下擴展進程之間的通信。

消息中間件能做什麼

消息中間件主要解決的就是分佈式系統之間消息傳遞的問題,它能夠屏蔽各種平臺以及協議之間的特性,實現應用程序之間的協同。舉個非常簡單的例子,就拿一個電商平臺的註冊功能來簡單分析下,用戶註冊這一個服務,不單單只是 insert 一條數據到數據庫裏面就完事了,還需要發送激活郵件、發送新人紅包或者積分、發送營銷短信等一系列操作。假如說這裏面的每一個操作,都需要消耗 1s,那麼整個註冊過程就需要耗時 4s 才能響應給用戶。
在這裏插入圖片描述
但是我們從註冊這個服務可以看到,每一個子操作都是相對獨立的,同時,基於領域劃分以後,發送激活郵件、發送營銷短信、贈送積分及紅包都屬於不同的子域。所以我們可以對這些子操作進行來實現異步化執行,類似於多線程並行處理的概念。

如何實現異步化呢?用多線程能實現嗎?
多線程當然可以實現,只是,消息的持久化、消息的重發這些條件,多線程並不能滿足。所以需要藉助一些開源中間件來解決。而分佈式消息隊列就是一個非常好的解決辦法,引入分佈式消息隊列以後,架構圖就變成這樣了(下圖是異步消息隊列的場景)。通過引入分佈式隊列,就能夠大大提升程序的處理效率,並且還解決了各個模塊之間的耦合問題

這個是分佈式消息隊列的第一個解決場景【異步處理】

在這裏插入圖片描述
我們再來展開一種場景,通過分佈式消息隊列來實現流量整形,比如在電商平臺的秒殺場景下,流量會非常大。通過消息隊列的方式可以很好的緩解高流量的問題

在這裏插入圖片描述

用戶提交過來的請求,先寫入到消息隊列。消息隊列是有長度的,如果消息隊列長度超過指定長度,直接拋棄

秒殺的具體核心處理業務,接收消息隊列中消息進行處理,這裏的消息處理能力取決於消費端本身的吞吐量當然,消息中間件還有更多應用場景,比如在弱一致性事務模型中,可以採用分佈式消息隊列的實現最大能力通知方式來實現數據的最終一致性等等

ActiveMQ 簡介

ActiveMQ 是完全基於JMS規範實現的一個消息中間件產品。是 Apache 開源基金會研發的消息中間件。ActiveMQ主要應用在分佈式系統架構中,幫助構建高可用、高性能、可伸縮的企業級面向消息服務的系統

ActiveMQ 特性

  1. 多語言和協議編寫客戶端

    語言:java/C/C++/C#/Ruby/Perl/Python/PHP
    應用協議 : openwire/stomp/REST/ws/notification/XMPP/AMQP

  2. 完全支持 jms1.1 和 J2ee1.4 規範

  3. 對Spring的支持,ActiveMQ可以很容易內嵌到Spring模塊中

ActiveMQ 安裝

  1. 登錄到http://activemq.apache.org,找到 ActiveMQ 的下載地址

  2. 直接copy到服務器上通過tar -zxvf apache-activeMQ.tar.gz
    在這裏插入圖片描述

  3. 啓動運行

    a) 普通啓動:到 bin 目錄下, sh activemq start
    b) 啓 動 並 指 定 日 志 文 件 sh activemq start > /tmp/activemqlog

在這裏插入圖片描述

  1. 檢查是否已啓動

ActiveMQ 默認採用 61616 端口提供 JMS 服務,使用 8161端口提供管理控制檯服務,執行以下命令可以檢查是否成功啓動 ActiveMQ 服務

netstat -an|grep 61616

在這裏插入圖片描述
5. 通過 http://192.168.200.111:8161 (這是我的activeMQ所在服務器地址)訪問 activeMQ 管理頁面 ,默認帳號密碼 admin/admin
在這裏插入圖片描述
登陸後
在這裏插入圖片描述
6. 關閉 ActiveMQ; sh activemq stop
在這裏插入圖片描述

從JMS規範來了解ActiveMQ

JMS定義

Java 消息服務(Java Message Service)是 java 平臺中關於面向消息中間件的 API,用於在兩個應用程序之間,或者分佈式系統中發送消息,進行異步通信。
JMS 是一個與具體平臺無關的 API ,絕大多數 MOM (Message Oriented Middleware)(面向消息中間件)提供商都對 JMS 提供了支持。今天給大家講的 ActiveMQ 就是其中一個實現

什麼是MOM

MOM 是面向消息的中間件,使用消息傳送提供者來協調消息傳送操作。MOM 需要提供 API 和管理工具。客戶端使用 api 調用,把消息發送到由提供者管理的目的地。在發送消息之後,客戶端會繼續執行其他工作,並且在接收方收到這個消息確認之前,提供者一直保留該消息。
在這裏插入圖片描述

MOM的特點

  1. 消息異步接收,發送者不需要等待消息接受者響應

  2. 消息可靠接收,確保消息在中間件可靠保存。只有接收方收到後才刪除消息

Java 消息傳送服務規範最初的開發目的是爲了使 Java 應用程序能夠訪問現有 MOM 系統。引入該規範之後,它已被許多現有的 MOM 供應商採用並且已經憑藉自身的功能實現爲異步消息傳送系統。

其他開源的 JMS 提供商

JbossMQ(jboss4) 、 jboss messaging(jboss5) 、 joram 、ubermq、mantamq、openjms…

大部分基於的 JMS provider 開源的消息中間件都已經停止維護了,剩下的幾個都抱到了大腿,比如 Jboss mq 和 jboss、 joram 與 jonas(objectweb 組 織 ) 、 ActiveMQ 與 Geronimo(apache 基金組織)。

JMS 規範

我們已經知道了 JMS 規範的目的是爲了使得 Java 應用程序能夠訪問現有 MOM (消息中間件)系統,形成一套統一的標準規範,解決不同消息中間件之間的協作問題。在創建 JMS 規範時,設計者希望能夠結合現有的消息傳送的精髓,比如說

  1. 不同的消息傳送模式或域,例如點對點消息傳送和發佈/訂閱消息傳送

  2. 提供於接收同步和異步消息的工具

  3. 對可靠消息傳送的支持

  4. 常見消息格式,例如流、文本和字節

JMS的體系結構

在這裏插入圖片描述

通過JMS規範結合ActiveMQ實現消息發送案例

案例演示

新建兩個項目,一個發送消息,一個接收消息在這裏插入圖片描述
首先導入ActiveMQ的依賴

 <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.activemq</groupId>
      <artifactId>activemq-all</artifactId>
      <version>5.15.0</version>
    </dependency>

案例一
在producter裏創建JMSQueueProducer
JMSQueueProducer

    public static void main(String[] args) {
        ConnectionFactory connectionFactory=
                new ActiveMQConnectionFactory
                        ("tcp://192.168.200.111:61616");
        Connection connection=null;
        try {

            connection=connectionFactory.createConnection();
            connection.start();

            Session session=connection.createSession
                    (Boolean.FALSE,Session.AUTO_ACKNOWLEDGE);
            //創建目的地
            Destination destination=session.createQueue("myQueue");
            //創建發送者
            MessageProducer producer=session.createProducer(destination);
            producer.setDeliveryMode(DeliveryMode.PERSISTENT);
            producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);

            for(int i=0;i<10;i++) {
                //創建需要發送的消息
                TextMessage message = session.createTextMessage("Hello World:"+i);
                //Text   Map  Bytes  Stream  Object
                producer.send(message);
            }

//            session.commit();
            session.close();
        } catch (JMSException e) {
            e.printStackTrace();
        }finally {
            if(connection!=null){
                try {
                    connection.close();
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        }
    }

效果如是:
在這裏插入圖片描述

Number Of Pending Messages :等待消費的消息 這個是當前未出隊列的數量。
Number Of Consumers :消費者 這個是消費者端的消費者數量
Messages Enqueued :進入隊列的消息 進入隊列的總數量,包括出隊列的。
Messages Dequeued :出了隊列的消息 可以理解爲是消費這消費掉的數量。

相當於創建了一個名稱叫做myQueue的隊列,並在broker中生產了10條數據,沒有被消費,現在我們寫消費端:

 public static void main(String[] args) {
        ConnectionFactory connectionFactory=
                new ActiveMQConnectionFactory
                        ("tcp://192.168.200.111:61616");
        Connection connection=null;
        try {

            connection=connectionFactory.createConnection();
            connection.start();

            Session session=connection.createSession
                    (Boolean.FALSE,Session.DUPS_OK_ACKNOWLEDGE); //延遲確認
            //創建目的地
            Destination destination=session.createQueue("myQueue");
            //創建發送者
            MessageConsumer consumer=session.createConsumer(destination);
            for(int i=0;i<10;i++) {
                TextMessage textMessage = (TextMessage) consumer.receive();
                System.out.println(textMessage.getText());
                if(i==8) {
                    textMessage.acknowledge();
                }
            }
//          session.commit();//表示消息被自動確認
            session.close();
        } catch (JMSException e) {
            e.printStackTrace();
        }finally {
            if(connection!=null){
                try {
                    connection.close();
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        }
    }

走你!
在這裏插入圖片描述
看看web顯示:
在這裏插入圖片描述
說明此時,我們已經將這10條數據消費掉了~

接下來,我們再次調用JMSQueueProducer生產10條數據:
在這裏插入圖片描述
這裏看說明還有10條數據沒有被消費,現在我們運行這樣一段代碼:

 public static void main(String[] args) {
        ConnectionFactory connectionFactory=
                new ActiveMQConnectionFactory
                        ("tcp://192.168.200.111:61616");
        Connection connection=null;
        try {

            connection=connectionFactory.createConnection();
            connection.start();

            Session session=connection.createSession
                    (Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
            //創建目的地
            Destination destination=session.createQueue("myQueue");
            //創建發送者
            MessageConsumer consumer=session.createConsumer(destination);


            MessageListener messageListener=new MessageListener() {
                @Override
                public void onMessage(Message message) {
                    try {
                        System.out.println(((TextMessage)message).getText());
                    } catch (JMSException e) {
                        e.printStackTrace();
                    }
                }
            };

            consumer.setMessageListener(messageListener);
            session.commit();

            System.in.read();


            TextMessage textMessage=(TextMessage) consumer.receive();
            System.out.println(textMessage.getText());

//            session.commit();
//            session.close();
        } catch (JMSException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(connection!=null){
                try {
                    connection.close();
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        }
    }

在這裏插入圖片描述
web顯示效果:
在這裏插入圖片描述
案例二:

  public static void main(String[] args) {
        ConnectionFactory connectionFactory=
                new ActiveMQConnectionFactory
                        ("tcp://192.168.200.111:61616");
        Connection connection=null;
        try {

            connection=connectionFactory.createConnection();
            connection.start();

            Session session=connection.createSession(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
            //創建目的地
            Destination destination=session.createTopic("myTopic");
            //創建發送者
            MessageProducer producer=session.createProducer(destination);
            producer.setDeliveryMode(DeliveryMode.PERSISTENT);

            //創建需要發送的消息
            TextMessage message=session.createTextMessage("send some messages");

            //Text   Map  Bytes  Stream  Object

            producer.send(message);

            session.commit();
            session.rollback();

            session.close();
        } catch (JMSException e) {
            e.printStackTrace();
        }finally {
            if(connection!=null){
                try {
                    connection.close();
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        }
    }

運行後web的效果:
在這裏插入圖片描述
Toptic消費端:

public static void main(String[] args) {
        ConnectionFactory connectionFactory=
                new ActiveMQConnectionFactory
                        ("tcp://192.168.200.111:61616");
        Connection connection=null;
        try {

            connection=connectionFactory.createConnection();
            connection.setClientID("test");

            connection.start();

            Session session=connection.createSession
                    (Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
            //創建目的地
            Topic destination=session.createTopic("myTopic");
            //創建發送者
            MessageConsumer consumer=session.createDurableSubscriber(destination,"test");

            TextMessage textMessage=(TextMessage) consumer.receive();
            System.out.println(textMessage.getText());

//            session.commit(); //消息被確認

            session.close();
        } catch (JMSException e) {
            e.printStackTrace();
        }finally {
            if(connection!=null){
                try {
                    connection.close();
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        }
    }

運行後效果:
在這裏插入圖片描述

案例總結

這個案例的架構圖如下
在這裏插入圖片描述

細化JMS的基本功能

通過前面的內容講解以及案例演示,我們已經知道了 JMS 規範以及他的基本功能是用於和麪向消息中間件相互通信的應用程序的接口,那麼 JMS 提供的具體標準有哪些呢?我們來仔細去研究下

消息傳遞域

JMS 規範中定義了兩種消息傳遞域:點對點(point-to-point ) 消 息 傳 遞 域 和 發 布 / 訂 閱 消 息 傳 遞 域 (publish/subscribe)

簡單理解就是:有點類似於我們通過 qq 聊天的時候,在羣裏面發消息和給其中一個同學私聊消息。在羣裏發消息,所有羣成員都能收到消息。私聊消息只能被私聊的學員能收到消息

點對點消息傳遞域

  1. 每個消息只能有一個消費者

  2. 消息的生產者和消費者之間沒有時間上的相關性。無論消費者在生產者發送消息的時候是否處於運行狀態,都可以提取消息
    在這裏插入圖片描述

發佈訂閱消息傳遞域

  1. 每個消息可以有多個消費者

  2. 生產者和消費者之間有時間上的相關性。訂閱一個主題的消費者只能消費自它訂閱之後發佈的消息。JMS 規範允許客戶創建持久訂閱,這在一定程度上降低了時間上的相關性要求。持久訂閱允許消費者消費它在未處於激活狀態時發送的消息
    在這裏插入圖片描述

消息結構組成

JMS 消息由及部分組成:消息頭、屬性、消息體

消息頭

消息頭(Header) - 消息頭包含消息的識別信息和路由信息,消息頭包含一些標準的屬性如:

JMSDestination 消息發送的目的地,queue 或者 topic) JMSDeliveryMode 傳送模式。持久模式和非持久模式

JMSPriority 消息優先級(優先級分爲 10 個級別,從 0(最低)到 9(最高). 如果不設定優先級,默認級別是 4。需要注意的是,JMS provider 並不一定保證按照優先級的順序提交消息)

JMSMessageID 唯一識別每個消息的標識

屬性

按類型可以分爲應用設置的屬性,標準屬性和消息中間件定義的屬性

1.應用程序設置和添加的屬性,比如

Message.setStringProperty(“key”,”value”);

通過下面的代碼可以獲得自定義屬性的,在接收端的代碼中編寫

在發送端,定義消息屬性

 message.setStringProperty("test","this is a activeMQ Producer");

在接收端接收數據

Enumeration enumeration=message.getPropertyNames();
while(enumeration.hasMoreElements()){
	String name=enumeration.nextElement().toString();
	System.out.println("name:"+name+":"+messag e.getStringProperty(name));

}

2.JMS 定義的屬性

使用“JMSX”作爲屬性名的前綴,通過下面這段代碼可以返回所有連接支持的 JMSX 屬性的名字

   			Enumeration names = connection.getMetaData();
            while (names.hasMoreElements()){
                System.out.println((String)names.nextElement());
            }

3.JMS provider 特定的屬性

消息體

就是我們需要傳遞的消息內容,JMS API 定義了 5 中消息體格式,可以使用不同形式發送接收數據,並可以兼容現有的消息格式,其中包括

消息格式 說明
TextMessage java.lang.String 對象,如 xml 文件內容
MapMessage 名/值對的集合,名是 String 對象,值類型可以是 Java 任何基本類型
BytesMessage 字節流
StreamMessage Java 中的輸入輸出流
ObjectMessage Java 中的可序列化對象
Message 沒有消息體,只有消息頭和屬性

絕大部分的時候,我們只需要基於消息體進行構造

持久訂閱

持久訂閱的概念,也很容易理解,比如還是以 QQ 爲例,我們把 QQ 退出了,但是下次登錄的時候,仍然能收到離線的消息。

持久訂閱就是這樣一個道理,持久訂閱有兩個特點:

  1. 持久訂閱者和非持久訂閱者針對的 Domain 是 Pub/Sub,而不是 P2P

  2. 當 Broker 發送消息給訂閱者時,如果訂閱者處於 未激活狀態狀態:持久訂閱者可以收到消息,而非持久訂閱者則收不到消息。

當然這種方式也有一定的影響:當持久訂閱者處於 未激活狀態時,Broker 需要爲持久訂閱者保存消息;如果持久訂閱者訂閱的消息太多則會溢出。

消費端改動

connection=connectionFactory.createConnection();
connection.setClientID("test");
connection.start();
Session session=connection.createSession(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
Topic destination=session.createTopic("myTopic");
MessageConsumer consumer=session.createDurableSubscriber(destination,"test");
TextMessagemessage=(TextMessage)consumer.receive();
System.out.println(message.getText());

先啓動消費端去註冊一個持久訂閱。

持久訂閱時,客戶端向 JMS 服務器註冊一個自己身份的 ID,當這個客戶端處於離線時,JMS Provider 會爲這個 ID 保存所有發送到主題的消息,當客戶再次連接到 JMS Provider 時,會根據自己的 ID 得到所有當自己處於離線時發送到主題的消息。

這個身份 ID,在代碼中的體現就是 connection 的 ClientID,這個其實很好理解,你要想收到朋友發送的 qq 消息,前提就是你得先註冊個 QQ 號,而且還要有臺能上網的設備,電腦或手機。設備就相當於是 clientId 是唯一的;qq 號相當於是訂閱者的名稱,在同一臺設備上,不能用同一個 qq 號掛 2 個客戶端。連接的 clientId 必須是唯一的,訂閱者的名稱在同一個連接內必須唯一。這樣才能唯一的確定連接和訂閱者。

JMS 消息的可靠性機制

理論上來說,我們需要保證消息中間件上的消息,只有被消費者確認過以後纔會被簽收,相當於我們寄一個快遞出去,收件人沒有收到快遞,就認爲這個包裹還是屬於待簽收狀態,這樣才能保證包裹能夠安全達到收件人手裏。消息中間件也是一樣。

消息的消費通常包含 3 個階段:客戶接收消息、客戶處理消息、消息被確認

首先,來簡單瞭解 JMS 的事務性會話和非事務性會話的概念

JMS Session 接口提供了 commit 和 rollback 方法。事務提交意味着生產的所有消息被髮送,消費的所有消息被確認;事務回滾意味着生產的所有消息被銷燬,消費的所有消息被恢復並重新提交,除非它們已經過期。 事務性的會話總是牽涉到事務處理中,commit 或 rollback 方法一旦被調用,一個事務就結束了,而另一個事務被開始。關閉事務性會話將回滾其中的事務

在事務型會話中

在事務狀態下進行發送操作,消息並未真正投遞到中間件,而只有進行 session.commit 操作之後,消息纔會發送到中
間件,再轉發到適當的消費者進行處理。如果是調用 rollback 操作,則表明,當前事務期間內所發送的消息都取消掉。通過在創建 session 的時候使用 true or false 來決定當前的會話是事務性還是非事務性 connection.createSession(Boolean.TRUE,Session.AUTO_ ACKNOWLEDGE);

在事務性會話中,消息的確認是自動進行,也就是通過session.commit()以後,消息會自動確認。

必須保證發送端和接收端都是事務性會話

在非事務型會話中

消 息 何 時 被 確 認 取 決 於 創 建 會 話 時 的 應 答 模 式 (acknowledgement mode). 有三個可選項

  • Session.AUTO_ACKNOWLEDGE
    當客戶成功的從 receive 方法返回的時候,或者從 MessageListenner.onMessage 方法成功返回的時候,會話自動確認客戶收到 消息。
  • Session.CLIENT_ACKNOWLEDGE
    客戶通過調用消息的 acknowledge 方法確認消息。
    CLIENT_ACKNOWLEDGE 特性
    在這種模式中,確認是在會話層上進行,確認一個被消費的消息將自動確認所有已被會話消費的消息。列如,如果一個消息消費者消費了 10 個消息,然後確認了第 5 個消息,那麼 0~5 的消息都會被確認

演示如下:發送端發送 10 個消息,接收端接收 10 個消息,但是在 i==5 的時候,調用 message.acknowledge()進行確認,會發現 0~4 的消息都會被確認
在這裏插入圖片描述

  • Session.DUPS_ACKNOWLEDGE
    消息延遲確認。指定消息提供者在消息接收者沒有確認發送時重新發送消息,這種模式不在乎接受者收到重複的消息。

消息的持久化存儲

消息的持久化存儲也是保證可靠性最重要的機制之一,也就是消息發送到 Broker 上以後,如果 broker 出現故障宕機了,那麼存儲在 broker 上的消息不應該丟失。可以通過下面的代碼來設置消息發送端的持久化和非持久化特性

MessageProducer producer=session.createProducer(destination);
producer.setDeliveryMode(DeliveryMode.PERSISTENT);

➢ 對於非持久的消息,JMS provider 不會將它存到文件/數據庫等穩定的存儲介質中。也就是說非持久消息駐留在內存中,如果 jms provider 宕機,那麼內存中的非持久消息會丟失

➢ 對於持久消息,消息提供者會使用存儲-轉發機制,先將消息存儲到穩定介質中,等消息發送成功後再刪除。如果 jms provider 掛掉了,那麼這些未送達的消息不會丟失;jms provider 恢復正常後,會重新讀取這些消息,並傳送給對應的消費者

後記

本節的代碼演示demo,下載地址:activeMQ-demo

贈送一本關於ActiveMQ的pdf,下載地址:《ActiveMQ_in_Action_中文版》
提取碼:ulkv

更多架構知識,歡迎關注本套Java系列文章,地址導航Java架構師成長之路

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章