ActiceMQ詳解

MPolaris發表於2021-01-19

1. MQ理解

1.1 MQ的產品種類和對比

MQ即訊息中介軟體。MQ是一種理念,ActiveMQ是MQ的落地產品。

訊息中介軟體產品

image-20210114005458326

各類MQ對比

image-20210114005649665
  • Kafka
    • 程式語言:Scala
    • 大資料領域的主流MQ
  • RabbitMQ
    • 程式語言:Erlang
    • 基於erlang語言,不好修改底層,不要查詢問題的原因,不建議選用。
  • RocketMQ
    • 程式語言:Java
    • 適用於大型專案,適用於叢集
  • ActiveMQ
    • 程式語言:Java
    • 適用於中小型專案
1.2 MQ產生背景

系統之間直接呼叫存在的問題?

微服務架構後,鏈式呼叫是我們在寫程式時候的一般流程,為了完成一個整體功能會將其拆分成多個函式(或子模組),比如模組A呼叫模組B,模組B呼叫模組C,模組C呼叫模組D。但在大型分散式應用中,系統間的RPC互動繁雜,一個功能背後要呼叫上百個介面並非不可能,從單機架構過渡到分散式微服務架構的通例。這些架構會有哪些問題?

  • 系統之間介面耦合比較嚴重

    每新增一個下游功能,都要對上游的相關介面進行改造。舉個例子:如果系統A要傳送資料給系統B和系統C,傳送給每個系統的資料可能有差異,因此係統A對要傳送給每個系統的資料進行了組裝,然後逐一傳送。當程式碼上線後又新增了一個需求:把資料也傳送給D,新上了一個D系統也要接受A系統的資料,此時就需要修改A系統,讓他感知到D系統的存在,同時把資料處理好再給D。在這個過程你會看到每接入一個下游系統都要對系統A進行程式碼改造,開發聯調的效率很低。其整體架構如下圖:

    image-20210114010536965
  • 面對大流量併發時容易被沖垮

    每個介面模組的吞吐能力是有限的,這個上限能力如果是堤壩,當大流量(洪水)來臨時容易被沖垮。舉例秒殺業務:上游系統發起下單購買操作就是下單一個操作很快就完成。然而下游系統要完成秒殺業務後面的所有邏輯(讀取訂單,庫存檢查,庫存凍結,餘額檢查,餘額凍結,訂單生產,餘額扣減,庫存減少,生成流水,餘額解凍,庫存解凍)。

  • 等待同步存在效能問題

    RPC介面上基本都是同步呼叫,整體的服務效能遵循“木桶理論”,即整體系統的耗時取決於鏈路中最慢的那個介面。比如A呼叫B/C/D都是50ms,但此時B又呼叫了B1,花費2000ms,那麼直接就拖累了整個服務效能。

    image-20210114010955756

根據上述的幾個問題,在設計系統時可以明確要達到的目標:

  1. 要做到系統解耦,當新的模組接進來時可以做到程式碼改動最小,能夠解耦

  2. 設定流量緩衝池,可以讓後端系統按照自身吞吐能力進行消費不被沖垮,能削峰

  3. 強弱依賴梳理能將非關鍵呼叫鏈路的操作非同步化並提升整體系統的吞吐能力,能夠非同步

1.3 MQ主要作用
  • 非同步 呼叫者無需等待
  • 解耦 解決了系統之間耦合呼叫的問題
  • 消峰 抵禦洪峰流量,保護了主業務
1.4 MQ的定義

面向訊息的中介軟體(message-oriented middleware)MOM能夠很好的解決以上問題。是指利用高效可靠的訊息傳遞機制與平臺無關的資料交流,並基於資料通訊來進行分散式系統的整合。通過提供訊息傳遞和訊息排隊模型在分散式環境下提供應用解耦,彈性伸縮,冗餘儲存、流量削峰,非同步通訊,資料同步等功能。

大致的過程是這樣的:傳送者把訊息傳送給訊息伺服器,訊息伺服器將訊息存放在若干佇列/主題topic中,在合適的時候訊息伺服器會將訊息轉發給接受者。在這個過程中傳送和接收是非同步的,也就是傳送無需等待,而且傳送者和接受者的生命週期也沒有必然的關係。尤其在釋出pub/訂閱sub模式下,也可以完成一對多的通訊即讓一個訊息有多個接受者。

image-20210114011510051
1.5 MQ特點

採用非同步處理模式

訊息傳送者可以傳送一個訊息而無須等待響應。訊息傳送者將訊息傳送到一條虛擬的通道(主題或者佇列)上。訊息接收者則訂閱或者監聽該通道。一條訊息可能最終轉發給一個或者多個訊息接收者,這些訊息接收者都無需對訊息傳送者做出同步回應。整個過程都是非同步的。

案例:也就是說一個系統跟另一個系統之間進行通訊的時候,假如系統A希望傳送一個訊息給系統B讓他去處理。但是系統A不關注系統B到底怎麼處理或者有沒有處理好,所以系統A把訊息傳送給MQ然後就不管這條訊息的“死活了”,接著系統B從MQ裡面消費出來處理即可。至於怎麼處理,是否處理完畢,什麼時候處理都是系統B的事兒與系統A無關。

應用系統之間解耦合

傳送者和接受者不必瞭解對方,只需要確認訊息。傳送者和接受者不必同時線上。

整體架構

image-20210114012651889

MQ缺點

兩個系統之間不能同步呼叫,不能實時回覆,不能響應某個呼叫的回覆。

1.6 CentOS7安裝ActiveMQ
cd /root
mkdir active_mq
tar -xzvf apache-activemq-5.14.0-bin.tar.gz

# /etc/init.d/目錄增加增加activemq檔案
cd /etc/init.d/
vim activemq

#!/bin/sh
#
# /etc/init.d/activemq
# chkconfig: 345 63 37
# description: activemq servlet container.
# processname: activemq 5.14.0

# Source function library.
#. /etc/init.d/functions
# source networking configuration.
#. /etc/sysconfig/network

export JAVA_HOME=/root/java/jdk1.8.0_221
export CATALINA_HOME=/root/active_mq/apache-activemq-5.14.0

case  $1 in
     start)
         sh $CATALINA_HOME/bin/activemq start
     ;;
     stop)
         sh $CATALINA_HOME/bin/activemq stop
     ;;
     restart)
         sh $CATALINA_HOME/bin/activemq stop
         sleep 1
         sh $CATALINA_HOME/bin/activemq start
     ;;

esac
exit 0

# 對activemq檔案授予許可權
chmod 777 activemq

# 設定開機啟動並啟動activemq
chkconfig activemq on
service activemq start

# 啟動時指定日誌輸出檔案,activemq日誌預設的位置是在:%activemq安裝目錄%/data/activemq.log
service activemq start  >  /root/active_mq/activemq.log

# 訪問地址:http://IP地址:8161/
# 預設賬戶:admin/admin
# 61616 埠提供JMS服務
# 8161 埠提供管理控制檯服務 

# 檢視activemq狀態
service activemq status

# 關閉activemq服務
service activemq stop

2. Java程式生成訊息基本案例

2.1 JMS簡介

JMS 總體編碼規範

image-20210114235714356

JMS開發基本步驟

image-20210114235837770

Destination

Destination 即目的地。下面拿 jvm 和 mq 做個對比,目的地可以理解為是資料儲存的地方。

image-20210115001050565

兩種Destination

image-20210115001820225
2.2 Idea新建Maven工程
<!--  activemq 所需要的jar包-->
<dependency>
	<groupId>org.apache.activemq</groupId>
	<artifactId>activemq-all</artifactId>
	<version>5.15.9</version>
</dependency>

<!--  activemq 和 spring 整合的基礎包 -->
<dependency>
	<groupId>org.apache.xbean</groupId>
	<artifactId>xbean-spring</artifactId>
	<version>3.16</version>
</dependency>

<!-- junit/log4j等基礎配置   -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
</dependency>
2.3 佇列訊息(Queue)

佇列訊息特點

點對點訊息傳遞域的特點如下

  • 每個訊息只能有一個消費者,類似 1對1 的關係。
  • 訊息的生產者和消費者之間 沒有時間上的相關性。無論消費者在生產者傳送訊息時是否處於執行狀態,消費者都可以提取訊息。如我們傳送簡訊,傳送者傳送後接受者不一定會及收及看。
  • 訊息被消費後佇列 不會再儲存,所以消費者 不會消費到已經被消費過的訊息。
image-20210115210703169

佇列訊息生產者

public class JmsProduce {
    public static final String ACTIVEMQ_URL = "tcp://mpolaris.top:61616";
    public static final String QUEUE_NAME = "queue_01";

    public static void main(String[] args) throws JMSException {
        //1.建立連線工廠,按照給定的url地址,採用預設使用者名稱和密碼
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        //2.通過連線工廠獲得連線connection並啟動訪問
        Connection conn = factory.createConnection();
        conn.start();
        //3.建立會話session
        //  兩個引數:事務,簽收
        Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
        //4.建立目的地(具體是佇列queue還是主題topic)
        //  Destination -> Queue/Topic
        Queue queue = session.createQueue(QUEUE_NAME);
        //5.建立訊息的生產者
        MessageProducer producer = session.createProducer(queue);
        //6.通過使用訊息生產者傳送三條訊息到MQ佇列中
        for (int i = 0; i < 3; i++) {
            //建立訊息
            TextMessage textMessage = session.createTextMessage("msg -> " + i);
            //通過訊息生產者傳送給MQ
            producer.send(textMessage);
        }
        //7.關閉資源
        producer.close();
        session.close();
        conn.close();

        System.out.println("====> 訊息釋出到MQ完成");
    }
}
image-20210115005955450

佇列訊息消費者 - 同步阻塞式 receive

public class JmsConsumer {
    public static final String ACTIVEMQ_URL = "tcp://mpolaris.top:61616";
    public static final String QUEUE_NAME = "queue_01";

    public static void main(String[] args) throws JMSException {
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        Connection conn = factory.createConnection();
        conn.start();
        Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Queue queue = session.createQueue(QUEUE_NAME);

        // 建立訊息的消費者
        MessageConsumer consumer = session.createConsumer(queue);
        while (true) {
            // reveive():一直等待接收訊息,在能夠接收到訊息之前將一直阻塞。 是同步阻塞方式,和socket的accept方法類似的。
            // reveive(Long time):等待n毫秒之後還沒有收到訊息就結束阻塞。
            // 因為訊息傳送者是 TextMessage,所以訊息接受者也要是TextMessage
            TextMessage message = (TextMessage) consumer.receive(4000L);
            if (null != message) {
                System.out.println("====> 消費者的訊息:" + message.getText());
            } else {
                break;
            }
        }

        consumer.close();
        session.close();
        conn.close();
    }
}
image-20210115011322834

佇列訊息消費者 - 非同步非阻塞監聽式 MessageListener

public class JmsConsumer {
    public static final String ACTIVEMQ_URL = "tcp://mpolaris.top:61616";
    public static final String QUEUE_NAME = "queue_01";

    public static void main(String[] args) throws Exception {
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        Connection conn = factory.createConnection();
        conn.start();
        Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Queue queue = session.createQueue(QUEUE_NAME);
        MessageConsumer consumer = session.createConsumer(queue);

        // 監聽器
        consumer.setMessageListener(new MessageListener() {
            @Override
            public void onMessage(Message message) {
                if(null != message && message instanceof TextMessage) {
                    TextMessage textMessage = (TextMessage) message;
                    try {
                        System.out.println("====> 消費者接受到訊息:" + textMessage.getText());
                    } catch (JMSException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        System.in.read(); //保證控制檯不停

        consumer.close();
        session.close();
        conn.close();
    }
}

消費者三種情況

  • 先生產,只啟動一個消費者 ① => ①消費者會消費掉全部訊息
  • 先生產,然後先啟動消費者①,再啟動消費者② => ①消費者會消費掉全部訊息,②消費者不能消費訊息
  • 先啟動消費者①和②,再生產 => ①和②輪詢消費,各自消費一半訊息
2.4 主題訊息(Topic)

主題訊息特點

在釋出訂閱訊息傳遞域中,目的地被稱為主題(topic)。

釋出/訂閱訊息傳遞域的特點如下:

  • 生產者將訊息釋出到topic中,每個訊息可以有多個消費者,屬於1:N的關係。

  • 生產者和消費者之間有時間上的相關性。訂閱某一個主題的消費者只能消費 自它訂閱之後釋出的訊息。

  • 生產者生產時,topic 不儲存訊息,它是 無狀態的 不落地的,假如無人訂閱就去生產那就是一條廢訊息,所以一般先啟動消費者再啟動生產者。

預設情況下如上所述,但是JMS規範允許客戶建立持久訂閱,這在一定程度上放鬆了時間上的相關性要求。持久訂閱允許消費者消費它在未處於啟用狀態時傳送的訊息。一句話,好比我們的微信公眾號訂閱。

image-20210115212649958

主題訊息生產者

public class JmsProduce {
    public static final String ACTIVEMQ_URL = "tcp://mpolaris.top:61616";
    public static final String TOPIC_NAME = "topic_01";

    public static void main(String[] args) throws JMSException {
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        Connection conn = factory.createConnection();
        conn.start();
        Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);

        //只有這一步和Queue有區別
        Topic topic = session.createTopic(TOPIC_NAME);

        MessageProducer producer = session.createProducer(topic);
        for (int i = 0; i < 3; i++) {
            TextMessage textMessage = session.createTextMessage("msg -> " + i);
            producer.send(textMessage);
        }
        producer.close();
        session.close();
        conn.close();

        System.out.println("====> 訊息釋出到MQ完成");
    }
}

主題訊息消費者

存在多個消費者,每個消費者都能收到自從自己啟動後所有生產的訊息。

public class JmsConsumer {
    public static final String ACTIVEMQ_URL = "tcp://mpolaris.top:61616";
    public static final String TOPIC_NAME = "topic_01";

    public static void main(String[] args) throws Exception {
        System.out.println("=====> 1號消費者");//多加幾個消費者做實驗

        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        Connection conn = factory.createConnection();
        conn.start();
        Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);

        //只有這一步和Queue有區別
        Topic topic = session.createTopic(TOPIC_NAME);

        MessageConsumer consumer = session.createConsumer(topic);
        consumer.setMessageListener(new MessageListener() {
            @Override
            public void onMessage(Message message) {
                if(null != message && message instanceof TextMessage) {
                    TextMessage textMessage = (TextMessage) message;
                    try {
                        System.out.println("====> 消費者接受到訊息:" + textMessage.getText());
                    } catch (JMSException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        System.in.read();

        consumer.close();
        session.close();
        conn.close();
    }
}
image-20210115214856154
2.5 Topic和Queue對比
image-20210115215225937

3. JMS (Java訊息服務) 詳解

3.1 Java訊息服務是什麼

Java訊息服務指的是兩個應用程式之間進行非同步通訊的API,它為標準協議和訊息服務提供了一組通用介面,包括建立、傳送、讀取訊息等,用於支援Java應用程式開發。在JavaEE中當兩個應用程式使用JMS進行通訊時,它們之間不是直接相連的,而是通過一個共同的訊息收發服務元件關聯起來以達到解耦/非同步削峰的效果。

image-20210115221749323
3.2 JMS四大組成元素
image-20210115221836939

Message - 訊息頭

JMS的訊息頭有哪些屬性:

  • JMSDestination:訊息目的地。主要是指Queue和Topic。

  • JMSDeliveryMode:訊息持久化模式。分為持久模式和非持久模式,一條永續性的訊息應該被傳送“一次僅僅一次”,這就意味著如果JMS提供者出現故障,該訊息並不會丟失,它會在伺服器恢復之後再次傳遞。一條非持久的訊息最多會傳遞一次,這意味著伺服器出現故障,該訊息將會永遠丟失。

  • JMSExpiration:訊息過期時間。可以設定訊息在一定時間後過期,預設是永不過期訊息過期時間,等於Destination的send方法中的timeToLive值加上傳送時刻的GMT時間值。如果timeToLive值等於0,則JMSExpiration被設為0,表示該訊息永不過期。如果傳送後在訊息過期時間之後還沒有被髮送到目的地,則該訊息被清除。

  • JMSPriority:訊息的優先順序。訊息優先順序從0-9十個級別,0-4是普通訊息,5-9是加急訊息。 JMS不要求MQ嚴格按照這十個優先順序傳送訊息但必須保證加急訊息要先於普通訊息到達。預設是4級。

  • JMSMessageID:訊息的唯一識別符號。唯一標識每個訊息的標識由MQ產生,也可以自己指定但是每個訊息的標識要求唯一。

說明:訊息的生產者可以set這些屬性,訊息的消費者可以get這些屬性。這些屬性在send方法裡面也可以設定。

public class JmsProduce {
    public static final String ACTIVEMQ_URL = "tcp://mpolaris.top:61616";
    public static final String TOPIC_NAME = "topic_01";

    public static void main(String[] args) throws JMSException {
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        Connection conn = factory.createConnection();
        conn.start();
        Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Topic topic = session.createTopic(TOPIC_NAME);
        MessageProducer producer = session.createProducer(topic);
        for (int i = 0; i < 3; i++) {
            TextMessage textMessage = session.createTextMessage("msg -> " + i);

            //這裡可以指定每個訊息的目的地
            textMessage.setJMSDestination(topic);
            //訊息的模式,持久模式/非持久模式
            textMessage.setJMSDeliveryMode(0);
            //訊息的過期時間
            textMessage.setJMSExpiration(1000);
            //訊息的優先順序
            textMessage.setJMSPriority(10);
            //指定每個訊息的標識。MQ會給我們預設生成一個,我們也可以自己指定。
            textMessage.setJMSMessageID("ABCD");
            
            //上面的屬性也可以通過send過載方法進行設定
            producer.send(textMessage);
        }
        producer.close();
        session.close();
        conn.close();

        System.out.println("====> 訊息釋出到MQ完成");
    }
}

Message - 訊息體

理解:封裝具體的訊息資料

五種訊息格式

image-20210115232313263

注意:傳送和接收的訊息體型別必須一致對應

訊息生產者

for (int i = 0; i < 3; i++) {
	// 傳送TextMessage訊息體
	TextMessage textMessage = session.createTextMessage("topic " + i);
	producer.send(textMessage);

	// 傳送MapMessage 訊息體。set方法新增,get方式獲取
	MapMessage mapMessage = session.createMapMessage();
	mapMessage.setString("name","rose" + i);
	mapMessage.setInt("age", 18 + i);
	producer.send(mapMessage);
}

訊息消費者

public void onMessage(Message message) {
    if(null != message && message instanceof TextMessage) {
         TextMessage textMessage = (TextMessage) message;
         try {
             System.out.println("====> 消費者接受到text訊息:" + textMessage.getText());
         } catch (JMSException e) {
              e.printStackTrace();
         }
     }

     if(null != message && message instanceof MapMessage) {
          MapMessage mapMessage = (MapMessage) message;
          try {
               System.out.println("====> 消費者接受到map訊息:" + mapMessage.getString("name"));
               System.out.println("====> 消費者接受到map訊息:" + mapMessage.getString("age"));
           } catch (JMSException e) {
               e.printStackTrace();
           }
      }
}

Message - 訊息屬性

如果需要除訊息頭欄位之外的值那麼可以使用訊息屬性。它是 識別 / 去重 / 重點標註 等操作非常有用的方法。

它們是以屬性名和屬性值對的形式制定的。可以將屬性是為訊息頭得擴充套件,屬性指定一些訊息頭沒有包括的附加資訊,比如可以在屬性裡指定訊息選擇器。訊息的屬性就像可以分配給一條訊息的附加訊息頭一樣。它們允許開發者新增有關訊息的不透明附加資訊。它們還用於暴露訊息選擇器在訊息過濾時使用的資料。

下圖是設定訊息屬性的API:

image-20210115233623890

生產者

for (int i = 0; i < 3; i++) {
	TextMessage textMessage = session.createTextMessage("topic " + i);

	// 呼叫Message的set*Property()方法就能設定訊息屬性
	// 根據value的資料型別的不同,有相應的API
	textMessage.setStringProperty("From","rose@qq.com");
	textMessage.setByteProperty("Spec", (byte) 1);
	textMessage.setBooleanProperty("Invalide",true);

	producer.send(textMessage);
}

消費者

public void onMessage(Message message) {
	if(null != message && message instanceof TextMessage) {
		TextMessage textMessage = (TextMessage) message;
		try {
			System.out.println("訊息體:" + textMessage.getText());
			System.out.println("訊息屬性:" + textMessage.getStringProperty("From"));
			System.out.println("訊息屬性:" + textMessage.getByteProperty("Spec"));
			System.out.println("訊息屬性:" + textMessage.getBooleanProperty("Invalide"));
		} catch (JMSException e) {
			e.printStackTrace();
		}
	}
}
3.5 JMS的可靠性

RERSISTENT - 永續性

什麼是持久化訊息 => 保證訊息只被傳送一次和成功使用一次。在永續性訊息傳送至目標時,訊息服務將其放入永續性資料儲存。如果訊息服務由於某種原因導致失敗,它可以恢復此訊息並將此訊息傳送至相應的消費者,雖然這樣增加了訊息傳送的開銷但卻增加了可靠性。

我的理解:在訊息生產者將訊息成功傳送給MQ訊息中介軟體之後。無論是出現任何問題如:MQ伺服器當機、消費者掉線等。都保證(topic要之前註冊過,queue不用)訊息消費者能夠成功消費訊息。如果訊息生產者傳送訊息就失敗了,那麼消費者也不會消費到該訊息。

image-20210115235300769
  1. Queue訊息非持久和持久
  • Queue非持久,當伺服器當機訊息不存在(訊息丟失了)。

注意:只要伺服器沒有當機,即便是非持久,消費者不線上的話訊息也不會丟失,等待消費者線上還是能夠收到訊息的。

//非持久化的消費者和之前的程式碼一樣。下面演示非持久化的生產者。

// 非持久化
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
  • Queue持久化,當伺服器當機訊息依然存在。Queue訊息預設是持久化的。

持久化訊息,保證這些訊息只被傳送一次和成功使用一次。對於這些訊息可靠性是優先考慮的因素。可靠性的另一個重要方面是確保永續性訊息傳送至目標後,訊息服務在向消費者傳送它們之前不會丟失這些訊息。

//持久化的消費者和之前的程式碼一樣。下面演示持久化的生產者。

//持久化
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
  1. Topic訊息非持久和持久
  • Topic非持久,Topic預設就是非持久化的,因為生產者生產訊息時消費者也要線上,這樣消費者才能消費到訊息。

  • Topic訊息持久化,只要消費者向MQ伺服器註冊過,所有生產者釋出成功的訊息該消費者都能收到,不管是MQ伺服器當機還是消費者不線上。

//持久化topic生產者程式碼

// 設定持久化topic 
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
// 設定持久化topic之後再啟動連線
conn.start();
//持久化topic消費者程式碼

public static void main(String[] args) throws Exception{
    ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
    Connection conn = activeMQConnectionFactory.createConnection();
    
	// 設定客戶端ID,向MQ伺服器註冊自己的名稱
    conn.setClientID("marrry");
    
    Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
    Topic topic = session.createTopic(TOPIC_NAME);
    
	// 建立一個topic訂閱者物件。一參是topic,二參是訂閱者名稱
    TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic,"remark...");
    // 之後再開啟連線
    connection.start();
    
    //之前是訊息的消費者,這裡就改為主題的訂閱者
    Message message = topicSubscriber.receive();
    while (null != message){
        TextMessage textMessage = (TextMessage)message;
        System.out.println(" 收到的持久化 topic:" + textMessage.getText());
        message = topicSubscriber.receive(2000L);//繼續監聽2s,從啟用到離線
        //經測試:離線再啟用後仍然能收到之前的訊息
    }
    
    session.close();
    conn.close();
}
image-20210116000703132

注意:

  1. 一定要先執行一次消費者,等於向MQ註冊,類似我訂閱了這個主題。

  2. 然後再執行生產者傳送訊息。

  3. 之後無論消費者是否線上都會收到訊息。如果不線上的話,下次連線的時候會把沒有收過的訊息都接收過來。

Transaction - 事務

生產者開啟事務後,執行commit方法這批訊息才真正的被提交。不執行commit方法這批訊息不會提交。執行rollback方法之前的訊息會回滾掉。生產者的事務機制要高於簽收機制,當生產者開啟事務後簽收機制不再重要。

消費者開啟事務後,執行commit方法這批訊息才算真正的被消費。不執行commit方法這些訊息不會標記已消費,下次還會被消費。執行rollback方法不能回滾之前執行過的業務邏輯,但是能夠回滾之前的訊息,回滾後的訊息下次還會被消費。消費者利用commit和rollback方法,甚至能夠違反一個消費者只能消費一次訊息的原理。

image-20210116004820189

注意:消費者和生產者需要同時操作事務才行嗎? => 消費者和生產者的事務完全沒有關聯,各自是各自的事務。

  • 生產者
public class JmsProduce {
    public static final String ACTIVEMQ_URL = "tcp://mpolaris.top:61616";
    public static final String TOPIC_NAME = "topic_01";

    public static void main(String[] args) throws JMSException {
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        Connection conn = factory.createConnection();
        conn.start();

        //1.建立會話session,兩個引數transacted=事務,acknowledgeMode=確認模式(簽收)
        //設定為開啟事務
        Session session = conn.createSession(true, Session.AUTO_ACKNOWLEDGE);

        Topic topic = session.createTopic(TOPIC_NAME);
        MessageProducer producer = session.createProducer(topic);

        try {
            for (int i = 0; i < 3; i++) {
                TextMessage textMessage = session.createTextMessage("topic " + i);
                producer.send(textMessage);
//                if(i == 2) {
//                    throw new RuntimeException("=====> GG");
//                }
            }

            // 2. 開啟事務後,使用commit提交事務,這樣這批訊息才能真正的被提交。
            session.commit();

            System.out.println("====> 訊息釋出到MQ完成");
        } catch (JMSException e) {
            System.out.println("出現異常,訊息回滾");

            // 3. 工作中一般當程式碼出錯我們在catch程式碼塊中回滾。這樣這批傳送的訊息就能回滾。
            session.rollback();

        } finally {
            producer.close();
            session.close();
            conn.close();
        }
    }
}

//如果有一條丟擲異常,則回滾
//Exception in thread "main" java.lang.RuntimeException: =====> GG
  • 消費者
public class JmsConsumer {
    public static final String ACTIVEMQ_URL = "tcp://mpolaris.top:61616";
    public static final String TOPIC_NAME = "topic_01";

    public static void main(String[] args) throws Exception {
        System.out.println("=====> 1號消費者");

        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        Connection conn = factory.createConnection();
        conn.start();

        // 建立會話session,兩個引數transacted=事務,acknowledgeMode=確認模式(簽收)
        // 消費者開啟了事務就必須手動提交,不然會重複消費訊息
        final Session session = conn.createSession(true, Session.AUTO_ACKNOWLEDGE);

        Topic topic = session.createTopic(TOPIC_NAME);
        MessageConsumer consumer = session.createConsumer(topic);
        consumer.setMessageListener(new MessageListener() {
            int a = 0;
            @Override
            public void onMessage(Message message) {
                if(null != message && message instanceof TextMessage) {
                    TextMessage textMessage = (TextMessage) message;
                    try {
                        System.out.println("訊息體:" + textMessage.getText());
                        if(a == 0){
                            System.out.println("commit");
                            session.commit();
                        }
                        if (a == 2) {
                            System.out.println("rollback");
                            session.rollback();
                        }
                        a++;
                    } catch (JMSException e) {
                        System.out.println("出現異常,消費失敗,放棄消費");
                        try {
                            session.rollback();
                        } catch (JMSException ex) {
                            ex.printStackTrace();
                        }
                    }
                }
            }
        });
        System.in.read();
        consumer.close();
        session.close();
        conn.close();
    }
}

// 不執行commit方法的1和2訊息不會標記已消費,下次還會被消費
// 執行rollback方法不能回滾之前執行過的業務邏輯,但是能夠回滾之前的訊息,回滾後的訊息下次還會被消費

// =====> 1號消費者
// 訊息體:topic 0
// commit
// 訊息體:topic 1
// 訊息體:topic 2
// rollback
// 訊息體:topic 1
// 訊息體:topic 2

Acknowledge - 簽收

簽收的幾種方式

  • 自動簽收(Session.AUTO_ACKNOWLEDGE):該方式是預設的,該種方式無需我們程式做任何操作,框架會幫我們自動簽收收到的訊息。

  • 手動簽收(Session.CLIENT_ACKNOWLEDGE):手動簽收,該種方式需要我們手動呼叫Message.acknowledge()來簽收訊息。如果不簽收訊息該訊息會被我們反覆消費直到被簽收。

  • 允許重複訊息(Session.DUPS_OK_ACKNOWLEDGE):多執行緒或多個消費者同時消費到一個訊息,因為執行緒不安全可能會重複消費。該種方式很少使用到。

  • 事務下的簽收(Session.SESSION_TRANSACTED):開啟事務的情況下可以使用該方式,該種方式很少使用到。

事務和簽收的關係

  • 在事務性會話中,當一個事務被成功提交則訊息被自動簽收。如果事務回滾則訊息會被再次傳送。事務優先於簽收,開始事務後簽收機制不再起任何作用。

  • 非事務性會話中,訊息何時被確認取決於建立會話時的應答模式。

  • 生產者事務開啟,只有commit後才能將全部訊息變為已消費。

  • 事務偏向生產者,簽收偏向消費者。也就是說生產者使用事務更好點,消費者使用簽收機制更好點。

非事務下的消費者如何使用手動簽收的方式

  • 非事務下的生產者跟之前的程式碼一樣
public class JmsProduce {
    public static final String ACTIVEMQ_URL = "tcp://mpolaris.top:61616";
    public static final String QUEUE_NAME = "queue_01";

    public static void main(String[] args) throws JMSException {
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        Connection conn = factory.createConnection();
        conn.start();
        Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Queue queue = session.createQueue(QUEUE_NAME);
        MessageProducer producer = session.createProducer(queue);
        for (int i = 0; i < 3; i++) {
            TextMessage textMessage = session.createTextMessage("msg -> " + i);
            producer.send(textMessage);
        }
        producer.close();
        session.close();
        conn.close();

        System.out.println("====> 訊息釋出到MQ完成");
    }
}
  • 非事務下的消費者如何手動簽收
public class JmsConsumer {
    public static final String ACTIVEMQ_URL = "tcp://mpolaris.top:61616";
    public static final String QUEUE_NAME = "queue_01";

    public static void main(String[] args) throws JMSException {
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        Connection conn = factory.createConnection();
        conn.start();
        //這裡改為Session.CLIENT_ACKNOWLEDGE
        Session session = conn.createSession(false, Session.CLIENT_ACKNOWLEDGE);
        Queue queue = session.createQueue(QUEUE_NAME);

        MessageConsumer consumer = session.createConsumer(queue);
        while (true) {
            TextMessage message = (TextMessage) consumer.receive(4000L);
            if (null != message) {
                System.out.println("====> 消費者的訊息:" + message.getText());
                
                //設定為Session.CLIENT_ACKNOWLEDGE後,要呼叫該方法,標誌著該訊息已被簽收(消費)。
                //如果不呼叫該方法,該訊息的標誌還是未消費,下次啟動消費者或其他消費者還會收到改訊息。
                message.acknowledge();
            } else {
                break;
            }
        }

        consumer.close();
        session.close();
        conn.close();
    }
}

注意:JMS保證可靠有四種方式,除了上面講到的永續性,事務,簽收,還可以通過多節點叢集的方式來保證可靠性。

3.6 JMS的點對點總結

點對點模型是基於佇列的,生產者發訊息到佇列,消費者從佇列接收訊息,佇列的存在使得訊息的非同步傳輸成為可能,和我們平時給朋友傳送簡訊類似。

如果在Session關閉時有部分訊息己被收到但還沒有被簽收(acknowledged),那當消費者下次連線到相同的佇列時,這些訊息還會被再次接收。

佇列可以長久地儲存訊息直到消費者收到訊息,消費者不需要因為擔心訊息會丟失而時刻和佇列保持啟用的連線狀態,充分體現了非同步傳輸模式的優勢。

3.7 JMS的釋出訂閱總結

JMS的釋出訂閱總結

JMS Pub/Sub 模型定義瞭如何向一個內容節點發布和訂閱訊息,這些節點被稱作Topic。

主題可以被認為是訊息的傳輸中介,釋出者(publisher)釋出訊息到主題,訂閱者(subscribe)從主題訂閱訊息。

主題使得訊息訂閱者和訊息釋出者保持互相獨立,不需要解除即可保證訊息的傳送。

非持久訂閱

非持久訂閱只有當客戶端處於啟用狀態,也就是和MQ保持連線狀態才能收發到某個主題的訊息。

如果消費者處於離線狀態,生產者傳送的主題訊息將會丟失作廢,消費者永遠不會收到。一句話:先訂閱註冊才能接受到釋出,只給訂閱者釋出訊息。

持久訂閱

客戶端首先向MQ註冊一個自己的身份ID識別號,當這個客戶端處於離線時,生產者會為這個ID儲存所有傳送到主題的訊息,當客戶再次連線到MQ的時候,會根據消費者的ID得到所有當自己處於離線時傳送到主題的訊息。

當非持久訂閱狀態下,不能恢復或重新派送一個未簽收的訊息。持久訂閱才能恢復或重新派送一個未簽收的訊息。

非持久和持久化訂閱如何選擇

當所有的訊息必須被接收則用持久化訂閱,當訊息丟失能夠被容忍則用非持久訂閱。

4. ActiveMQ的Broker

4.1 broker是什麼

相當於 一個ActiveMQ伺服器例項。說白了Broker其實就是實現了用程式碼的形式啟動ActiveMQ將MQ嵌入到Java程式碼中,以便隨時用隨時啟動,在用的時候再去啟動這樣能節省了資源,也保證了可用性。這種方式,我們實際開發中很少採用,因為他缺少太多了東西,如:日誌,資料儲存等等。

4.2 啟動broker時指定配置檔案

啟動broker時指定配置檔案,可以幫助我們在一臺伺服器上啟動多個broker。實際工作中一般一臺伺服器只啟動一個broker。

image-20210116022208482
4.3 嵌入式的broker啟動

用ActiveMQ Broker作為獨立的訊息伺服器來構建Java應用。

ActiveMQ也支援在vm中通訊基於嵌入的broker,能夠無縫的整合其他java應用。

下面演示如何啟動嵌入式的broker

pom.xml新增一個依賴

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.10.1</version>
</dependency>

嵌入式broker的啟動類

import org.apache.activemq.broker.BrokerService;

public class EmbedBroker {
    public static void main(String[] args) throws Exception {
        //ActiveMQ也支援在vm中通訊基於嵌入的broker
        BrokerService brokerService = new BrokerService();
        brokerService.setPopulateJMSXUserID(true);
        brokerService.addConnector("tcp://127.0.0.1:61616");
        brokerService.start();
   }
}
image-20210116022845378 image-20210116022903297
public class JmsProduce {
    public static final String ACTIVEMQ_URL = "tcp://localhost:61616";
    public static final String QUEUE_NAME = "queue_01";
	...
	
	
public class JmsConsumer {
    public static final String ACTIVEMQ_URL = "tcp://localhost:61616";
    public static final String QUEUE_NAME = "queue_01";
    ...

5. Spring整合ActiveMQ

理解

我們之前介紹的內容也很重要,它更靈活,支援各種自定義功能,可以滿足我們工作中複雜的需求。

很多activemq的功能要看官方文件或者部落格,這些功能大多是在上面程式碼的基礎上修改完善的。如果非要把這些功能強行整合到spring,就有些緣木求魚了。而另一種方式整合spring更好,就是將上面的類注入到Spring中,其他不變。這樣既能保持原生的程式碼,又能整合到spring。

下面我們講的Spring和SpringBoot整合ActiveMQ也重要,它給我們提供了一個模板,簡化了程式碼,減少我們工作中遇到坑,能夠滿足開發中90%以上的功能。

**pom.xml新增依賴 **

<dependencies>
	<!--  ActiveMQ 所需要的jar包-->
	<dependency>
		<groupId>org.apache.activemq</groupId>
		<artifactId>activemq-all</artifactId>
		<version>5.15.9</version>
	</dependency>
	<!--  ActiveMQ 和 Spring 整合的基礎包 -->
	<dependency>
		<groupId>org.apache.xbean</groupId>
		<artifactId>xbean-spring</artifactId>
		<version>3.16</version>
	</dependency>
	<!-- 嵌入式ActiveMQ -->
	<dependency>
		<groupId>com.fasterxml.jackson.core</groupId>
		<artifactId>jackson-databind</artifactId>
		<version>2.10.1</version>
	</dependency>

	<!-- Spring對JMS的支援,整合Spring和ActiveMQ -->
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-jms</artifactId>
		<version>5.2.1.RELEASE</version>
		</dependency>
	<!-- ActiveMQ連線池 -->
	<dependency>
		<groupId>org.apache.activemq</groupId>
		<artifactId>activemq-pool</artifactId>
		<version>5.15.10</version>
	</dependency>

	<!-- Spring核心依賴 -->
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-core</artifactId>
		<version>4.3.23.RELEASE</version>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-context</artifactId>
		<version>4.3.23.RELEASE</version>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-aop</artifactId>
		<version>4.3.23.RELEASE</version>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-orm</artifactId>
		<version>4.3.23.RELEASE</version>
	</dependency>

	<!-- junit/log4j等基礎配置   -->
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-api</artifactId>
		<version>1.7.25</version>
	</dependency>
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.12</version>
		<scope>test</scope>
	</dependency>
</dependencies>

Spring的ActiveMQ配置檔案

src/main/resources/spring-activemq.cml

<?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:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd 
       http://www.springframework.org/schema/context 
       https://www.springframework.org/schema/context/spring-context.xsd">

    <!--  開啟包的自動掃描  -->
    <context:component-scan base-package="com.polaris"/>
    
    <!--  配置生產者  -->
    <bean id="connectionFactory" 
          class="org.apache.activemq.pool.PooledConnectionFactory" 
          destroy-method="stop">
        <property name="connectionFactory">
            <!--  真正可以生產Connection的ConnectionFactory,由對應的JMS服務商提供  -->
            <bean class="org.apache.activemq.spring.ActiveMQConnectionFactory">
                <property name="brokerURL" value="tcp://mpolaris.top:61616"/>
            </bean>
        </property>
        <property name="maxConnections" value="100"/>
    </bean>

    <!--  這個是佇列目的地,點對點的Queue  -->
    <bean id="destinationQueue" class="org.apache.activemq.command.ActiveMQQueue">
        <!--    通過構造注入Queue名    -->
        <constructor-arg index="0" value="spring-active-queue"/>
    </bean>

    <!--  這個是主題目的地,  釋出訂閱的主題Topic-->
    <bean id="destinationTopic" class="org.apache.activemq.command.ActiveMQTopic">
        <constructor-arg index="0" value="spring-active-topic"/>
    </bean>

    <!--  Spring提供的JMS工具類,他可以進行訊息傳送,接收等  -->
    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
        <!--    傳入連線工廠    -->
        <property name="connectionFactory" ref="connectionFactory"/>
        <!--    傳入目的地    -->
        <property name="defaultDestination" ref="destinationQueue"/>
        <!--    訊息自動轉換器    -->
        <property name="messageConverter">
            <bean class="org.springframework.jms.support.converter.SimpleMessageConverter"/>
        </property>
    </bean>
</beans>

佇列生產者

@Service
public class JmsProduce {
    @Autowired
    private JmsTemplate jmsTemplate;

    public static void main(String[] args) {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-activemq.xml");
        JmsProduce produce = (JmsProduce) ioc.getBean("jmsProduce");

        produce.jmsTemplate.send(new MessageCreator() {
            @Override
            public Message createMessage(Session session) throws JMSException {
                TextMessage message = session.createTextMessage("====> Spring和ActiveMQ的整合情況");
                return message;
            }
        });

        System.out.println("Send task over!");
    }
}

佇列消費者

@Service
public class JmsConsumer {
    @Autowired
    private JmsTemplate jmsTemplate;

    public static void main(String[] args) {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-activemq.xml");
        JmsConsumer consumer = (JmsConsumer) ioc.getBean("jmsConsumer");

        String value = (String) consumer.jmsTemplate.receiveAndConvert();
        System.out.println(value);
    }
}

主題生產者和消費者

只需要修改配置檔案目的地即可

<!--  Spring提供的JMS工具類,他可以進行訊息傳送,接收等  -->
    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
        <!--    傳入連線工廠    -->
        <property name="connectionFactory" ref="connectionFactory"/>
        <!--    傳入目的地    -->
        <property name="defaultDestination" ref="destinationTopic"/>
        <!--    訊息自動轉換器    -->
        <property name="messageConverter">
            <bean class="org.springframework.jms.support.converter.SimpleMessageConverter"/>
        </property>
    </bean>

配置消費者的監聽類

寫了一個類來實現訊息監聽後,只需要啟動生產者,消費者不需要啟動就自動會監聽記錄!

<!-- 配置監聽程式 -->
<bean id="jmsContainer"
      class="org.springframework.jms.listener.DefaultMessageListenerContainer">
    <property name="connectionFactory" ref="connectionFactory"/>
    <property name="destination" ref="destinationTopic"/>
</bean>

<bean id="myMessageListener" class="com.polaris.queue.MyMessageListener">
</bean>
@Component
public class MyMessageListener implements MessageListener {
    @Override
    public void onMessage(Message message) {
        if(null != message && message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            try {
                System.out.println(textMessage.getText());
            } catch (JMSException e) {
                e.printStackTrace();
            }
        }
    }
}

6. SpringBoot整合ActiveMQ

個人不太贊成使用這種方式SpringBoot整合ActiveMQ,因為這樣做會失去原生程式碼的部分功能和靈活性。但是工作中這種方式做能夠滿足我們常見的需求,也方便和簡化我們的程式碼,也為了適應工作中大家的習慣。

6.1 佇列案例 - 生產者點選投遞

pom.xml檔案

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
        <relativePath/>
    </parent>

    <groupId>com.polaris</groupId>
    <artifactId>springboot-activemq</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-activemq</artifactId>
            <version>2.1.5.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.yml

# web佔用的埠
server:
  port: 8085

spring:
  activemq:
    # activemq的broker的url
    broker-url: tcp://mpolaris.top:61616
    # 連線activemq的broker所需的賬號和密碼
    user: admin
    password: admin
  jms:
    # 目的地是queue還是topic, false(預設)=queue    true=topic
    pub-sub-domain: false

# 自定義佇列名稱,這只是個常量
myqueue: boot-activemq-queue

ActiveMQ配置類

@Configuration
@EnableJms   //開啟Jms適配的註解
public class ConfigBean {

    @Value("${myqueue}")
    private String myQueue;

    //注入目的地
    @Bean
    public Queue queue() {
        return new ActiveMQQueue(myQueue);
    }

}

佇列訊息生產者

@Component
public class QueueProduce {
    @Autowired
    private JmsMessagingTemplate jmsMessagingTemplate;

    @Autowired
    private Queue queue;

    public void produceMsg() {
        jmsMessagingTemplate.convertAndSend(queue,"===> SpringBoot + ActiveMQ訊息");
    }
}

測試類

@SpringBootTest(classes = Application.class)
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
public class TestActiveMQ {
    @Resource   //這個是java 的註解,而Autowried是spring 的
    private QueueProduce produce;

    @Test
    public void testSend() {
        produce.produceMsg();
    }
}
6.2 佇列案例 - 生產者間隔定投

QueueProduce新增定時投遞方法

/**
 * 間隔3秒定時投送
 */
@Scheduled(fixedDelay = 3000)
public void produceMsgScheduled() {
	jmsMessagingTemplate.convertAndSend(queue,"定時投送 => "
		+ UUID.randomUUID().toString().substring(0,6));
}

主啟動類新增一個註解

@SpringBootApplication
@EnableScheduling      //允許開啟定時投送功能
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

直接開啟主啟動類,間隔投遞訊息

6.3 佇列案例 - 消費者監聽
@Component
public class QueueCustomer {
    @JmsListener(destination = "${myqueue}")
    public void receive(TextMessage message) throws JMSException {
        System.out.println("消費者收到訊息 => " + message.getText());
    }
}
6.4 主題基本案例

application.yml配置檔案

server:
  port: 6666

spring:
  activemq:
    broker-url: tcp://mpolaris.top:61616
    user: admin
    password: admin
  jms:
    # 目的地是queue還是topic, false(預設)=queue    true=topic
    pub-sub-domain: true
 
mytopic: boot-activemq-topic

ActiveMQ配置檔案

@Configuration
@EnableJms      //開啟Jms適配的註解
public class ConfigBean {

    @Value("${mytopic}")
    private String myTopic;

    @Bean
    public Topic topic() {
        return new ActiveMQTopic(myTopic);
    }
}

主題生產者

@Component
public class TopicProduce {
    @Autowired
    private JmsMessagingTemplate jmsMessagingTemplate;

    @Autowired
    private Topic topic;

    public void produceMsg() {
        jmsMessagingTemplate.convertAndSend(topic,"===> SpringBoot + ActiveMQ訊息");
    }

    @Scheduled(fixedDelay = 3000)
    public void produceMsgScheduled() {
        jmsMessagingTemplate.convertAndSend(topic,"定時投送 => "
                + UUID.randomUUID().toString().substring(0,6));
        System.out.println("定時投送");
    }
}

主題消費者

@Component
public class QueueCustomer {
    @JmsListener(destination = "${mytopic}")
    public void receive(TextMessage message) throws JMSException {
        System.out.println("消費者收到訊息 => " + message.getText());
    }
}

7. ActiveMQ傳輸協議

7.1 簡介

ActiveMQ支援的client-broker通訊協議有:TCP、NIO、UDP、SSL、Http(s)、VM等。其中配置Transport Connector的檔案在ActiveMQ安裝目錄的conf/activemq.xml中的標籤之內。

image-20210117000034185

activemq傳輸協議的官方文件:http://activemq.apache.org/configuring-version-5-transports.html

image-20210116235502513

除了tcp和nio協議其他的瞭解就行。各種協議有各自擅長該協議的中介軟體,工作中一般不會使用activemq去實現這些協議。如: mqtt是物聯網專用協議,採用的中介軟體一般是mosquito。ws是websocket的協議,是和前端對接常用的,一般在java程式碼中內嵌一個基站(中介軟體)。stomp好像是郵箱使用的協議的,各大郵箱公司都有基站(中介軟體)。

注意:協議不同,我們的程式碼都會不同。

7.2 各協議理解

TCP協議

Transmission Control Protocol(TCP)是預設的,TCP的Client監聽埠61616

在網路傳輸資料前必須要先序列化資料,訊息是通過一個叫wire protocol的來序列化成位元組流。預設情況下ActiveMQ把wrie protocol 叫做 OpenWire,它的目的就是促使網路上的效率更高和資料快速交換。

TCP連線的URI形式如:tcp://HostName:port?key=value&key=value,後面的引數是可選的。

TCP傳輸的的優點:

  • TCP協議傳輸可靠性高,穩定性強

  • 高效率:位元組流方式傳遞,效率很高

  • 有效性、可用性:應用廣泛,支援任何平臺

關於Transport協議的可選配置引數可以參考官網http://activemq.apache.org/tcp-transport-reference

NIO協議

New I/O API Protocol(NIO)。NIO協議和TCP協議類似,但NIO更側重於底層的訪問操作。它允許開發人員對同一資源可有更多的client呼叫和伺服器端有更多的負載。

適合使用NIO協議的場景:

  • 可能有大量的Client去連線到Broker上,一般情況下大量的Client去連線Broker是被作業系統的執行緒所限制的。因此NIO的實現比TCP需要更少的執行緒去執行,所以建議使用NIO協議。

  • 可能對於Broker有一個很遲鈍的網路傳輸,NIO比TCP提供更好的效能。

NIO連線的URI形式:nio://hostname:port?key=value&key=value

關於Transport協議的可選配置引數可以參考官網http://activemq.apache.org/configuring-version-5-transports.html

圖片1

AMQP協議

Advanced Message Queuing Protocol,一個提供統一訊息服務的應用層標準高階訊息佇列協議,是應用層協議的一個開放標準,為面向訊息的中介軟體設計。基於此協議的客戶端與訊息中介軟體可傳遞訊息,並不受客戶端,中介軟體,不同產品,不同開發語言等條件限制。

STOMP協議

STOP,Streaming Text Orientation Message Protocol,是流文字定向訊息協議,是一種為MOM(Message Oriented Middleware,面向訊息中介軟體)設計的簡單文字協議。

MQTT協議

MQTT(Message Queuing Telemetry Transport,訊息佇列遙測傳輸)是IBM開發的一個即時通訊協議,有可能成為物聯網的重要組成部分。該協議支援所有平臺,幾乎可以把所有聯網物品和外部連線起來,被用來當作感測器和致動器(比如通過Twitter讓房屋聯網)的通訊協議。

GitLub檢視MQTT示例程式碼:https://github.com/fusesource/mqtt-client

7.3 NIO協議案例

ActiveMQ這些協議傳輸的底層預設都是使用BIO網路的IO模型。只有當我們指定使用nio才使用NIO的IO模型。

NIO網路IO模型簡單配置

修改配置檔案activemq.xml

如果你 不特別指定ActiveMQ的網路監聽埠,那麼這些埠都將使用BIO網路IO模型,所以為了首先提高單節點的網路吞吐效能,我們需要明確指定ActiveMQ網路IO模型。如下所示:URI格式頭以“nio”開頭,表示這個埠使用以TCP協議為基礎的NIO網路IO模型。

<transportConnectors>   
    <!-- 新增NIO協議 -->
	<transportConnector name="nio" uri="nio://0.0.0.0:61618?trace=true" /></transportConnectors>

SpringBoot修改埠即可

server:
  port: 6666

spring:
  activemq:
    broker-url: nio://mpolaris.top:61618
    user: admin
    password: admin
  jms:
    pub-sub-domain: true
mytopic: boot-activemq-topic

NIO增強

image-20210117230310659 image-20210117232434530

修改activemq.xml配置檔案(其實只要auto+nio一條都行了)

auto: 針對所有的協議,他會識別我們是什麼協議。

nio:使用NIO網路IO模型

<transportConnectors>
	<transportConnector name="openwire" uri="tcp://0.0.0.0:61626?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
	<transportConnector name="amqp" uri="amqp://0.0.0.0:5682?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
	<transportConnector name="stomp" uri="stomp://0.0.0.0:61623?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
	<transportConnector name="mqtt" uri="mqtt://0.0.0.0:1893?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
	<transportConnector name="ws" uri="ws://0.0.0.0:61624?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
	<transportConnector name="nio" uri="nio://0.0.0.0:61618?trace=true" />
  	<transportConnector name="auto+nio" uri="auto+nio://0.0.0.0:61608?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600&amp;org.apache.activemq.transport.nio.SelectorManager.corePoolSize=20&amp;org.apache.activemq.transport.nio.Se1ectorManager.maximumPoo1Size=50"/>
</transportConnectors>

修改埠號為61608即可

server:
  port: 6666

spring:
  activemq:
    # broker-url: tcp://mpolaris.top:61608  適配多種協議(注意有些協議程式碼不一樣)
    broker-url: nio://mpolaris.top:61608
    user: admin
    password: admin
  jms:
    pub-sub-domain: true
mytopic: boot-activemq-topic

8. ActiveMQ的訊息儲存和持久化

8.1 理解

此處持久化和之前永續性的區別

MQ高可用:事務、永續性、簽收,是屬於MQ自身特性,自帶的。這裡的持久化是外力,是外部外掛。之前講的永續性是MQ的外在表現,現在講的的持久是是底層實現。

image-20210117233600172
8.2 持久化是什麼

官網文件:http://activemq.apache.org/persistence

持久化是什麼?一句話就是:ActiveMQ當機了訊息不會丟失的機制。

說明:為了避免意外當機以後丟失資訊,需要做到重啟後可以恢復訊息佇列,訊息系統一般都會採用持久化機制。ActiveMQ的訊息持久化機制有JDBC,AMQ,KahaDB和LevelDB,無論使用哪種持久化方式,訊息的儲存邏輯都是一致的。就是在傳送者將訊息傳送出去後,訊息中心首先將訊息儲存到本地資料檔案、記憶體資料庫或者遠端資料庫等。再試圖將訊息發給接收者,成功則將訊息從儲存中刪除,失敗則繼續嘗試傳送。訊息中心啟動以後,要先檢查指定的儲存位置是否有未成功傳送的訊息,如果有則會先把儲存位置中的訊息發出去。

8.3 MQ持久化機制有哪些

AMQ Message Store

基於檔案的儲存機制,是以前的預設機制,現在不再使用。AMQ是一種檔案儲存形式,它具有寫入速度快和容易恢復的特點。訊息儲存在一個個檔案中,檔案的預設大小為32M,當一個檔案中的訊息已經全部被消費,那麼這個檔案將被標識為可刪除,在下一個清除階段這個檔案會被刪除。AMQ適用於ActiveMQ5.3之前的版本。

KahaDB

基於日誌檔案,從ActiveMQ5.4(含)開始預設的持久化,下面我們詳細介紹。

LevelDB訊息儲存

新興的技術,現在有些不確定。 官方文件:http://activemq.apache.org/leveldb-store。這種檔案系統是從ActiveMQ5.8之後引進的,它和KahaDB非常相似,也是基於檔案的本地資料庫儲存形式,但是它提供比KahaDB更快的永續性。但它不使用自定義B-Tree實現來索引獨寫日誌,而是使用基於LevelDB的索引,預設配置如下:

<persistenceAdapter>  
	<levelDB directory="activemq-data"/>
</persistenceAdapter> 

JDBC訊息儲存

下面我們再詳細介紹

JDBC Message Store with ActiveMQ Journal

下面我們再詳細介紹

8.4 KahaDB訊息儲存

理解

KahaDB是目前預設的儲存方式,可用於任何場景,提高了效能和恢復能力。訊息儲存使用一個 事務日誌 和僅僅用一個 索引檔案 來儲存它所有的地址。KahaDB是一個專門針對訊息持久化的解決方案,它對典型的訊息使用模型進行了優化。資料被追加到data logs中。當不再需要log檔案中的資料的時候,log檔案會被丟棄。

官網文件:http://activemq.aache.org/kahadb,官網上還有一些其他配置引數。

activemq.xml配置檔案

<persistenceAdapter>
    <kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>

KahaDB儲存原理

KahaDB在訊息儲存的目錄中有4類檔案和一個lock,跟ActiveMQ的其他幾種檔案儲存引擎相比,這就非常簡潔了。

image-20210118000120620
  • db-number.log KahaDB儲存訊息到預定大小的資料紀錄檔案中,檔名為db-number.log。當資料檔案已滿時,一個新的檔案會隨之建立,number數值也會隨之遞增,它隨著訊息數量的增多,如每32M一個檔案,檔名按照數字進行編號,如db-1.log,db-2.log······。當不再有引用到資料檔案中的任何訊息時,檔案會被刪除或者歸檔。
image-20210118001312244
  • db.data 該檔案包含了持久化的BTree索引,索引了訊息資料記錄中的訊息,它是訊息的索引檔案,本質上是B-Tree(B樹),使用B-Tree作為索引指向db-number。log裡面儲存訊息。
  • db.free 記錄當前db.data檔案裡面哪些頁面是空閒的,檔案具體內容是所有空閒頁的ID
  • db.redo 用來進行訊息恢復,如果KahaDB訊息儲存再強制退出後啟動,用於恢復BTree索引。
  • lock 檔案鎖,表示當前kahadb獨寫許可權的broker。
8.4 JDBC訊息儲存

原理圖

image-20210118002603363

配置

新增mysql資料庫的驅動包到ActiveMQ的lib資料夾下

image-20210118003932035

在activemq.xml配置檔案指定JDBC訊息儲存

<!--  
<persistenceAdapter>
     <kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>
-->
<persistenceAdapter>  
    <!-- dataSource指定將要引用的持久化資料庫的bean名稱
 		createTablesOnStartup指定是否在啟動的時候建立資料表,預設為true
		注意:一般是第一次啟動時設定為true,之後改為false  -->
    <jdbcPersistenceAdapter dataSource="#mysql-ds" createTablesOnStartup="true"/> 
</persistenceAdapter>

在activemq.xml配置檔案的標籤和標籤之間插入資料庫連線池配置

注意:

① 我們需要準備一個mysql資料庫,並建立一個名為activemq的資料庫

② 預設是的dbcp資料庫連線池,如果要換成其他資料庫連線池,需要將該連線池jar包,也放到lib目錄下。

...
</broker>

<bean id="mysql-ds" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">     
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>     
    <property name="url" value="jdbc:mysql://mpolaris.top:3306/activemq?relaxAutoCommit=true"/>     
    <property name="username" value="root"/>     
    <property name="password" value="123456"/>
    <property name="maxTotal" value="200" />
    <property name="poolPreparedStatements" value="true"/>   
</bean> 


<import resource="jetty.xml"/>
...

重啟activemq會自動生成如下3張表。如果沒有自動生成需要我們手動執行SQL。我個人建議要自動生成,我在操作過程中檢視日誌檔案發現了不少問題,最終解決了這些問題後是能夠自動生成的。如果不能自動生成說明你的操作有問題。表欄位說明如下

  • ACTIVEMQ_MSGS 訊息資料表
image-20210118013013862
  • ACTIVEMQ_ACKS資料表
image-20210118012701231
  • ACTIVEMQ_LOCK資料表:表ACTIVEMQ_LOCK在叢集環境下才有用,只有一個Broker可以獲取訊息,稱為Master Broker,其他的只能作為備份等待Master Broker不可用,才可能成為下一個Master Broker。這個表用於記錄哪個Broker是當前的Master Broker 。

Queue驗證和資料表變化

在點對點型別中,當DeliveryMode設定為NON_PERSISTENCE時,訊息被儲存在記憶體中。當DeliveryMode設定為PERSISTENCE時,訊息儲存在broker的相應的檔案或者資料庫中。而且點對點型別中訊息一旦被Consumer消費,就從資料中刪除,消費前的訊息會被存放到資料庫 上面的訊息被消費後被MQ自動刪除。

  • Queue非持久化模式:不會將訊息持久化到資料庫
  • Queue持久化模式:會將訊息持久化到資料庫,但是訊息被消費者消費後會自動刪除持久化資料。

我們使用queue持久化模式釋出3條訊息後,發現ACTIVEMQ_MSGS資料表多了3條資料。

image-20210118224439794

啟動消費者消費了所有的訊息後,發現資料表的資料消失了。

Topic驗證和說明

設定了持久訂閱資料庫裡面會儲存訂閱者的資訊

ACTIVEMQ_ACKS表中的LAST_ACKED_ID記錄了CLIENT_ID最後簽收的一條訊息,而LAST_ACKED_ID和ACTIVEMQ_MSGS的ID欄位是外來鍵關聯關係,這樣就可以實現Topic的訊息儲存到ACTIVEMQ_MSGS表內的同時還能根據ACTIVEMQ_ACKS表中的持久訂閱者查到該訂閱者上次收到的最後一條訊息是什麼。值得注意的是Topic內的訊息是不會被刪除的,而Queue的訊息在被刪除後會在資料庫中被刪除,如果需要儲存Queue,應該使用其他方案解決。

我們啟動主題持久化,生產者釋出3個資料,ACTIVEMQ_MSGS資料表新增3條資料,消費者消費所有的資料後,ACTIVEMQ_MSGS資料表的資料並沒有消失。持久化topic的訊息不管是否被消費,是否有消費者,產生的資料永遠都存在,且只儲存一條。這個是要注意的,持久化的topic大量資料後可能導致效能下降。這裡就像公總號一樣,消費者消費完後,訊息還會保留。

image-20210118224334233 image-20210118223750666

總結

如果是Queue,在沒有消費者消費的情況下會將訊息儲存到activemq_msgs表中,只要有任意一個消費者消費了,就會刪除消費過的訊息。

如果是Topic,一般是先啟動消費訂閱者然後再生產的情況下會將持久訂閱者永久儲存到qctivemq_acks,而訊息則永久儲存在activemq_msgs,在acks表中的訂閱者有一個last_ack_id對應了activemq_msgs中的id欄位,這樣就知道訂閱者最後收到的訊息是哪一條。

常見坑

在配置關係型資料庫作為ActiveMQ的持久化儲存方案時,有許多坑。

  • 資料庫jar包:注意對應版本的資料庫jar或者你自己使用的非自帶的資料庫連線池jar包

  • createTablesOnStartup屬性:該屬性預設為true,每次啟動activemq都會自動建立表,在第一次啟動後應改為false避免不必要的損失。

  • 下劃線:報錯"java.lang.IllegalStateException: LifecycleProcessor not initialized"。確認計算機主機名名稱沒有下劃線

8.5 JDBC Message Store with ActiveMQ Journal

理解

這種方式克服了JDBC Store的不足,JDBC每次訊息過來都需要去寫庫讀庫。ActiveMQ Journal,使用快取記憶體寫入技術大大提高了效能。當消費者的速度能夠及時跟上生產者訊息的生產速度時,journal檔案能夠大大減少需要寫入到DB中的訊息。

舉個例子:生產者生產了1000條訊息,這1000條訊息會儲存到journal檔案,如果消費者的消費速度很快的情況下,在journal檔案還沒有同步到DB之前,消費者已經消費了90%的以上訊息,那麼這個時候只需要同步剩餘的10%的訊息到DB。如果消費者的速度很慢,這個時候journal檔案可以使訊息以批量方式寫到DB。

為了高效能,這種方式使用日誌檔案儲存+資料庫儲存。先將訊息持久到日誌檔案,等待一段時間再將未消費的訊息持久到資料庫。該方式要比JDBC效能要高

配置(基於JDBC配置稍作修改)

activemq.xml修改

# 修改配置前
<persistenceAdapter>         
     <jdbcPersistenceAdapter dataSource="#mysql-ds" /> 
</persistenceAdapter>

# 修改配置後(註釋掉之前的jdbc配置使用下面的)
<persistenceFactory>                            
    <journalPersistenceAdapterFactory                                                               journalLogFiles="5"
          journalLogFileSize="32768"
          useJournal="true"
          useQuickJournal="true"
          dataSource="#mysql-ds"
          dataDirectory="../activemq-data" /> 
</persistenceFactory> 
8.6 總結
  • Jdbc效率低,KahaDB效率高,Jdbc+Journal效率較高。

  • 持久化訊息主要指的是:MQ所在伺服器當機了訊息不會丟試的機制。

  • 持久化機制演變的過程:從最初的AMQ Message Store方案到ActiveMQ V4版本退出的High Performance Journal(高效能事務支援)附件,並且同步推出了關於關係型資料庫的儲存方案。ActiveMQ5.3版本又推出了對KahaDB的支援(5.4版本後被作為預設的持久化方案),後來ActiveMQ 5.8版本開始支援LevelDB,到現在5.9提供了標準的Zookeeper+LevelDB叢集化方案。

  • ActiveMQ訊息持久化機制有:

持久化機制 特點
AMQ 基於日誌檔案
KahaDB 基於日誌檔案,從ActiveMQ5.4開始預設使用
JDBC 基於第三方資料庫
Replicated LevelDB Store 從5.9開始提供了LevelDB和Zookeeper的資料複製方法,用於Master-slave方式的首選資料複製方案。

9. ActiveMQ多節點叢集

9.1 理解

基於zookeeper和LevelDB搭建ActiveMQ叢集。叢集僅提供主備方式的高可用叢集功能,避免單點故障。

9.2 三種叢集方式
  • 基於shareFileSystem共享檔案系統(KahaDB)

  • 基於JDBC

  • 基於可複製的LevelDB

9.3 ZK + Replicated LevelDB Store 案例

Replicated LevelDB Store

是什麼:http://activemq.apache.org/replicated-leveldb-store

使用Zookeeper叢集註冊所有的ActiveMQ Broker但只有其中一個Broker可以提供服務,它將被視為Master,其他的Broker處於待機狀態被視為Slave。如果Master因故障而不能提供服務,Zookeeper會從Slave中選舉出一個Broker充當Master。Slave連線Master並同步他們的儲存狀態,Slave不接受客戶端連線。所有的儲存操作都將被複制到連線至Maste的Slaves。如果Master當機得到了最新更新的Slave會變成Master。故障節點在恢復後會重新加入到叢集中並連線Master進入Slave模式。所有需要同步的訊息操作都將等待儲存狀態被複制到其他法定節點的操作完成才能完成。所以,如給你配置了replicas=3,name法定大小是(3/2)+1 = 2。Master將會儲存更新然後等待(2-1)=1個Slave儲存和更新完成,才彙報success,至於為什麼是2-1,陽哥的zookeeper講解過自行復習。有一個ode要作為觀察者存在。當一個新的Master被選中,你需要至少保障一個法定mode線上以能夠找到擁有最新狀態的ode,這個ode才可以成為新的Master。因此,推薦執行至少3個replica nodes以防止一個node失敗後服務中斷。

部署規劃和步驟

  • 環境和版本
  • 關閉防火牆並保證各個伺服器能夠ping通
  • 具備zk叢集並可以成功啟動
  • 叢集部署規劃列表
  • 建立3臺叢集目錄(就是一臺電腦複製三份ActiveMQ)
  • 修改管理控制檯埠(就是ActiveMQ後臺管理頁面的訪問埠)
  • hostname名字對映(如果不對映只需要吧mq配置檔案的hostname改成當前主機ip)
  • ActiveMQ叢集配置
    • 配置檔案裡面的BrokerName要全部一致
    • 持久化配置(必須)
  • 修改各個節點的訊息埠(真實的三臺機器不用管)
  • 按順序啟動3個ActiveMQ節點,到這步前提是zk叢集已經成功啟動執行(先啟動Zk 在啟動ActiveMQ)
  • zk叢集節點狀態說明
    • 3臺Zk連線任意一臺驗證三臺ActiveMQ是否註冊上了Zookeeper
    • 檢視Master

叢集可用性測試

10 ActiveMQ高階特性

10.1 引入訊息中介軟體後如何保證其高可用

zookeeper+Replicated LevelDB

10.2 非同步投遞Async Sends

非同步投遞

http://activemq.apache.org/async-sends

對於一個Slow Consumer,使用同步傳送訊息可能出現Producer堵塞的情況,慢消費者適合使用非同步傳送。

是什麼

ActiveMQ支援同步,非同步兩種傳送的模式將訊息傳送到broker,模式的選擇對傳送延時有巨大的影響。producer能達到怎麼樣的產出率(產出率=傳送資料總量/時間)主要受傳送延時的影響,使用非同步傳送可以顯著提高傳送的效能。

ActiveMQ預設使用非同步傳送的模式:除非明確指定使用同步傳送的方式或者在未使用事務的前提下傳送持久化的訊息,這兩種情況都是同步傳送的。

如果你 沒有使用事務且傳送的是持久化的訊息,每一次傳送都是同步傳送的且會阻塞producer知道broker返回一個確認,表示訊息已經被安全的持久化到磁碟。確認機制提供了訊息安全的保障,但同時會阻塞客戶端帶來了很大的延時。很多高效能的應用,允許在失敗的情況下有少量的資料丟失。如果你的應用滿足這個特點,你可以使用非同步傳送來提高生產率,即使傳送的是持久化的訊息。

非同步傳送:它可以最大化producer端的傳送效率。我們通常在傳送訊息量比較密集的情況下使用非同步傳送,它可以很大的提升Producer效能,不過這也帶來了額外的問題:就是需要消耗更多的Client端記憶體同時也會導致broker端效能消耗增加;此外它不能有效的確保訊息的傳送成功。在userAsyncSend=true的情況下客戶端需要容忍訊息丟失的可能。

自我理解:此處的非同步是指生產者和broker之間傳送訊息的非同步。不是指生產者和消費者之間非同步。

說明:對於一個Slow Consumer,使用同步傳送訊息可能出成Producer堵塞等情況,慢消費者適合使用非同步傳送。(這句話我認為有誤)

總結:① 非同步傳送可以讓生產者發的更快。② 如果非同步投遞不需要保證訊息是否傳送成功,傳送者的效率會有所提高。如果非同步投遞還需要保證訊息是否成功傳送,並採用了回撥的方式,傳送者的效率提高不多,這種就有些雞肋。

參考官網程式碼實現

非同步訊息如何確定傳送成功?

非同步傳送丟失訊息的場景是:生產者設定userAsyncSend=true,使用producer.send(msg)持續傳送訊息。如果訊息不阻塞,生產者會認為所有send的訊息均被成功傳送至MQ

如果MQ突然當機,此時生產者端記憶體中尚未被髮送至MQ的訊息都會丟失

所以正確的非同步傳送方法是需要接收回撥的。

同步傳送和非同步傳送的區別就在此,同步傳送等send不阻塞了就表示一定傳送成功了,非同步傳送需要客戶端回執並由客戶端再判斷一次是否傳送成功。

10.3 延遲投遞和定時投遞

官網說明:http://activemq.apache.org/delay-and-schedule-message-delivery.html

四大屬性

image-20210118234204346

案例

要在activemq.xml中配置schedulerSupport屬性為true

Java程式碼裡面封裝的輔助訊息型別:ScheduledMessage

10.4 分發策略
10.5 ActiveMQ訊息重試機制

官網說明:http://activemq.apache.org/redelivery-policy

是什麼

消費者收到訊息,之後出現異常了,沒有告訴broker確認收到該訊息,broker會嘗試再將該訊息傳送給消費者。嘗試n次,如果消費者還是沒有確認收到該訊息,那麼該訊息將被放到死信佇列重,之後broker不會再將該訊息傳送給消費者。

具體哪些情況會引發訊息重發

  • Client用了transactions且再session中呼叫了rollback

  • Client用了transactions且再呼叫commit之前關閉或者沒有commit

  • Client再CLIENT_ACKNOWLEDGE的傳遞模式下,session中呼叫了recover

請說說訊息重發時間間隔和重發次數

  • 間隔:1

  • 次數:6

  • 每秒發6次

有毒訊息Poison ACK

一個訊息被redelivedred超過預設的最大重發次數(預設6次)時,消費的回個MQ發一個“poison ack”表示這個訊息有毒,告訴broker不要再發了。這個時候broker會把這個訊息放到DLQ(私信佇列)。

屬性說明

image-20210118234607271
10.6 死信佇列

官方文件:http://activemq.apache.org/redelivery-policy

是什麼

異常訊息規避處理的集合,主要處理失敗的訊息。

image-20210118234737121

使用:處理失敗的訊息

  • 一般生產環境中在使用MQ時設計兩個佇列:一個核心業務佇列,一個死信佇列
  • 核心業務佇列:比如下圖專門用來讓訂單系統傳送訂單訊息的,然後另一個死信佇列就是用來處理異常情況的。
  • 假如第三方物流系統故障了,此時無法請求,那麼倉儲系統每次消費到一條訂單訊息,嘗試通知發貨和配送都會遇到對方的介面報錯。此時倉儲系統就可以把這條訊息拒絕訪問或者標誌位處理失敗。一旦標誌這條訊息處理失敗了之後,MQ就會把這條訊息轉入提前設定好的一個死信佇列中。
  • 然後你會看到的就是,在第三方物流系統故障期間,所有訂單訊息全部處理失敗,全部會轉入死信佇列。然後你的倉儲系統得專門有一個後臺執行緒,監控第三方物流系統是否正常,是否請求,不停的監視。一旦發現對方恢復正常,這個後臺執行緒就從死信佇列消費出來處理失敗的訂單,重新執行發貨和配送的通知邏輯。
image-20210118234828832

死信佇列的配置(一般採用預設)

  • sharedDeadLetterStrategy
    • 不管是queue還是topic,失敗的訊息都放到這個佇列中。下面修改activemq.xml的配置,可以達到修改佇列的名字。
    • 將所有的DeadLetter儲存在一個共享的佇列中,這是ActiveMQ broker端預設的策略。共享佇列預設為“ActiveMQ.QLQ”,可以通過"deaLetterQueue"屬性來設定
<deadLetterStrategy>
    <sharedDeadLetterStrategy deaLetterQueue="DLQ-QUEUE"/>
</deadLetterStrategy>
  • individualDeadLetterStrategy

    可以為queue和topic單獨指定兩個死信佇列。還可以為某個話題,單獨指定一個死信佇列。

image-20210118235956137

屬性"useQueueForTopicMessages",此值表示是否將Topic的DeaLetter儲存在Queue中,預設為true

image-20210119000123044
  • 自動刪除過期訊息

過期訊息是值生產者指定的過期時間,超過這個時間的訊息

image-20210119000210743
  • 存放非持久訊息到死信佇列中
image-20210119000255042
10.7 訊息不被重複消費,冪等性問題

之後回來完善

activemq的API文件:http://activemq.apache.org/maven/apidocs/index.html