一、整體介紹
- RocketMQ 是一款分佈式、隊列模型的消息中間件
- 支持分佈式事務
- 天然的支持集羣模型、負載均衡、水平擴展能力
- 億級別的消息堆積能力
- 採用零拷貝的原理, 循序寫盤,隨機讀
- 底層通信框架採用netty NIO 框架
- NameServer 代替Zookeeper ,更輕量級
- 消息失敗重試機制,消息可查詢(可設置時間間隔和重試次數)
應用場景
- 削峯填谷:諸如秒殺、搶紅包、企業開門紅等大型活動時皆會帶來較高的流量脈衝,或因沒做相應的保護而導致系統超負荷甚至崩潰,或因限制太過導致請求大量失敗而影響用戶體驗,消息隊列 MQ 可提供削峯填谷的服務來解決該問題。
- 異步解耦:交易系統作爲淘寶/天貓主站最核心的系統,每筆交易訂單數據的產生會引起幾百個下游業務系統的關注,包括物流、購物車、積分、流計算分析等等,整體業務系統龐大而且複雜,消息隊列 MQ 可實現異步通信和應用解耦,確保主站業務的連續性。
- 順序收發: 細數日常中需要保證順序的應用場景非常多,比如證券交易過程時間優先原則,交易系統中的訂單創建、支付、退款等流程,航班中的旅客登機消息處理等等。與先進先出(First In First Out,縮寫 FIFO)原理類似,消息隊列 MQ 提供的順序消息即保證消息 FIFO。
- 分佈式事務一致性:交易系統、支付紅包等場景需要確保數據的最終一致性,大量引入消息隊列 MQ 的分佈式事務,既可以實現系統之間的解耦,又可以保證最終的數據一致性。
- 大數據分析: 數據在“流動”中產生價值,傳統數據分析大多是基於批量計算模型,而無法做到實時的數據分析,利用阿里雲消息隊列 MQ 與流式計算引擎相結合,可以很方便的實現將業務數據進行實時分析。
- 分佈式緩存同步: 天貓雙 11 大促,各個分會場琳琅滿目的商品需要實時感知價格變化,大量併發訪問數據庫導致會場頁面響應時間長,集中式緩存因爲帶寬瓶頸限制商品變更的訪問流量,通過消息隊列 MQ 構建分佈式緩存,實時通知商品數據的變化。
更多使用場景參考 -> 阿里使用場景
二、 RocketMQ 安裝
Git地址:https://github.com/apache/rocketmq/tree/release-4.3.0
1、 在解壓後的文件夾中執行maven命令,獲取程序運行包(生成的包在rocketmq-distribution/target路徑下):
mvn -Prelease-all -DskipTests clean install -U
2、rocketmq是一個集羣模型的消息隊列,這裏我們用兩臺服務器來部署rocketmq,爲了方便和區分,分別把兩臺服務器標註一下角色,如下節點配置:
IP | 角色 | 模式 |
---|---|---|
106.53.92.xxx | nameServer1,brokerServer1 | Master1 |
47.105.189.xx | nameServer2,brokerServer2 | Master2 |
3、創建消息隊列信息保存路徑
/usr/local/rocketmq/store/index
/usr/local/rocketmq/store/commitlog
/usr/local/rocketmq/store/consumequeue
4、修改broker配置
brokerClusterName=rocketmq-cluster
# broker 名字,不同文件命名不一樣
brokerName=broker-a
# 0表示Master >0 表示slave
brokerId=0
brokerIP1=本機IP
# nameServer地址,多個使用分號分割
namesrvAddr=rocketmq-nameserver1:9876
# 默認創建的隊列數
defaultTopicQueueNums=4
# 是否允許broker 自動創建topic
autoCreateTopicEnable=true
# 是否允許broker自動創建訂閱組
autoCreateSubscriptionGroup=true
# Broker 對外服務的監聽端口
listenPort=10911
# 刪除文件時間點,默認凌晨4點
deleteWhen=04
# 文件保留時間,默認48小時
fileReservedTime=120
# commitLog每個文件的大小默認1G
mapedFileSizeCommitLog=1073741824
# consumQueue每個文件默認存30W條
mapedFileSizeConsumeQueue=300000
# 檢測物理文件磁盤空間
diskMaxUsedSpaceRatio=88
#存儲路徑
storePathRootDir=/usr/local/rocketmq/store
# commitLog存儲路徑
storePathCommitLog=/usr/local/rocketmq/store/commitlog
# 消費隊列存儲路徑
storePathConsumeQueue=/usr/local/rocketmq/store/consumequeue
# 消息索引存儲路徑
storePathIndex=/usr/local/rocketmq/store/index
# checkpoint 文件存儲路徑
storeCheckpoint=/usr/local/rocketmq/store/checkpoint
# abort 文件存儲路徑
abortFile=/usr/local/rocketmq/store/abort
# ASYNC_MASTER 異步賦值 SYNC_MASTER 同步刷盤 SLAVE
brokerRole=ASYNC_MASTER
# 刷盤方式 ASYNC_FLUSH 異步刷盤 SYNC_FLUSH同步刷盤
flushDiskType=ASYNC_FLUSH
如果配置主從模式,則需要修改broke-a-s.properties
# 0表示Master >0 表示slave
brokerId=1
brokerIP1=本機IP
# ASYNC_MASTER 異步賦值 SYNC_MASTER 同步刷盤 SLAVE
brokerRole=SLAVE
5、 修改bin 下的runbroker.sh和 runserver.sh
JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn1g"
6、運行
// 啓動nameserver
nohup sh ./mqnamesrv &
// 啓動broker
nohup sh ./mqbroker -c ../conf/2m-2s-async/broker-a.properties > /dev/null 2&>1 &
// 關閉broker
sh mqshutdown broker
// 關閉nameServer
sh mqshutdown namesrv
可以用netstat -ntlp 查看一下端口占用情況
6、RocketMQ-Console(Git地址)
進入console模塊,修改application.properties
rocketmq.config.namesrvAddr=106.53.92.208:9876
// 在pom目錄下打包
$ mvn clean package -Dmaven.test.skip=true
// 運行
$ java -jar target/rocketmq-console-ng-1.0.0.jar
// 如果配置文件沒有填寫Name Server
$ java -jar target/rocketmq-console-ng-1.0.0.jar --rocketmq.config.namesrvAddr='10.0.74.198:9876;10.0.74.199:9876'
三、 代碼部分
1、一個簡單的實例
消息生產者
public class Producer {
private static final Logger logger = LoggerFactory.getLogger("Customer");
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("rocketmq-cluster");
// 設置nameserver 地址
producer.setNamesrvAddr(MQConstant.NAMESERVER1);
// 啓動實例
producer.start();
for (int i = 0; i < 10; i++) {
// 加入TagA TagB 兩個標籤用於測試
String tag = "TagA";
if(i%2==0){ tag = "TagB"; }
// 創建消息
Message msg = new Message("test", tag ,"key"+i,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
// 發送消息
SendResult sendResult = producer.send(msg);
logger.info(JSONObject.toJSONString(sendResult));
}
// 一旦生產者實例不再使用,則關閉該實例
producer.shutdown();
}
}
消息消費者
public class Customer {
private static final Logger logger = LoggerFactory.getLogger("Customer");
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocketmq-cluster");
// nameserver 地址
consumer.setNamesrvAddr(MQConstant.NAMESERVER1);
// 設置消息offset 位置
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
// 訂閱消息主題
consumer.subscribe("test","*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
MessageExt me = list.get(0);
//for (MessageExt me : list){
try {
String topic = me.getTopic();
String tags = me.getTags();
String keys = me.getKeys();
String msgBody = "";
msgBody = new String(me.getBody(),"utf-8");
logger.info("topic:{}, tags:{}, keys:{} {}",topic,tags,keys,msgBody);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("消費者已啓動");
}
}
消費者中, 如果消息第一次消費失敗怎麼辦?
customer 中可以添加消息重試機制,當消息第一次失敗可以可以進行重試,代碼如下
public class Customer {
private static final Logger logger = LoggerFactory.getLogger("Customer");
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocketmq-cluster");
// nameserver 地址
consumer.setNamesrvAddr(MQConstant.NameServerAndSlave);
// 設置消息offset 位置
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
// 訂閱消息主題
consumer.subscribe("test","*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
MessageExt me = list.get(0);
//for (MessageExt me : list){
try {
String topic = me.getTopic();
String tags = me.getTags();
String keys = me.getKeys();
String msgBody = "";
msgBody = new String(me.getBody(),"utf-8");
int i = 2/0;
logger.info("topic:{}, tags:{}, keys:{} {}",topic,tags,keys,msgBody);
} catch (Exception e) {
e.printStackTrace();
int reconsumeTimes = me.getReconsumeTimes();
logger.info("重試次數:{}",reconsumeTimes);
if(reconsumeTimes==3){
logger.error("日誌補償。。。");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
//}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("消費者已啓動");
}
}
2、生產者
2.1、延遲消息
- 延遲消息,消息發送到broker後,要特定的時間纔會被Consumer消費
- 目前只支持固定精度的定時消息
- msg.setDelayTimeLevel(2); 方法設置
2.2、發送消息到指定隊列
// 發送到指定的隊列中去
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
Integer queueNum = (Integer) o;
return list.get(queueNum);
}
}, 1);
3、生產者
3.1 PushConsumer 消費模式 - 集羣模式
- Clustering 模式 (默認)
- GroupName 用於把多個Comsummer組織到一起
- 相同Groupname 的Consumer 只消費所訂閱消息的一部分
- 目的:達到天然的負載均衡機制
適用場景
適用於消費端集羣化部署,每條消息只需要被處理一次的場景。此外,由於消費進度在服務端維護,可靠性更高。具體消費示例如下圖所示。
注意事項
集羣消費模式下,每一條消息都只會被分發到一臺機器上處理。如果需要被集羣下的每一臺機器都處理,請使用廣播模式。
集羣消費模式下,不保證每一次失敗重投的消息路由到同一臺機器上。
當我們啓動兩個消費之 A, B,然後再生產10條消息, 這時我們可以看到A消費了6條,B消費了4條,爲啥會這樣,不能實現負載均衡嗎?
因爲消費者通過監聽消息對列來實現消息的接受, 加入四個消息對列, 生產者分別向兩個消息隊列投了三條消息,另外兩個投了兩條,則會出現這種情況。
21:47:04.981 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":2,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B3490000","offsetMsgId":"6A355CD000002A9F000000000002045E","queueOffset":40,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.028 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":3,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B3950001","offsetMsgId":"6A355CD000002A9F0000000000020515","queueOffset":60,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.065 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":0,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B3C40002","offsetMsgId":"6A355CD000002A9F00000000000205CC","queueOffset":39,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.112 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":1,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B3E90003","offsetMsgId":"6A355CD000002A9F0000000000020683","queueOffset":64,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.149 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":2,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4180004","offsetMsgId":"6A355CD000002A9F000000000002073A","queueOffset":41,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.185 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":3,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B43E0005","offsetMsgId":"6A355CD000002A9F00000000000207F1","queueOffset":61,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.219 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":0,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4610006","offsetMsgId":"6A355CD000002A9F00000000000208A8","queueOffset":40,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.259 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":1,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4830007","offsetMsgId":"6A355CD000002A9F000000000002095F","queueOffset":65,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.298 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":2,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4AB0008","offsetMsgId":"6A355CD000002A9F0000000000020A16","queueOffset":42,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.338 [main] INFO Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":3,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4D20009","offsetMsgId":"6A355CD000002A9F0000000000020ACD","queueOffset":62,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
如果存在這樣一個場景, 一個producer 的同一個topic下分別有TagA、TagB、TagC , 這三個tag 分別需要被三個不同的consumer 消費,這樣該如何實現?
我們所知的pushConsumer的集羣模式不能很好地去實現這種需求,會有其他辦法嗎?
3.2 PushConsumer 消費模式 - 廣播模式
-
BroadCasting 模式 (廣播模式)
-
同一個ConsumerGroup 裏的Consumer 都消費訂閱topic 全部信息,也就是一條消息會被每一個Consumer消費
-
consumer.setMessageModel(MessageModel.BROADCASTING);
方法設置廣播模式
廣播消費:當使用廣播消費模式時,消息隊列 MQ 會將每條消息推送給集羣內所有註冊過的消費者,保證消息至少被每個消費者消費一次。
注意事項
- 廣播消費模式下不支持順序消息。
- 廣播消費模式下不支持重置消費位點。
- 每條消息都需要被相同訂閱邏輯的多臺機器處理。
- 消費進度在客戶端維護,出現重複消費的概率稍大於集羣模式。
- 廣播模式下,消息隊列 MQ 保證每條消息至少被每臺客戶端消費一次,但是並不會重投消費失敗的消息,因此業務方需要關注消費失敗的情況。
- 廣播模式下,客戶端每一次重啓都會從最新消息消費。客戶端在被停止期間發送至服務端的消息將會被自動跳過,請謹慎選擇。
- 廣播模式下,每條消息都會被大量的客戶端重複處理,因此推薦儘可能使用集羣模式。
- 廣播模式下服務端不維護消費進度,所以消息隊列 MQ 控制檯不支持消息堆積查詢、消息堆積報警和訂閱關係查詢功能。
通過上述可以看到廣播模式並不推薦使用,我們可以通過集羣模式來模擬廣播模式
使用集羣模式模擬廣播
適用場景:
適用於每條消息都需要被多臺機器處理,每臺機器的邏輯可以相同也可以不一樣的場景。具體消費示例如下圖所示。
如果業務需要使用廣播模式,也可以創建多個 Group ID,用於訂閱同一個 Topic。
注意事項
消費進度在服務端維護,可靠性高於廣播模式。
對於一個 Group ID 來說,可以部署一個消費者實例,也可以部署多個消費者實例。當部署多個消費者實例時,實例之間又組成了集羣模式(共同分擔消費消息)。假設 Group ID 1 部署了三個消費者實例 C1、C2、C3,那麼這三個實例將共同分擔服務器發送給 Group ID 1 的消息。同時,實例之間訂閱關係必須保持一致。
3.3 PullConsumer 消息拉取消費模式
pull方式裏,取消息的過程需要用戶自己寫,首先通過打算消費的Topic拿到MessageQueue的集合,遍歷MessageQueue集合,然後針對每個MessageQueue批量取消息,一次取完後,記錄該隊列下一次要取的開始offset,直到取完了,再換另一個MessageQueue。
/**
* 消息拉取模式
**/
public class PullConsumer {
private static final Logger log = LoggerFactory.getLogger("PullConsumer");
//保存上一次消費的消息位置
private static final Map offsetTable = new HashMap();
public static void main(String[] args) throws MQClientException {
//實例化pullConsumer
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("rocketmq-cluster");
consumer.setNamesrvAddr(MQConstant.Master_Slave);
consumer.start();
//獲取topic下所有的隊列
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("test");
//遍歷消息隊列
for (MessageQueue mq : mqs) {
log.info("消息隊列信息: " + mq);
SINGLE_MQ:
while (true) {
try {
//設置上次消費消息下標
PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
// 保存消息下次讀取的 offset
putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
switch (pullResult.getPullStatus()) {
//根據結果狀態,如果找到消息,批量消費消息
case FOUND:
List<MessageExt> messageExtList = pullResult.getMsgFoundList();
for (MessageExt m : messageExtList) {
log.info("topic:{}; getQueueId:{}; offset:{}; 消息內容:{}",m.getTopic(),m.getQueueId(),m.getQueueOffset(),new String(m.getBody()));
}
break;
case NO_MATCHED_MSG:
log.warn("沒有匹配的信息");
break;
case NO_NEW_MSG:
log.warn("沒有新的的信息");
break SINGLE_MQ;
case OFFSET_ILLEGAL:
log.warn("OFFSET_ILLEGAL");
break;
default:
break;
}
} catch (Exception e) {
log.error("消息消費出現異常:");
e.printStackTrace();
}
}
}
consumer.shutdown();
}
//保存上次消費的消息下標,這裏使用了一個全局HashMap來保存
private static void putMessageQueueOffset(MessageQueue mq, long offset) {
offsetTable.put(mq, offset);
}
//獲取上次消費的消息的下表 這裏可以保存在硬盤或redis中 ConsumerName-topic-queueId做key
private static long getMessageQueueOffset(MessageQueue mq) {
Long offset = (Long) offsetTable.get(mq);
if (offset != null) {
return offset;
}
return 0;
}
}
4、消息落地方式
4.1 同步刷盤和異步刷盤
RocketMQ的消息是存儲到磁盤上的,這樣既能保證斷電後恢復,又可以讓存儲的消息量超出內存的限制。
消息在通過Producer寫入RocketMQ的時候,有兩種寫磁盤方式:
異步刷盤方式: 在返回寫成功狀態時,消息可能只是被寫入了內存的PAGECACHE,寫操作的返回快,
吞吐量大;當內存裏的消息量積累到一定程度時,統一觸發寫磁盤操作,快速寫入
同步刷盤方式: 在返回寫成功狀態時,消息已經被寫入磁盤。具體流程是,消息寫入內存的PAGECACHE後,立刻通知刷盤線程刷盤,然後等待刷盤完成,刷盤線程執行完成後喚醒等待的線程,返回消息寫成功的狀態。
4.12同步雙寫和異步複製
異步複製和同步雙寫主要是主和從的關係。消息需要實時消費的,就需要採用主從模式部署
異步複製: 比如這裏有一主一從,我們發送一條消息到主節點之後,這樣消息就算從producer端發送成功了,然後通過異步複製的方法將數據複製到從節點
同步雙寫: 比如這裏有一主一從,我們發送一條消息到主節點之後,這樣消息就並不算從producer端發送成功了,需要通過同步雙寫的方法將數據同步到從節點後, 纔算數據發送成功。