一、簡介
1.1 什么是 JMS
JMS 即 Java 消息服務(Java Message Service)應用程序接口,是一個Java平臺中關于面向消息中間件(MOM)的API,用于在兩個應用程序之間,或分布式系統中發送消息,進行異步通信。Java 消息服務是一個與具體平臺無關的 API,絕大多數MOM提供商都對JMS提供支持。
1.2 什么是 ActiveMQ
ActiveMQ 是由 Apache 出品的一款開源消息中間件,旨在為應用程序提供高效、可擴展、穩定、安全的企業級消息通信。
它的設計目標是提供標準的、面向消息的、多語言的應用集成消息通信中間件。ActiveMQ 實現了 JMS 1.1 并提供了很多附加的特性,比如 JMX 管理、主從管理、消息組通信、消息優先級、延遲接收消息、虛擬接收者、消息持久化、消息隊列監控等
1.3 ActiveMQ 特點
- 支持包括 Java、C、C++、C#、Ruby、Perl、Python、PHP 等多種語言的客戶端和協議。協議包含 OpenWire、Stomp、AMQP、MQTT 。
- 提供了像消息組通信、消息優先級、延遲接收消息、虛擬接收者、消息持久化之類的高級特性
- 完全支持 JMS 1.1 和 J2EE 1.4規范(包括持久化、分布式事務消息、事務)
- 對 Spring 框架的支持,ActiveMQ 可以通過 Spring 的配置文件方式很容易嵌入到 Spring 應用中
- 通過了常見的 J2EE 服務器測試,比如 TomEE、Geronimo、JBoss、GlassFish、WebLogic
- 連接方式的多樣化,ActiveMQ 提供了多種連接模式,例如 in-VM、TCP、SSL、NIO、UDP、多播、JGroups、JXTA
- 支持通過使用 JDBC 和 journal 實現消息的快速持久化
- 為高性能集群、客戶端-服務器、點對點通信等場景而設計
- 提供了技術和語言中立的 REST API 接口
- 支持 Ajax 方式調用 ActiveMQ
- ActiveMQ 可以輕松地與 CXF、Axis 等 Web Service 技術整合,以提供可靠的消息傳遞
- 可用作為內存中的 JMS 提供者,非常適合 JMS 單元測試
1.4 基本組件
ActiveMQ 使用時包含的基本組件如下(與 JMS 相同):
- Broker:消息代理,表示消息隊列服務器實體,接受客戶端連接,提供消息通信的核心服務;
- Producer:消息生產者,業務的發起方,負責生產消息并傳輸給 Broker;
- Consumer:消息消費者,業務的處理方,負責從 Broker 獲取消息并進行業務邏輯處理;
- Topic:主題,發布訂閱模式下的消息統一匯集地,不同生產者向 Topic 發送消息,由 Broker 分發到不同的訂閱者,實現消息的廣播;
- Queue:隊列,點對點模式下特定生產者向特定隊列發送消息,消費者訂閱特定隊列接收消息并進行業務邏輯處理;
- Message:消息體,根據不同通信協議定義的固定格式進行編碼的數據包,來封裝業務 數據,實現消息的傳輸。
1.5 消息傳送模型
ActiveMQ 支持兩種截然不同的消息傳送模型:PTP(即點對點模型)和Pub/Sub(即發布 /訂閱模型),分別稱作:PTP Domain 和Pub/Sub Domain。
PTP(即點對點模型)
PTP 消息域使用 queue(隊列) 作為 Destination(消息被尋址、發送以及接收的對象),消息可以被同步或異步的發送和接收,每個消息只會給一個 Consumer 傳送一次。Consumer 可以使用 MessageConsumer.receive()
同步地接收消息,也可以通過使用MessageConsumer.setMessageListener()
注冊一個 MessageListener 實現異步接收。多個 Consumer 可以注冊到同一個 queue 上,但一個消息只能被一個 Consumer 所接收,然后由該 Consumer 來確認消息。并且在這種情況下,Provider 對所有注冊的 Consumer 以輪詢的方式發送消息。
Pub/Sub 發布訂閱模型
Pub/Sub(發布/訂閱,Publish/Subscribe)消息域使用 topic (主題) 作為 Destination(消息被尋址、發送以及接收的對象),發布者向 topic 發送消息,訂閱者注冊接收來自 topic 的消息。發送到 topic 的任何消息都將自動傳遞給所有訂閱者。接收方式(同步和異步)與 P2P 域相同。除非顯式指定,否則 topic 不會為訂閱者保留消息。當然,這可以通過持久化(Durable)訂閱來實現消息的保存。這種情況下,當訂閱者與 Provider 斷開時,Provider 會為它存儲消息。當持久化訂閱者重新連接時,將會受到所有的斷連期間未消費的消息。
1.6 消息存儲
JMS 規范中消息的分發方式有兩種:非持久化和持久化。對于非持久化消息 JMS 實現者須保證盡最大努力分發消息,但消息不會持久化存儲;而持久化方式分發的消息則必須進行持久化存儲。非持久化消息常用于發送通知或實時數據,當你比較看重系統性能并且即使丟失一些消息并不影響業務正常運作時可選擇非持久化消息。持久化消息被發送到消息服務器后如果當前消息的消費者并沒有運行則該消息繼續存在,只有等到消息被處理并被消息消費者確認之后,消息才會從消息服務器中刪除。具體的消息存儲方式如下:
- AMQ,是 ActiveMQ 5.0及以前版本默認的消息存儲方式,它是一個基于文件的、支持事務的消息存儲解決方案。在此方案下消息本身以日志的形式實現持久化,存放在 Data Log 里。并且還對日志里的消息做了引用索引,方便快速取回消息
<broker brokerName="broker" persistent="true" useShutdownHook="false"> <persistenceAdapter> <amqPersistenceAdapter directory="${activemq.data}/amq" syncOnWrite="true" indexPageSize="16kb" indexMaxBinSize="100" maxFileLength="10mb" /> </persistenceAdapter> </broker>
- KahaDB,也是一種基于文件并具有支持事務的消息存儲方式,從5.3開始推薦使用 KahaDB 存儲消息,它提供了比 AMQ 消息存儲更好的可擴展性和可恢復性
<broker brokerName="broker" persistent="true" useShutdownHook="false"> <persistenceAdapter> <kahaDB directory="${activemq.data}/kahadb" journalMaxFileLength="16mb"/> </persistenceAdapter> </broker>
- JDBC,基于 JDBC 方式將消息存儲在數據庫中,將消息存到數據庫相對來說比較慢,所以 ActiveMQ 建議結合 journal 來存儲,它使用了快速的緩存寫入技術,大大提高了性能;
<beans> <broker brokerName="broker" persistent="true" xmlns="http://activemq.apache.org/schema/core"> <persistenceAdapter> <jdbcPersistenceAdapter dataSource="#mysql-ds"/> </persistenceAdapter> </broker> <bean id="mysql-ds" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost/activemq?relaxAutoCommit=true"/> <property name="username" value="activemq"/> <property name="password" value="activemq"/> <property name="maxActive" value="200"/> <property name="poolPreparedStatements" value="true"/> </bean> </beans>
- 內存存儲,是指將所有要持久化的消息放到內存中,因為這里沒有動態的緩存,所以需要注意設置消息服務器的 JVM 和內存大小
<broker brokerName="broker" persistent="false" xmlns="http://activemq.apache.org/schema/core"> <transportConnectors> <transportConnector uri="tcp://localhost:61635"/> </transportConnectors> </broker>
- LevelDB,5.6版本之后推出了 LevelDB 的持久化引擎,它使用了自定義的索引代替常用的 BTree 索引,其持久化性能高于 KahaDB,雖然默認的持久化方式還是 KahaDB,但是 LevelDB 將是趨勢。在5.9版本還提供了基于 LevelDB 和 Zookeeper 的數據復制方式,作為 Master-Slave 方式的首選數據復制方案
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="broker" dataDirectory="${activemq.data}"> <persistenceAdapter> <levelDB directory="${activemq.data}/levelDB"/> </persistenceAdapter> </broker>
1.7 連接器
ActiveMQ Broker (消息代理) 的主要作用是為客戶端應用提供一種通信機制,為此 ActiveMQ 提供了一種連接機制,并用連接器(connector)來描述這種連接機制。ActiveMQ 中的連接器有兩種,一種是用于客戶端與消息代理服務器(client-to-broker)之間通信的傳輸連接器(transport connector),一種是用于消息代理服務器之間(broker-to-broker)通信的網絡連接器(network connector)。connector 使用 URI(統一資源定位符)來表示,URI 格式為: <schema name>:<hierarchical part>[?<query>][#<fragment>]
, 例如:foo://username:password@example.com:8042/over/there/index.dtb?type=animal&name=narwhal#nose
- schema name: fool
- hierarchical part: username:password@example.com:8042/over/there/index.dtb
- query: type=animal&name=narwhal
- fragment: nose
1.7.1 傳輸連接器(transport connector)
為了交換消息,消息生產者和消息消費者(統稱為客戶端)都需要連接到消息代理服務器,這種客戶端和消息代理服務器之間的通信就是通過傳輸連接器(Transport connectors)完成的。很多情況下用戶連接消息代理時的需求側重點不同,有的更關注性能,有的更注重安全性,因此 ActiveMQ 提供了一系列l連接協議供選擇,來覆蓋這些使用場景。從消息代理的角度看,傳輸連接器就是用來處理和監聽客戶端連接的,查看 ActiveMQ demo 的配置文件(/examples/conf/activemq-demo.xml),傳輸連接的相關配置如下:
<transportConnectors>
<transportConnector name="openwire" uri="tcp://localhost:61616" discoveryUri="multicast://default"/>
<transportConnector name="ssl" uri="ssl://localhost:61617"/>
<transportConnector name="stomp" uri="stomp://localhost:61613"/>
<transportConnector name="ws" uri="ws://localhost:61614/" />
</transportConnectors>
傳輸連接器定義在<transportConnectors>
元素中,一個<transportConnector>
元素定義一個特定的連接器,一個連接器必須有自己唯一的名字和URI
屬性,但discoveryUri
屬性是可選的。目前在 ActiveMQ 最新的5.15版本中常用的傳輸連接器連接協議有:vm、tcp、udp、multicast、nio、ssl、http、https、websocket、amqp、mqtt、stomp 等等
- VM:允許客戶端和消息服務器直接在 VM 內部通信,采用的連接不是 Socket 連接,而是直接的虛擬機本地方法調用,從而避免網絡傳輸的開銷。應用場景僅限于服務器和客戶端在同一 JVM 中。如使用代碼啟動嵌入式的ActiveMQ Broker實例,通常用于單元測試。因為是嵌入式,所以不需要配置ActiveMQ的配置文件,只要在連接Broker的URI中直接使用即可
<bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory" depends-on="broker"> <property name="brokerURL" value="vm://localhost"/> </bean>
- TCP:ActiveMQ默認的傳輸連接,也是最常用的使用方式。長連接,每個客戶端實例都會與服務器維持一個連接。每個連接一個線程。TCP的優點是:
- 性能高:ActiveMQ使用默認協議OpenWire序列化和反序列化消息。OpenWire是一個性能很高的序列化協議
- 可用性高:TCP是使用最廣泛的技術,幾乎所有的開發語言都支持TCP協議
- 可靠性高:TCP協議確保消息不會在網絡傳說的過程中丟失
<transportConnector name="tcp" uri="tcp://localhost:61616"/>
- UDP:與面向連接,可靠的字節流服務的TCP不同,UDP是一個面向數據的簡單傳輸連接,沒有TCP的三次握手,所以性能大大強于TCP,但是是以犧牲可靠性為前提。適用于丟失也無所謂的消息,如統計uv,pv。(當然如果真是統計uv什么的,有Kafka這樣專門的消息中間件)
<transportConnector name="udp" uri="udp://localhost:8123"/>
- NIO:使用Java的NIO方式對連接進行改進,因為NIO使用線程池,可以復用線程,所以可以用更少的線程維持更多的連接。如果有大量的客戶端,或者性能瓶頸在網絡傳輸上,可以考慮使用NIO的連接方式。也可以根據不同的場景選擇不用的傳輸連接,比如:Producer有很多,但是Consumer很少,可以Producer用NIO協議,Consumer用TCP協議。從ActiveMQ 5.6版本開始,NIO可以支持和SSL搭配使用的傳輸連接。
- NIO配置:
<transportConnector name="nio" uri="nio://localhost:61616"/>
- NIO+SSL配置:
<transportConnector name="nio+ssl" uri="nio+ssl://localhost:61616"/>
- SSL:需要一個安全連接的時候可以考慮使用SSL,適用于client和broker在公網的情況,如使用aws云平臺等
<transportConnector name="ssl" uri="ssl://localhost:8123"/>
- HTTP(S):需要穿越防火墻,可以考慮使用HTTP(S),但由于HTTP(S)是短連接,每次創建連接的成本較高,所以性能最差。允許客戶端使用 REST 或 Ajax 的方式進行連接,這意味著可以直接使用 Javascript 向 ActiveMQ 發送消息
- HTTP配置
<transportConnector name="http" uri="http://localhost:8080"/>
- HTTPS配置
<transportConnector name="https" uri="http://localhost:8080"/>
- AMQP:ActiveMQ 5.8新增加的傳輸連接。用于支持AMQP(高級消息隊列協議)。因為AMQP是消息隊列的標準協議,而且已經越來越被廣泛使用,所以ActiveMQ也支持了此協議。AMQP協議可以搭配NIO或SSL協議使用,amqp+nio用于提升系統的延展性和性能。amqp+ssl可以創建安全連接。
- amqp配置:
<transportConnector name="amqp" uri="amqp://localhost:5672"/>
- amqp+nio配置:
<transportConnector name="amqp+nio" uri="amqp://localhost:5672"/>
- amqp+ssl配置:
<transportConnector name="amqp+ssl" uri="amqp://localhost:5672"/>
- MQTT:ActiveMQ 5.8新增加的傳輸連接。是一個輕量級的消息訂閱/發布協議。和AMQP一樣,同樣支持搭配NIO或SSL使用
<transportConnector name="mqtt" uri="mqtt://localhost:1883"/>
1.7.2 網絡連接器(network connector)
很多情況下,我們要處理的數據可能是海量的,這種場景單臺服務器很難支撐,這就要用到集群功能,為此 ActiveMQ 提供了網絡連接的模式,簡單說就是通過把多個消息服務器實例連接在一起作為一個整體對外提供服務,從而提高整體對外的消息服務能力。通過這種方式連接在一起的服務器實例之間可共享隊列和消費者列表,從而達到分布式隊列的目的,網絡連接器就是用來配置服務器之間的通信。
如圖所示,服務器S1 和 S2 通過 NewworkConnector 相連,生產者 P1 發送的消息,消費者 C3 和 C4 都可以接收到,而生產者 P3 發送的消息,消費者 C1 和 C2 也可以接收到。要使用網絡連接器的功能需要在服務器 S1 的 activemq.xml 中的 broker 節點下添加如下配置(假設192.168.11.23:61617 為 S2 的地址):
<networkConnectors>
<networkConnector uri="static:(tcp://192.168.11.23:61617)"/>
</networkConnectors>
如果只是這樣,S1 可以將消息發送到 S2,但這只是單方向的通信,發送到 S2 上的的消息還不能發送到 S1 上。如果想 S1 也收到從 S2 發來的消息需要在 S2 的 activemq.xml 中的 broker 節點下也添加如下配置(假設192.168.11.45:61617為 S1 的地址):
<networkConnectors>
<networkConnector uri="static:(tcp://192.168.11.45:61617)"/>
</networkConnectors>
這樣,S1和S2就可以雙向通信了。目前在 ActiveMQ 最新的5.15版本中常用的網絡連接器協議有 static
和 multicast
兩種:
- static,靜態協議,用于為一個網絡中多個代理創建靜態配置,這種配置協議支持復合的 URI (即包含其他 URI 的 URI)。例如:
static://(tcp://ip:61616,tcp://ip2:61616)
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="brokerA" dataDirectory="${activemq.base}/data"> <networkConnectors> <networkConnector uri="static:(tcp://localhost:61617)" /> </networkConnectors> <transportConnectors> <transportConnector name="openwire" uri="tcp://localhost:61616" /> </transportConnectors> </broker>
- multicast,多點傳送協議,消息服務器會廣播自己的服務,也會定位其他代理。這種方式用于服務器之間實現動態識別,而不是配置靜態的 IP 組。默認配置:
multicast://default
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="multicast" dataDirectory="${activemq.base}/data"> <networkConnectors> <networkConnector name="default-nc" uri="multicast://default"/> </networkConnectors> <transportConnectors> <transportConnector name="openwire" uri="tcp://localhost:61616" discoveryUri="multicast://default"/> </transportConnectors> </broker>
2.1 添加依賴
<?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 http://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.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>priv.simon.boot</groupId>
<artifactId>activemq</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>activemq</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.2 點對點配置
spring.activemq.broker-url=tcp://localhost:61616
spring.activemq.user=admin
spring.activemq.password=admin
2.3 發布/訂閱配置
Spring Boot 默認開啟點對點模式,發布訂閱模式需要手動開啟
spring.activemq.broker-url=tcp://localhost:61616
spring.activemq.user=admin
spring.activemq.password=admin
spring.jms.pub-sub-domain=true
2.4 消息提供者
@Service
public class ProviderService {
@Autowired
JmsMessagingTemplate jmsMessagingTemplate;
/**
* 點對點消息發送
*/
public void sendPTPMessage(String message){
ActiveMQQueue queue = new ActiveMQQueue("queue");
jmsMessagingTemplate.convertAndSend(queue,message);
}
/**
* 發布訂閱消息發送
*/
public void sendPubSubMesage(String message){
ActiveMQTopic topic = new ActiveMQTopic("topic");
jmsMessagingTemplate.convertAndSend(topic,message);
}
}
2.5 消息消費者
@Service
public class ConsumerService {
/**
* 監聽點對點消息
*/
@JmsListener(destination = "queue")
public void receiveQueue(String message){
System.err.println("queue收到的消息:"+message);
}
/**
* 監聽發布訂閱消息
*/
@JmsListener(destination = "topic")
public void receiveTopic(String message){
System.err.println("topic收到的消息:"+message);
}
}
2.6 測試類
@RunWith(SpringRunner.class)
@SpringBootTest
public class ActivemqApplicationTests {
@Autowired
private ProviderService providerService;
@Test
public void ptpTest() {
providerService.sendPTPMessage("ptp hello");
}
@Test
public void pubSubTest() {
providerService.sendPubSubMesage("pub/sub hello");
}
}