最近的項目開發中涉及到支付業務的模塊需要用到MQ進行業務解耦以及把用戶請求量削峯填谷,提高系統的可用性和可靠性,我們選擇了RocketMQ來部署消息中間件集羣,我也在此回顧和歸納下RocketMQ的相關知識。
什麼是RocketMQ
阿里開源的分佈式消息中間件,單機就能支持千萬級的消息堆積,集羣模式能滿足海量消息堆積的場景
RocketMQ的特點
支持發佈/訂閱(Pub/Sub)和點對點(P2P)消息模型
在一個隊列中可靠的先進先出(FIFO)和嚴格的順序傳遞
支持拉(pull)和推(push)兩種消息模式
單一隊列百萬消息的堆積能力
支持多種消息協議,如 JMS、MQTT 等
分佈式高可用的部署架構,滿足至少一次消息傳遞語義
提供 docker 鏡像用於隔離測試和雲集羣部署
提供配置、指標和監控等功能豐富的 Dashboard
核心機制與原理
RocketMQ底層基於隊列模型來實現消息收發功能。RocketMQ集羣中包含4個模塊:Namesrv, Broker, Producer, Consumer。
Namesrv: 存儲當前集羣所有Brokers信息、Topic跟Broker的對應關係。
Broker: 集羣最核心模塊,主要負責Topic消息存儲、消費者的消費位點管理(消費進度)。
Producer: 消息生產者,每個生產者都有一個ID(編號),多個生產者實例可以共用同一個ID。同一個ID下所有實例組成一個生產者集羣。
Consumer: 消息消費者,每個訂閱者也有一個ID(編號),多個消費者實例可以共用同一個ID。同一個ID下所有實例組成一個消費者集羣。
每30秒心跳機制
單個Broker跟所有Namesrv保持心跳請求,心跳間隔爲30秒,心跳請求中包括當前Broker所有的Topic信息。Namesrv會反查Broer的心跳信息, 如果某個Broker在2分鐘之內都沒有心跳,則認爲該Broker下線,調整Topic跟Broker的對應關係。但此時Namesrv不會主動通知Producer、Consumer有Broker宕機。
Consumer跟Broker是長連接,會每隔30秒發心跳信息到Broker。Broker端每10秒檢查一次當前存活的Consumer,若發現某個Consumer 2分鐘內沒有心跳, 就斷開與該Consumer的連接,並且向該消費組的其他實例發送通知,觸發該消費者集羣的負載均衡(rebalance)。
生產者每30秒從Namesrv獲取Topic跟Broker的映射關係,更新到本地內存中。再跟Topic涉及的所有Broker建立長連接,每隔30秒發一次心跳。 在Broker端也會每10秒掃描一次當前註冊的Producer,如果發現某個Producer超過2分鐘都沒有發心跳,則斷開連接。
集羣模式部署
單 master 模式 也就是隻有一個 master 節點,稱不上是集羣,一旦這個 master 節點宕機,那麼整個服務就不可用,適合個人學習使用。
多 master 模式 多個 master 節點組成集羣,單個 master 節點宕機或者重啓對應用沒有影響。
優點:所有模式中性能最高
缺點:單個master節點宕機期間,未被消費的消息在節點恢復之前不可用,消息的實時性就受到影響。注意:使用同步刷盤可以保證消息不丟失,同時 Topic 相對應的 queue 應該分佈在集羣中各個節點,而不是隻在某各節點上,否則,該節點宕機會對訂閱該 topic 的應用造成影響。
多 master 多 slave 異步複製模式 在多 master 模式的基礎上,每個 master 節點都有至少一個對應的 slave。master 節點可讀可寫,但是 slave 只能讀不能寫,類似於 mysql 的主備模式。 優點: 在 master 宕機時,消費者可以從 slave 讀取消息,消息的實時性不會受影響,性能幾乎和多 master 一樣。 缺點:使用異步複製的同步方式有可能會有消息丟失的問題。
多 master 多 slave 同步雙寫模式 同多 master 多 slave 異步複製模式類似,區別在於 master 和 slave 之間的數據同步方式。 優點:同步雙寫的同步模式能保證數據不丟失。 缺點:發送單個消息 RT 會略長,性能相比異步複製低10%左右。 刷盤策略:同步刷盤和異步刷盤(指的是節點自身數據是同步還是異步存儲) 同步方式:同步雙寫和異步複製(指的一組 master 和 slave 之間數據的同步) 注意:要保證數據可靠,需採用同步刷盤和同步雙寫的方式,但性能會較其他方式低。
在此舉個可靠異步消息發佈和消息訂閱的例子 , 更多的實戰經歷後續文章繼續發佈
可靠異步消息
發送可靠異步消息
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// MQ服務器地址端口
producer.setNamesrvAddr("localhost:9876");
//初始化對象實例
producer.start();
producer.setRetryTimesWhenSendAsyncFailed(0);
for (int i = 0; i < 100; i++) {
final int index = i;
//創建消息實體 包括 topic, tag and message body.
Message msg = new Message("TopicTest",
"TagA",
"OrderID188",
"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.printf("%-10d OK %s %n", index,
sendResult.getMsgId());
}
@Override
public void onException(Throwable e) {
System.out.printf("%-10d Exception %s %n", index, e);
e.printStackTrace();
}
});
}
//關閉不再使用的連接,釋放資源
producer.shutdown();
消費消息
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("TopicTest", "TagA || TagC || TagD");
consumer.registerMessageListener(new MessageListenerOrderly() {
AtomicLong consumeTimes = new AtomicLong(0);
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeOrderlyContext context) {
context.setAutoCommit(false);
System.out.printf(Thread.currentThread().getName() + " Receive New Messages: " + msgs + "%n");
this.consumeTimes.incrementAndGet();
if ((this.consumeTimes.get() % 2) == 0) {
return ConsumeOrderlyStatus.SUCCESS;
} else if ((this.consumeTimes.get() % 3) == 0) {
return ConsumeOrderlyStatus.ROLLBACK;
} else if ((this.consumeTimes.get() % 4) == 0) {
return ConsumeOrderlyStatus.COMMIT;
} else if ((this.consumeTimes.get() % 5) == 0) {
context.setSuspendCurrentQueueTimeMillis(3000);
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();