首先咱們先來認識什麼是消息隊列 MQ 呢?
消息隊列與 RocketMQ
消息隊列 MQ
消息隊列(Message Queue)簡稱 MQ,是一種跨進程的通信機制,通常用於應用程序間進行數據的異步傳輸,MQ 產品在架構中通常也被叫作“消息中間件”。它的最主要職責就是保證服務間進行可靠的數據傳輸,同時實現服務間的解耦。
這麼說太過學術,我們看一個項目的實際案例,假設市級稅務系統向省級稅務系統上報本年度稅務彙總數據,按以往的設計市級稅務系統作爲數據的生產者需要了解省級稅務系統的 IP、端口、接口等諸多細節,然後通過 RPC、RESTful 等方式同步向省級稅務系統發送數據,省級稅務系統作爲數據的消費者接受後響應“數據已接收”。
雖然從邏輯上是沒有問題的,但是從技術層面卻衍生出三個新問題:
- 假如上報時省級稅務系統正在升級維護,市級稅務系統就必須設計額外的重發機制保證數據的完整性;
- 假如省級稅務系統接收數據需要 1 分鐘處理時間,市級稅務系統採用同步通信,則市級稅務系統傳輸線程就要阻塞 1 分鐘,在高併發場景下如此長時間的阻塞很容易造成系統的崩潰;
- 假如省級稅務系統接口的調用方式、接口、IP、端口有任何改變,都必須立即通知市級稅務系統進行調整,否則就會出現通信失敗。
從以上三個問題可以看出,省級系統產生的變化直接影響到市級稅務系統的執行,兩者產生了強耦合,如果問題放在互聯網的微服務架構中,幾十個服務進行串聯調用,每個服務間如果都產生類似的強耦合,系統必然難以維護。
可以看到,引入消息隊列後,生產者與消費者都只面向消息隊列進行數據處理,數據生產者根本不需要了解具體消費者的信息,只要把數據按事先約定放在指定的隊列中即可。而消費者也是一樣的,消費者端監聽消息隊列,如果隊列中產生新的數據,MQ 就會通過“推送” PUSH”或者“抽籤” PULL”的方式讓消費者獲取到新數據進行後續處理。
通過示意圖可以看到,只要消息隊列產品是穩定可靠的,那消息通信的過程就是有保障的。在架構領域,很多廠商都開發了自己的 MQ 產品,最具代表性的開源產品有:
- Kafka
- ActiveMQ
- ZeroMQ
- RabbitMQ
- RocketMQ
每一種產品都有自己不同的設計與實現原理,但根本的目標都是相同的:爲進程間通信提供可靠的異步傳輸機制。RocketMQ 作爲阿里系產品天然被整合進 Spring Cloud Alibaba 生態,在經歷過多次雙 11 的考驗後,RocketMQ 在性能、可靠性、易用性方面都是非常優秀的,下面咱們來了解下 RocketMQ 吧。
RocketMQ
RocketMQ 是一款分佈式消息隊列中間件,RocketMQ 最初設計是爲了滿足阿里巴巴自身業務對異步消息傳遞的需要,在 3.X 版本後正式開源並捐獻給 Apache,目前已孵化成爲 Apache 頂級項目,同時也是國內使用最廣泛、使用人數最多的 MQ 產品之一。
RocketMQ 有很多優秀的特性,在可用性方面,RocketMQ 強調集羣無單點,任意一點高可用,客戶端具備負載均衡能力,可以輕鬆實現水平擴容;在性能方面,在天貓雙 11 大促背後的億級消息處理就是通過 RocketMQ 提供的保障;在 API 方面,提供了豐富的功能,可以實現異步消息、同步消息、順序消息、事務消息等豐富的功能,能滿足大多數應用場景;在可靠性方面,提供了消息持久化、失敗重試機制、消息查詢追溯的功能,進一步爲可靠性提供保障。
瞭解 RocketMQ 的諸多特性後,咱們來理解 RocketMQ 幾個重要的概念:
- 消息 Message:消息在廣義上就是進程間傳遞的業務數據,在狹義上不同的 MQ 產品對消息又附加了額外屬性如:Topic(主題)、Tags(標籤)等;
- 消息生產者 Producer:指代負責生產數據的角色,在前面案例中市級稅務系統就充當了消息生產者的角色;
- 消息消費者 Consumer:指代使用數據的角色,前面案例的省級稅務系統就是消息消費者;
- MQ消息服務 Broker:MQ 消息服務器的統稱,用於消息存儲與消息轉發;
- 生產者組 Producer Group:對於發送同一類消息的生產者,RocketMQ 對其分組,成爲生產者組;
- 消費者組 Consumer Group:對於消費同一類消息的消費者,RocketMQ 對其分組,成爲消費者組。
在理解這些基本概念後,咱們正式進入 RocketMQ 的部署與使用環節,通過案例代碼理解 RocketMQ 的執行過程。對於 RocketMQ 來說,使用它需要兩個階段:搭建 RocketMQ 服務器集羣與應用接入 RocketMQ 隊列,首先咱們來部署 RocketMQ 集羣。
部署 RocketMQ 集羣
RocketMQ 天然採用集羣模式,常見的 RocketMQ 集羣有三種形式:多 Master 模式、多 Master 多 Slave- 異步複製模式、多 Master 多 Slave- 同步雙寫模式,這三種模式各自的優缺點如下。
- 多 Master 模式是配置最簡單的模式,同時也是使用最多的形式。優點是單個 Master 宕機或重啓維護對應用無影響,在磁盤配置爲 RAID10 時,即使機器宕機不可恢復情況下,由於 RAID10 磁盤非常可靠,同步刷盤消息也不會丟失,性能也是最高的;缺點是單臺機器宕機期間,這臺機器尚未被消費的消息在機器恢復之前不可訂閱,消息實時性會受到影響。
- 多 Master 多 Slave 異步複製模式。每個 Master 配置一個 Slave,有多對 Master-Slave,HA 採用異步複製方式,主備有短暫消息毫秒級延遲,即使磁盤損壞只會丟失少量消息,且消息實時性不會受影響。同時 Master 宕機後,消費者仍然可以從 Slave 消費,而且此過程對應用透明,不需要人工干預,性能同多 Master 模式幾乎一樣;缺點是 Master 宕機,磁盤損壞情況下會丟失少量消息。
- 多 Master 多 Slave 同步雙寫模式,HA 採用同步雙寫方式,即只有主備都寫成功,才嚮應用返回成功,該模式數據與服務都無單點故障,Master 宕機情況下,消息無延遲,服務可用性與數據可用性都非常高;缺點是性能比異步複製模式低 10% 左右,發送單個消息的執行時間會略高,且目前版本在主節點宕機後,備機不能自動切換爲主機。
這裏搭建一個空間 Master 服務器集羣,首先來看一下部署架構圖:
在雙 Master 架構中,出現了一個新角色 NameServer(命名服務器),NameServer 是 RocketMQ 自帶的輕量級路由註冊中心,支持 Broker 發動動態註冊與發現。在 Broker 啓動後會自動向 NameServer 發送心跳報告,通知 Broker 上線。當 Provider 向 NameServer 獲取路由信息,然後向指定地點 Broker 建立長連接完成數據發送。
爲了避免單節點瓶頸,通常 NameServer 會部署兩臺以上作爲高可用冗餘。NameServer 本身是無狀態的,各實例間不進行通信,因此在 Broker 集羣配置時要配置所有 NameServer 節點以保證狀態同步。
部署 RocketMQ 集羣要分兩步:部署 NameServer 與部署 Broker 集羣。
第一步,部署 NameServer 集羣。
我們創建兩臺 CentOS7 虛擬機,IP 地址分別爲 192.168.31.200 與 192.168.31.201,要求這兩臺虛擬機內存大於 2G,並安裝好 64 位 JDK1.8,具體過程不再演示。
之後訪問 Apache RocketMQ 下載頁:
https://www.apache.org/dyn/closer.cgi?path=rocketmq/4.8.0/rocketmq-all-4.8.0-bin-release.zip
獲取 RocketMQ 最新版
rocketmq-all-4.8.0-bin-release.zip,解壓後編輯 rocketmq-all-4.8.0-bin-release/bin/runserver.sh 文件,因爲 RocketMQ 是服務器軟件,默認爲其配置 8G 內存,這是 PC 機及或者筆記本喫不消的,所以在 82 行附近將 JVM 內存縮小到 1GB 以方便演示。
修改前:
JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g -Xmn4g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
修改後:
cd /usr/local/rocketmq-all-4.8.0-bin-release/bin/
sh mqnamesrv
mqnamesrv 是 RocketMQ 自帶 NameServer 的啓動命令,執行後看到 The Name Server boot success. serializeType=JSON 就代表 NameServer 啓動成功,NameServer 將佔用 9876 端口提供服務,不要忘記在防火牆設置放行。之後如法炮製在另一臺 201 設備上部署 NameServer,構成 NameServer 集羣。
第二步,部署 Broker 集羣。
我們再額外創建兩臺 CentOS7 虛擬機,IP 地址分別爲 192.168.31.210 與 192.168.31.211,同樣要求這兩臺虛擬機內存大於 2G,並安裝好 64 位 JDK1.8。
打開
rocketmq-all-4.8.0-bin-release 目錄,編輯 /bin/runbroker.sh 文件,同樣將啓動 Broker 默認佔用內存從 8G 縮小到 1G,將 64 行調整爲以下內容:
JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn512m"
在 conf 目錄下,RocketMQ 已經給我們貼心的準備好三組集羣配置模板:
- 2m-2s-async 代表雙主雙從異步複製模式;
- 2m-2s-sync 代表雙主雙從同步雙寫模式;
- 2m-noslave 代表雙主模式。
我們在 2m-noslave 雙主模式目錄中,在 broker-a.properties 與 broker-b.properties 末尾追加 NameServer 集羣的地址,爲了方便理解我也將模板裏面每一項的含義進行註釋,首先是 broker-a.properties 的完整內容如下:
#集羣名稱,同一個集羣下的 broker 要求統一
brokerClusterName=DefaultCluster
#broker 名稱
brokerName=broker-a
#brokerId=0 代表主節點,大於零代表從節點
brokerId=0
#刪除日誌文件時間點,默認凌晨 4 點
deleteWhen=04
#日誌文件保留時間,默認 48 小時
fileReservedTime=48
#Broker 的角色
#- ASYNC_MASTER 異步複製Master
#- SYNC_MASTER 同步雙寫Master
brokerRole=ASYNC_MASTER
#刷盤方式
#- ASYNC_FLUSH 異步刷盤,性能好宕機會丟數
#- SYNC_FLUSH 同步刷盤,性能較差不會丟數
flushDiskType=ASYNC_FLUSH
#末尾追加,NameServer 節點列表,使用分號分割
namesrvAddr=192.168.31.200:9876;192.168.31.201:9876
broker-b.properties 只有 brokerName 不同,如下所示:
brokerClusterName=DefaultCluster
brokerName=broker-b
brokerId=0
deleteWhen=04
fileReservedTime=48
brokerRole=ASYNC_MASTER
flushDiskType=ASYNC_FLUSH
#末尾追加,NameServer 節點列表,使用分號分割
namesrvAddr=192.168.31.200:9876;192.168.31.201:9876
之後將
rocketmq-all-4.8.0-bin-release 目錄上傳到 /usr/local 目錄,運行下面命令啓動 broker 節點 a。
cd /usr/local/rocketmq-all-4.8.0-bin-release/
sh bin/mqbroker -c ./conf/2m-noslave/broker-a.properties
在 mqbroker 啓動命令後增加 c 參數說明要加載哪個 Broker 配置文件。
啓動成功會看到下面的日誌,Broker 將佔用 10911 端口提供服務,請設置防火牆放行。
The broker[broker-a, 192.168.31.210:10911] boot success. serializeType=JSON and name server is 192.168.31.200:9876;192.168.31.201:9876
同樣的,在另一臺 Master 執行下面命令,啓動並加載 broker-b 配置文件。
cd /usr/local/rocketmq-all-4.8.0-bin-release/
sh bin/mqbroker -c ./conf/2m-noslave/broker-b.properties
到這裏 NameServer 集羣與 Broker 集羣就部署好了,下面執行兩個命令驗證下。
第一個,使用 mqadmin 命令查看集羣狀態。
在 bin 目錄下存在 mqadmin 命令用於管理 RocketMQ 集羣,我們可以使用 clusterList 查看集羣節點,命令如下:
sh mqadmin clusterList -n 192.168.31.200:9876
通過查詢 NameServer 上的註冊信息,得到以下結果。
可以看到在 DefaultCluster 集羣中存在兩個 Broker,因爲 BID 編號爲 0,代表它們都是 Master 主節點。
第二個,利用 RocketMQ 自帶的 tools.sh 工具通過生成演示數據來測試 MQ 實際的運行情況。在 bin 目錄下使用下面命令。
export NAMESRV_ADDR=192.168.31.200:9876
sh tools.sh org.apache.rocketmq.example.quickstart.Producer
你會看到屏幕輸出日誌:
SendResult [sendStatus=SEND_OK, msgId=7F0000010B664DC639969F28CF540000, offsetMsgId=C0A81FD200002A9F00000000000413B6, messageQueue=MessageQueue [topic=TopicTest, brokerName=broker-a, queueId=1], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F0000010B664DC639969F28CF9B0001, offsetMsgId=C0A81FD200002A9F000000000004147F, messageQueue=MessageQueue [topic=TopicTest, brokerName=broker-a, queueId=2], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F0000010B664DC639969F28CFA30002, offsetMsgId=C0A81FD200002A9F0000000000041548, messageQueue=MessageQueue [topic=TopicTest, brokerName=broker-a, queueId=3], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F0000010B664DC639969F28CFA70003, offsetMsgId=C0A81FD300002A9F0000000000033C56, messageQueue=MessageQueue [topic=TopicTest, brokerName=broker-b, queueId=0], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F0000010B664DC639969F28CFD60004, offsetMsgId=C0A81FD300002A9F0000000000033D1F, messageQueue=MessageQueue [topic=TopicTest, brokerName=broker-b, queueId=1], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F0000010B664DC639969F28CFDB0005, offsetMsgId=C0A81FD300002A9F0000000000033DE8, messageQueue=MessageQueue [topic=TopicTest, brokerName=broker-b, queueId=2], queueOffset=0]
...
其中broker-a、broker-b 交替出現說明集羣生效了。
前面測試的是服務提供者,下面測試消費者,運行下面命令:
export NAMESRV_ADDR=192.168.31.200:9876
sh tools.sh org.apache.rocketmq.example.quickstart.Consumer
會看到消費者也獲取到數據,到這裏 RocketMQ 雙 Master 集羣的搭建就完成了,至於多 Master 多 Slave 的配置也是相似的,大家查閱官方文檔相信也能很快上手。
ConsumeMessageThread_11 Receive New Messages: [MessageExt [brokerName=broker-b, queueId=2, storeSize=203, queueOffset=157, sysFlag=0, bornTimestamp=1612100880154, bornHost=/192.168.31.210:54104, storeTimestamp=1612100880159, storeHost=/192.168.31.211:10911, msgId=C0A81FD300002A9F0000000000053509, commitLogOffset=341257, bodyCRC=1116443590, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TopicTest', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=158, CONSUME_START_TIME=1612100880161, UNIQ_KEY=7F0000010DA64DC639969F2C4B1A0314, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 82, 111, 99, 107, 101, 116, 77, 81, 32, 55, 56, 56], transactionId='null'}]]
ConsumeMessageThread_12 Receive New Messages: [MessageExt [brokerName=broker-b, queueId=3, storeSize=203, queueOffset=157, sysFlag=0, bornTimestamp=1612100880161, bornHost=/192.168.31.210:54104, storeTimestamp=1612100880162, storeHost=/192.168.31.211:10911, msgId=C0A81FD300002A9F00000000000535D4, commitLogOffset=341460, bodyCRC=898409296, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TopicTest', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=158, CONSUME_START_TIME=1612100880164, UNIQ_KEY=7F0000010DA64DC639969F2C4B210315, CLUSTER=DefaultCluster, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 82, 111, 99, 107, 101, 116, 77, 81, 32, 55, 56, 57], transactionId='null'}]]
集羣部署好,那如何使用 RocketMQ 進行消息收發呢?我們結合 Spring Boot 代碼進行講解。
應用接入 RocketMQ 集羣
我們以前面的報稅爲例,利用 Spring Boot 集成 MQ 客戶端實現消息收發,首先咱們模擬生產者 Producer。
生產者 Producer 發送消息
第一步,利用 Spring Initializr 嚮導創建 rocketmq-provider 工程,確保 pom.xml 引入以下依賴。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- RocketMQ客戶端,版本與Broker保持一致 -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.8.0</version>
</dependency>
第二步,配置應用 application.yml。
rocketmq-client 主要通過編碼實現通信,因此無須在 application.yml 做額外配置。
server:
port: 8000
spring:
application:
name: rocketmq-producer
第三步,創建 Controller,生產者發送消息。
@RestController
public class ProviderController {
Logger logger = LoggerFactory.getLogger(ProviderController.class);
@GetMapping(value = "/send_s1_tax")
public String send1() throws MQClientException {
//創建DefaultMQProducer消息生產者對象
DefaultMQProducer producer = new DefaultMQProducer("producer-group");
//設置NameServer節點地址,多個節點間用分號分割
producer.setNamesrvAddr("192.168.31.200:9876;192.168.31.201:9876");
//與NameServer建立長連接
producer.start();
try {
//發送一百條數據
for(int i = 0 ; i< 100 ; i++) {
//數據正文
String data = "{\"title\":\"X市2021年度第一季度稅務彙總數據\"}";
/*創建消息
Message消息三個參數
topic 代表消息主題,自定義爲tax-data-topic說明是稅務數據
tags 代表標誌,用於消費者接收數據時進行數據篩選。2021S1代表2021年第一季度數據
body 代表消息內容
*/
Message message = new Message("tax-data-topic", "2021S1", data.getBytes());
//發送消息,獲取發送結果
SendResult result = producer.send(message);
//將發送結果對象打印在控制檯
logger.info("消息已發送:MsgId:" + result.getMsgId() + ",發送狀態:" + result.getSendStatus());
}
} catch (RemotingException e) {
e.printStackTrace();
} catch (MQBrokerException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
producer.shutdown();
}
return "success";
}
}
在程序運行後,訪問
http://localhost:8000/send_s1_tax,在控制檯會看到如下輸出說明數據已被 Broker 接收,Broker 接收後 Producer 端任務已完成。
消息已發送:MsgId:7F00000144E018B4AAC29F3B7B280062,發送狀態:SEND_OK
消息已發送:MsgId:7F00000144E018B4AAC29F3B7B2A0063,發送狀態:SEND_OK
下面咱們開發消費者 Consumer。
消費者 Consumer 接收消息
第一步,利用 Spring Initializr 嚮導創建 rocketmq-consumer 工程,確保 pom.xml 引入以下依賴。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- RocketMQ客戶端,版本與Broker保持一致 -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.8.0</version>
</dependency>
第二步,application.yml 同樣無須做額外設置。
server:
port: 9000
spring:
application:
name: rocketmq-consumer
第三步,在應用啓動入口
RocketmqConsumerApplication 增加消費者監聽代碼,關鍵的代碼都已做好註釋。
@SpringBootApplication
public class RocketmqConsumerApplication {
private static Logger logger = LoggerFactory.getLogger(RocketmqConsumerApplication.class);
public static void main(String[] args) throws MQClientException {
SpringApplication.run(RocketmqConsumerApplication.class, args);
//創建消費者對象
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
//設置NameServer節點
consumer.setNamesrvAddr("192.168.31.200:9876;192.168.31.201:9876");
/*訂閱主題,
consumer.subscribe包含兩個參數:
topic: 說明消費者從Broker訂閱哪一個主題,這一項要與Provider保持一致。
subExpression: 子表達式用於篩選tags。
同一個主題下可以包含很多不同的tags,subExpression用於篩選符合條件的tags進行接收。
例如:設置爲*,則代表接收所有tags數據。
例如:設置爲2020S1,則Broker中只有tags=2020S1的消息會被接收,而2020S2就會被排除在外。
*/
consumer.subscribe("tax-data-topic", "*");
//創建監聽,當有新的消息監聽程序會及時捕捉並加以處理。
consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus consumeMessage(
List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
//批量數據處理
for (MessageExt msg : msgs) {
logger.info("消費者消費數據:"+new String(msg.getBody()));
}
//返回數據已接收標識
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//啓動消費者,與Broker建立長連接,開始監聽。
consumer.start();
}
}
當應用啓動後,Provider 產生新消息的同時,Consumer 端就會立即消費掉,控制檯產生輸出。
2021-01-31 22:25:14.212 INFO 17328 --- [MessageThread_3] c.l.r.RocketmqConsumerApplication : 消費者消費數據:{"title":"X市2021年度第一季度稅務彙總數據"}
2021-01-31 22:25:14.217 INFO 17328 --- [MessageThread_2] c.l.r.RocketmqConsumerApplication : 消費者消費數據:{"title":"X市2021年度第一季度稅務彙總數據"}
以上便是 Spring Boot 接入 RocketMQ 集羣的過程。對於當前的案例我們是通過代碼方式控制消息收發,在 Spring Cloud 生態中還提供了 Spring Cloud Stream 模塊,允許程序員採用“聲明式”的開發方式實現與 MQ 更輕鬆地接入,但 Spring Cloud Stream 本身封裝度太高,很多 RocketMQ 的細節也被隱藏了,這對於入門來說並不是一件好事。在掌握 RocketMQ 的相關內容後再去學習 Spring Cloud Stream 你會理解得更加透徹。