目錄
RocketMQ優點
解耦、削峯、數據分發
RocketMQ缺點
- 系統可用性降低:系統引入外部依賴越多,系統穩定性越差,一旦mq宕機,就會對業務造成影響。如何保證MQ高可用?
- 系統複雜度提高:mq的加入大大增加了系統的複雜度,以前系統間是同步的遠程調用,現在是通過mq進行異步調用。如何保證 消息沒有被重複消息?怎麼處理消息丟失情況?如何保證消息傳遞的順序性?
- 一致性問題:A系統處理完業務,通過MQ給B、C、D三個系統發消息數據,如果B系統、C系統處理成功,D系統處理失敗。如何保證消息數據處理的一致性?
RocketMQ 集羣搭建
RocketMQ角色
- producer:消息的發送者,會與nameserver(隨機選擇)建立長鏈接,發送topic消息定期從nameserver獲取broker路由信息,並向提供topic master建立長鏈接,且定時向master broker發送心跳。producer完全無狀態,可集羣部署
- consumer:消息接受者,會與nameserver(隨機選擇)建立長鏈接,定期從namesever獲取topic路由信息,並向提供topic服務的master、salve建立長鏈接,且定時向master、salve發送心跳。consumer既可以從master訂閱消息,也可以從slave訂閱消息,訂閱規則由broker配置決定。
- broker:暫存和傳輸消息,broker分爲 master和slave,一個master可以對應多個slave,但是一個slave只能對應一個master,master也可以比部署多個。每個broker與namesever集羣中的所有節點建立長鏈接,定時註冊topic信息到所有nameserver。
- namesever:管理broker,namesever集羣是無狀態的節點,其中的每個節點broker都會向每個namesever上報信息
- topic:區分消息的種類;一個發送者可以發送消息給一個或者多個topic;一個消息的接收者可以訂閱一個或者多個topic消息
- message queue:相當於topic的分區,用於並行發送和接收消息
集羣模式
單 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 之間數據的同步)
- 注意:要保證數據可靠,需採用同步刷盤和同步雙寫的方式,但性能會較其他方式低。
雙主雙從集羣搭建
- 總體構架
- 集羣工作流程
- 啓動namsever,namesever起來後監聽端口,等待broker、producer、consumer連上來,相當於一個路由管理控制中心。
- broker啓動,跟所有的namesever保持長鏈接,定時發送心跳包,心跳包中包含當前broker信息(ip+端口號等)以及存儲所有topic信息。註冊成功後,namesever集羣中就有topic跟broker的映射關係。
- 發送消息前,先創建topic,創建topic時需要指定該topic要存儲在哪些broker上,也可以在發送消息時自動創建topic
- producer發送消息,啓動時先跟nameserver集羣中的其中一臺建立鏈接,並從namesever中獲取當前發送topic存在哪些broker上,輪詢從隊列列表中選擇一個隊列,然後與隊列所在broker建立長鏈接從而向broker發消息。
- consumer跟producer類似,跟其中一臺nameserver建立長鏈接,獲取當前發送topic存在哪些broker上,然後跟broker建立通道,開始消費消息。
3. 雙主雙從集羣搭建
搭建過程可以參考如下鏈接: https://www.jianshu.com/p/66afa5ae41ba
RocketMQ消息發送樣例
- 導入mq客戶端依賴
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.7.0</version>
</dependency>
- 消息發送者步驟分析
1.創建消息生產者producer,並制定生產者組名
2.制定NameServer地址
3.啓動producer
4.創建消息對象,制定主題Topic、tag和消息體
5.發送消息
6.關閉生產者produce
- 消息消費者步驟分析
1.創建消費者consumer,制定消費者組名
2.指定Nameserver地址
3.訂閱主題Topic和tag
4.設置回調函數、處理消息
5.啓動消費者consumer
基本樣例
-
發送同步消息:可靠性同步地發送方式使用的比較廣泛,比如:重要的消息通知、短信通知
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import java.util.concurrent.TimeUnit;
public class SyncProducer {
public static void main(String[] args) throws Exception {
// 1.創建消息生產者producer,並制定生產者組名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 3.啓動producer
producer.start();
for (int i = 0; i < 10; i++) {
// 4.創建消息對象,指定主題Topic、Tag和消息體
/*
參數1:消息主題Topic
參數2:消息Tag
參數3:消息內容
*/
Message msg = new Message("base","tag1",("hello world"+i).getBytes());
// 5.發送消息結果包含 發送狀態 消息id 消息接收隊列id等
SendResult result = producer.send(msg);
System.out.println("發送結果"+result);
// 線程睡眠1秒
TimeUnit.SECONDS.sleep(1);
}
// 6關閉生產者producer
producer.shutdown();
}
}
- 發送異步消息:
通常對響應時間敏感的業務場景,即發送不能容忍長時間地等待broker的響應
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import java.util.concurrent.TimeUnit;
public class AsyncProducer {
public static void main(String[] args) throws Exception {
// 1.創建消息生產者producer,並制定生產者組名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 3.啓動producer
producer.start();
for (int i = 0; i < 10; i++) {
// 4.創建消息對象,指定主題Topic、Tag和消息體
/*
參數1:消息主題Topic
參數2:消息Tag
參數3:消息內容
*/
Message msg = new Message("base","tag2",("hello world"+i).getBytes());
// 5.發送消息結果包含 發送狀態 消息id 消息接收隊列id等
producer.send(msg, new SendCallback() {
// 發送成功回調函數
public void onSuccess(SendResult sendResult) {
System.out.println("發送結果:"+sendResult);
}
// 發送失敗回調函數
public void onException(Throwable e) {
System.out.println("發送異常"+e);
}
});
// 線程睡眠1秒
TimeUnit.SECONDS.sleep(1);
}
// 6關閉生產者producer
producer.shutdown();
System.out.println("發送完成");
}
}
- 發送單向消息:
這種方式主要用於不特別關心發送結果的場景,例如日誌發送
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.util.concurrent.TimeUnit;
public class OnewayProducer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
// 1.創建消息生產者producer,並制定生產者組名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 3.啓動producer
producer.start();
for (int i = 0; i < 10; i++) {
// 4.創建消息對象,指定主題Topic、Tag和消息體
/*
參數1:消息主題Topic
參數2:消息Tag
參數3:消息內容
*/
Message msg = new Message("base","tag3",("hello world,單向消息"+i).getBytes());
// 5.發送單向消息
producer.sendOneway(msg);
System.out.println("發送單向消息");
// 線程睡眠1秒
TimeUnit.SECONDS.sleep(1);
}
// 6關閉生產者producer
producer.shutdown();
}
}
- 消費者
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import java.util.List;
public class Consumer {
public static void main(String[] args) throws MQClientException {
// 1.創建消費者Consumer,制定消費者組名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 2.指定Nameserver地址
consumer.setNamesrvAddr("localhost:9876");
// 3.訂閱主題Topic和Tag
consumer.subscribe("base","tag1");
// 消費模式:默認是負載均衡模式,還有一種是廣播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
// 4.設置回調函數,處理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接收消息內容
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println(new String(messageExt.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 5.啓動消費者consumer
consumer.start();
}
}
- 廣播消費模式
- 集羣消費模式
順序消息
消息有序指的是可以按照消息的發送順序來消費(FIFO)RocketMQ可以嚴格的保證消息有序,可以分爲分區有序或者全局有序。
順序消費的原理解析,在默認的情況下消息發送採取round robin輪詢方式把消息發送到不同的queue(區分隊列);而消費消息的時候從多個queue上拉去消息,這種情況發送和消費不能保證順序。但是如果控制發送的順序消息只依次發送到同一個queue中,消費的時候只從這個queue上依次拉取,則就保證了順序。當發送和消費參與的queue只有一個,則是全局有序;如果多個queue參與,則爲分區有序,即相對每個queue,消息是有序的
public class OrderStep {
private long orderId;
private String desc;
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public long getOrderId() {
return orderId;
}
public void setOrderId(long orderId) {
this.orderId = orderId;
}
@Override
public String toString() {
return "OrderStep{" +
"orderId=" + orderId +
", desc='" + desc + '\'' +
'}';
}
public static List<OrderStep> buildOrders() {
// 1039L 創建 付款 推送 完成
// 1065L 創建 付款
// 7235L 創建 付款
List<OrderStep> orderList = new ArrayList<OrderStep>();
OrderStep demo = new OrderStep();
demo = new OrderStep();
demo.setOrderId(6L);
demo.setDesc("創建");
orderList.add(demo);
demo = new OrderStep();
demo.setOrderId(6L);
demo.setDesc("付款");
orderList.add(demo);
demo.setOrderId(6L);
demo.setDesc("推送");
orderList.add(demo);
demo = new OrderStep();
demo.setOrderId(6L);
demo.setDesc("完成");
orderList.add(demo);
demo = new OrderStep();
demo.setOrderId(7L);
demo.setDesc("推送");
orderList.add(demo);
demo = new OrderStep();
demo.setOrderId(7L);
demo.setDesc("完成");
orderList.add(demo);
demo = new OrderStep();
demo.setOrderId(9L);
demo.setDesc("創建");
orderList.add(demo);
demo = new OrderStep();
demo.setOrderId(9L);
demo.setDesc("付款");
orderList.add(demo);
demo = new OrderStep();
demo.setOrderId(9L);
demo.setDesc("推送");
orderList.add(demo);
demo = new OrderStep();
demo.setOrderId(9L);
demo.setDesc("完成");
orderList.add(demo);
return orderList;
}
}
- 生產者
public class Producer {
public static void main(String[] args) throws Exception {
// 1.創建消息生產者producer,並制定生產者組名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 3.啓動producer
producer.start();
// 構建消息集合
List<OrderStep> orderStepList = OrderStep.buildOrders();
// 發送消息
for (int i = 0; i < orderStepList.size(); i++) {
String body = orderStepList.get(i)+"";
Message message = new Message("OrderTopic","Order","i"+i,body.getBytes());
/**
* 參數1:消息對象
* 參數2:消息隊列的選擇器
* 參數3:選擇隊列的業務標識(訂單id)
*/
SendResult send = producer.send(message, new MessageQueueSelector() {
/**
*
* @param list 隊列集合
* @param message 消息對象
* @param o 業務標識的參數
* @return
*/
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
long orderId = (Long) o;
long index = orderId % list.size();
return list.get((int) index);
}
}, orderStepList.get(i).getOrderId());
System.out.println("發送結果:"+send);
}
producer.shutdown();
}
}
- 消費者
public class Consumer {
public static void main(String[] args) throws MQClientException {
// 1.創建消費者Consumer,制定消費者組名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 2.指定Nameserver地址
consumer.setNamesrvAddr("localhost:9876");
// 3.訂閱主題Topic和Tag
consumer.subscribe("OrderTopic","*");
// 4.註冊消息監聽器
consumer.registerMessageListener(new MessageListenerOrderly() {
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
for (MessageExt messageExt : list) {
System.out.println("線程名稱:["+Thread.currentThread().getName()+"]消費消息:"+new String(messageExt.getBody()));
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
// 5.啓動消費者
consumer.start();
System.out.println("消費者啓動");
}
}
延時消息
- 生產者
public class Producer {
public static void main(String[] args) throws Exception {
// 1.創建消息生產者producer,並制定生產者組名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 3.啓動producer
producer.start();
for (int i = 0; i < 10; i++) {
// 4.創建消息對象,指定主題Topic、Tag和消息體
/*
參數1:消息主題Topic
參數2:消息Tag
參數3:消息內容
*/
Message msg = new Message("DelayTopic","tag1",("hello world"+i).getBytes());
// 設置延遲時間 RocketMq並不支持任意時間的延時,需要試着幾個固定的延時等級
// 從1s到2h分別對應着等級1到18 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
msg.setDelayTimeLevel(2);
// 5.發送消息結果包含 發送狀態 消息id 消息接收隊列id等
SendResult result = producer.send(msg);
System.out.println("發送結果"+result);
// 線程睡眠1秒
TimeUnit.SECONDS.sleep(1);
}
// 6關閉生產者producer
producer.shutdown();
}
}
- 消費者
public class Consumer {
public static void main(String[] args) throws MQClientException {
// 1.創建消費者Consumer,制定消費者組名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 2.指定Nameserver地址
consumer.setNamesrvAddr("localhost:9876");
// 3.訂閱主題Topic和Tag
consumer.subscribe("DelayTopic","tag1");
// 消費模式:默認是負載均衡模式,還有一種是廣播模式
// consumer.setMessageModel(MessageModel.BROADCASTING);
// 4.設置回調函數,處理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接收消息內容
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println("消息id:【"+messageExt.getMsgId()+"】,延時時間:"+(System.currentTimeMillis()-messageExt.getStoreTimestamp()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 5.啓動消費者consumer
consumer.start();
System.out.println("消費者啓動");
}
}
批量消息
批量發送消息能顯著提高傳遞小消息的性能。限制是這些批量消息應該有相同的topic,相同的waitStoreMsgOk,
而且不能是延時消息。此外,這一批消息的總大小不應超過4MB
如果超過4MB,這時候最好把消息進行分割
- 生產者
public class Producer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
// 1.創建消息生產者producer,並制定生產者組名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 3.啓動producer
producer.start();
List<Message> msgs = new ArrayList<Message>();
// 4.創建消息對象,指定主題Topic、Tag和消息體
/*
參數1:消息主題Topic
參數2:消息Tag
參數3:消息內容
*/
Message msg1 = new Message("batchTopic","tag1",("hello world"+1).getBytes());
Message msg2 = new Message("batchTopic","tag1",("hello world"+2).getBytes());
Message msg3 = new Message("batchTopic","tag1",("hello world"+2).getBytes());
msgs.add(msg1);
msgs.add(msg2);
msgs.add(msg3);
// 5.發送消息結果包含 發送狀態 消息id 消息接收隊列id等
SendResult result = producer.send(msgs);
System.out.println("發送結果"+result);
// 線程睡眠1秒
TimeUnit.SECONDS.sleep(1);
// 6關閉生產者producer
producer.shutdown();
}
}
- 消費者
public class Consumer {
public static void main(String[] args) throws MQClientException {
// 1.創建消費者Consumer,制定消費者組名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 2.指定Nameserver地址
consumer.setNamesrvAddr("localhost:9876");
// 3.訂閱主題Topic和Tag
consumer.subscribe("batchTopic","*");
// 消費模式:默認是負載均衡模式,還有一種是廣播模式
// consumer.setMessageModel(MessageModel.BROADCASTING);
// 4.設置回調函數,處理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接收消息內容
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println(new String(messageExt.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 5.啓動消費者consumer
consumer.start();
System.out.println("消費者啓動");
}
}
過濾消息
- 生產者
public class Producer {
public static void main(String[] args) throws Exception {
// 1.創建消息生產者producer,並制定生產者組名
DefaultMQProducer producer = new DefaultMQProducer("group1");
// 2.指定NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 3.啓動producer
producer.start();
for (int i = 0; i < 3; i++) {
// 4.創建消息對象,指定主題Topic、Tag和消息體
/*
參數1:消息主題Topic
參數2:消息Tag
參數3:消息內容
*/
Message msg = new Message("filterTopic","tag1",("hello world"+i).getBytes());
// 5.發送消息結果包含 發送狀態 消息id 消息接收隊列id等
SendResult result = producer.send(msg);
System.out.println("發送結果"+result);
// 線程睡眠1秒
TimeUnit.SECONDS.sleep(1);
}
// 6關閉生產者producer
producer.shutdown();
}
}
- 消費者
public class Consumer {
public static void main(String[] args) throws MQClientException {
// 1.創建消費者Consumer,制定消費者組名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
// 2.指定Nameserver地址
consumer.setNamesrvAddr("localhost:9876");
// 3.訂閱主題Topic和Tag
consumer.subscribe("filterTopic","tag1 || tag2");
// 消費模式:默認是負載均衡模式,還有一種是廣播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
// 4.設置回調函數,處理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接收消息內容
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println(new String(messageExt.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 5.啓動消費者consumer
consumer.start();
System.out.println("消費者啓動");
}
}
事務消息
這張圖說明了事務消息的大致方案,分爲兩個邏輯:正常事務消息的發送及提交、事務消息的補償流程
1、事務消息發送及提交:
- 發送消息(half消息)
- 服務端響應消息寫入結果
- 根據發送結果執行本地事務(如果寫入失敗,此時half消息對業務不可見,本地邏輯不執行)
- 根據本地事務狀態執行Commit或者Rollback(Commit操作生成消息索引,消息對消費者可見)
2、補償流程:
- 對沒有Commit/Rollback的事務消息(pending狀態的消息),從服務端發起一次“回查”
- Producer收到回查消息,檢查回查消息對應的本地事務的狀態
- 根據本地事務狀態,重新Commit或者Rollback
- 補償階段用於解決消息Commit或者Rollback發生超時或者失敗的情況。
3、事務消息狀態
事務消息共有三種狀態,提交狀態、回滾狀態、中間狀態:
- TransactionStatus.CommitTransaction: 提交事務,它允許消費者消費此消息
- TransactionStatus.RollbackTransaction: 回滾事務,它代表該消息將被刪除,不允許被消費
- TransactionStatus.Unknown: 中間狀態,它代表需要檢查消息隊列來確定狀態
注意事項:要使用集羣實現事務消息,集羣必須是異步的2m-2s-async
4、生產者
public class Producer {
public static void main(String[] args) throws Exception {
// 1.創建消息生產者producer,並制定生產者組名 事務生產者
TransactionMQProducer producer = new TransactionMQProducer("group5");
// 2.指定NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 生產者監聽器
producer.setTransactionListener(new TransactionListener() {
/**
* 在該方法中執行本地事務 第二步
* @param message
* @param o
* @return
*/
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
if (StringUtils.equals("taga",message.getTags())){
return LocalTransactionState.COMMIT_MESSAGE;
} else if (StringUtils.equals("tagb",message.getTags())){
return LocalTransactionState.ROLLBACK_MESSAGE;
} else if (StringUtils.equals("tagc",message.getTags())){
return LocalTransactionState.UNKNOW;
}
return LocalTransactionState.UNKNOW;
}
/**
* 該方法是mq進行消息事務狀態回查 第三步
* @param messageExt
* @return
*/
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
System.out.println("消息的tag:"+messageExt.getTags());
return LocalTransactionState.COMMIT_MESSAGE;
}
});
// 3.啓動producer
producer.start();
String[] tags = {"taga","tagb","tagc"};
for (int i = 0; i < 3; i++) {
// 4.創建消息對象,指定主題Topic、Tag和消息體
/*
參數1:消息主題Topic
參數2:消息Tagf
參數3:消息內容
*/
Message msg = new Message("transactionTopic",tags[i],("hello world"+i).getBytes());
// 5.發送消息結果包含 發送狀態 消息id 消息接收隊列id等 第一步
SendResult result = producer.sendMessageInTransaction(msg,null);
System.out.println("發送結果"+result);
// 線程睡眠1秒
TimeUnit.SECONDS.sleep(3);
}
// 6關閉生產者producer
// producer.shutdown();
}
}
5、消費者
public class Consumer {
public static void main(String[] args) throws MQClientException {
// 1.創建消費者Consumer,制定消費者組名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group5");
// 2.指定Nameserver地址
consumer.setNamesrvAddr("localhost:9876");
// 3.訂閱主題Topic和Tag
consumer.subscribe("transactionTopic","*");
// 4.設置回調函數,處理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接收消息內容
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println(new String(messageExt.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 5.啓動消費者consumer
consumer.start();
System.out.println("消費者啓動");
}
}
6、使用限制
- 事務消息不支持批量或者延遲消息
- 對於UNKNOW的消息,RocketMQ會嘗試回調15次,超過15次會扔掉消息,並記錄錯誤日誌;可以修改broker的transactionCheckMax屬性來改變這個次數,如果不想扔掉消息(或者想做一些其他的操作),也可以重寫AbstractTransactionCheckListener類來修改默認的扔掉消息的行爲。
- 如果返回狀態爲UNKNOW,那麼會每 60秒 執行一次事務狀態檢查回調函數,這個時間可以通過修改broker的transactionTimeout參數來改變,或者在發送消息的時候修改用戶參數:CHECK_IMMUNITY_TIME_IN_SECONDS,這個參數優先於transactionTimeout。
- 事務消息可能被檢查或者消費多次?
- 反正RocketMQ開源版本還是會丟數據,如果你一點兒都不想丟,建議使用同步的 double wirte機制。
- Producer 發送者的ID不能和其他非事務型生產者的ID共用
消息存儲
分佈式隊列因爲有高可靠行的要求,所以數據要進行持久化存儲
- 消息生成者發送消息
- mq收到消息,將消息進行持久化,在存儲中新增一條記錄
- 返回ack給生產者
- mq push消息給對應的消費者,然後等待消費者返回ack
- 如果消息者在指定時間內成功返回ack,那麼mq 認爲消息消費成功,在存儲中刪除消息,即執行第6步;如果mq在指定時間內沒有收到ack,則認爲消息消費失敗,會嘗試重新push消息,重複執行push操作
- mq刪除消息
存儲介質
- 關係型數據庫
普通關係型數據庫(如Mysql)在單表數據量達到千萬級別的情況下,其IO讀寫性能往往 會出現瓶頸。在可靠性方面,該種方案非常依賴DB,如果一旦DB出現故障,則MQ的消息就無法落盤存儲會導致線上故障。
- 文件系統(推薦)
目前業界較爲常用的幾款產品(RocketMQ/Kafka/RabbitMQ)均採用的是消息刷盤 至所部署虛擬機/物理機的文件系統來做持久化(刷盤一般可以分爲異步刷盤和同步刷盤兩種模式)。消息刷盤爲消息存儲提供了一種高效率、高可靠性和高性能的數據持久化方式。除非部署MQ機器本身或是本地磁盤掛了,否則一般是不會出現無法持 久化的故障問題。
- 性能對比
文件系統 > 關係型數據庫
消息的存儲和發送
- 消息存儲
RocketMQ的消息用順序寫, 保證了消息存儲的速度。目前的高性能磁盤,順序寫速度可以達到600MB/s, 超過了一般網卡的傳輸速度。但是磁盤隨機寫的速度只有大概100KB/s,和順序寫的性能相差6000倍
- 消息發送
Linux操作系統分爲【用戶態】和【內核態】,文件操作、網絡操作需要涉及這兩種形態 的切換,免不了進行數據複製。 一臺服務器 把本機磁盤文件的內容發送到客戶端,一般分爲兩個【讀】和【寫】兩個步驟,這兩個看似簡單的操作,實際進行了4 次數據複製,分別是:
- 從磁盤複製數據到內核態內存;
- 從內核態內存復 制到用戶態內存;
- 然後從用戶態 內存複製到網絡驅動的內核態內存;
- 最後是從網絡驅動的內核態內存復 制到網卡中進行傳輸。
通過使用mmap的方式(零拷貝),可以省去向用戶態的內存複製,提高速度。這種機制在Java中是 通過MappedByteBuffer實現的 RocketMQ充分利用了上述特性,提高消息存盤和網絡發送的速度。
這裏需要注意的是,採用MappedByteBuffer這種內存映射的方式有幾個限制,其中之一是一次只能映射1.5~2G 的文件至用戶態的虛擬內存,這也是爲何RocketMQ 默認設置單個CommitLog日誌數據文件爲1G的原因了。
消息存儲結構
- 簡介
RocketMQ消息的存儲是由ConsumeQueue和CommitLog配合完成的,消息真正的物理存儲文件是CommitLog,ConsumeQueue是消息的邏輯隊列,類似數據庫的索引文件,存儲的是指向物理存儲的地址。
每個Topic下的每個Message Queue都有一個對應 的ConsumeQueue文件。
- 存儲結構例圖
-
CommitLog:存儲消息的元數據
-
ConsumerQueue:存儲消息在CommitLog的索引,(即使ConsumerQueue丟失也沒關係,可以通過CommitLog恢復回來)
-
IndexFile:爲了消息查詢提供了一種通過key或時間區間來查詢消息的方法,這種通過IndexFile來查找消息的方法不影響發送與消費消息的主流程
-
配置文件對應參數
#commitLog 存儲路徑
storePathCommitLog=/usr/local/java/rocketmq/store/commitlog
#消費隊列存儲路徑存儲路徑
storePathConsumeQueue=/usr/local/java/rocketmq/store/consumequeue
#消息索引存儲路徑
storePathIndex=/usr/local/java/rocketmq/store/index
刷盤機制
- 簡介
RocketMQ的消息是存儲到磁盤上的,這樣既能保證斷電後恢復,又可以讓存儲的消息量超出內存的限制。RocketMQ爲了提高性能,會盡可能地保證磁盤的順序寫。消息在通過Producer寫入RocketMQ的時候,有兩種寫磁盤方式,分佈式同步刷盤和異步刷盤。
- 同步刷盤
在返回寫成功狀態時,消息已經被寫入磁盤。具體流程是,消息寫入內存的PAGECACHE後,立刻通知刷盤線程刷盤,然後等待刷盤完成,刷盤線程執行完成後喚醒等待的線程,返回消息寫成功的狀態。
- 異步刷盤
在返回寫成功狀態時,消息可能只是被寫入了內存的PAGECACHE,寫操作的返回快,吞吐量大;當內存裏的消息量積累到一定程度時,統一觸發寫磁盤動作,快速寫入。
- 配置
同步刷盤還是異步刷盤,都是通過Broker配置文件裏的flushDiskType參數設置的,這個參數被配置成SYNC_FLUSH、ASYNC_FLUSH中的一個。
#刷盤方式
#- ASYNC_FLUSH 異步刷盤
#- SYNC_FLUSH 同步刷盤
flushDiskType=SYNC_FLUSH
高可用性機制
- RocketMQ集羣架構圖
- Master和Slave配合達到高可用
RocketMQ分佈式集羣是通過Master和Slave的配合達到高可用性的。
Master和Slave的區別:
- 在Broker的配置文件中,參數brokerId的值爲0表明這個Broker是Master,大於0表明這個Broker是Slave,同時brokerRole參數也會說明這個Broker是Master還是Slave。
- Master角色的Broker支持讀和寫,Slave角色的Broker僅支持讀,也就是Producer只能和Master角色的Broker連接寫入消息;Consumer可以連接Master角色的Broker,也可以連接Slave角色的Broker來讀取消息。
#- ASYNC_MASTER 異步複製Master
#- SYNC_MASTER 同步雙寫Master
#- SLAVE
brokerRole=SLAVE
消息消費高可用
- 自動切換機制
在Consumer的配置文件中,並不需要設置是從Master讀還是從Slave 讀,當Master不可用或者繁忙的時候,Consumer會被自動切換到從Slave讀。有了自動切換Consumer這種機制,當一個Master角色的機器出現故障後,Consumer仍然可以從Slave讀取消息,不影響Consumer程序。
消息發送高可用
- Broker組
在創建Topic的時候,把Topic的多個Message Queue創建在多個Broker組上(相同 Broker名稱,不同 brokerId的機器組成一個Broker組),這樣當一個Broker組的 Master不可 用後,其他組的Master仍然可用,Producer仍然可以發送消息。
RocketMQ目前還不支持把Slave自動轉成Master,如果機器資源不足,需要把Slave轉成Master,則要手動停止Slave角色Broker,更改配置文件,用新的配置文件啓動Broker。
消息主從複製
- 複製方式
如果一個Broker組有Master和Slave,消息需要從Master複製到Slave 上,有同步和異步兩種複製方式。
- 同步複製
同步複製方式是等Master和Slave均寫成功後才反饋給客戶端寫成功狀態。在同步複製方式下,如果Master出故障, Slave上有全部的備份數據,容易恢復,但是同步複製會增大數據寫入延遲,降低系統吞吐量。
- 異步複製
異步複製方式是隻要Master寫成功即可反饋給客戶端寫成功狀態。在異步複製方式下,系統擁有較低的延遲和較高的吞吐量,但是如果Master出了故障,有些數據因爲沒有被寫入Slave,有可能會丟失;
- 配置
同步複製和異步複製是通過Broker配置文件裏的brokerRole參數進行設置的,這個參數可以被設置成ASYNC_MASTER、 SYNC_MASTER、SLAVE三個值中的一個
- 總結
異步刷盤保證吞吐量,主從複製保證數據不丟失
實際應用中要結合業務場景,合理設置刷盤方式和主從複製方式, 尤其是SYNC_FLUSH 方式,由於頻繁地觸發磁盤寫動作,會明顯降低性能。通常情況下,應該把Master和Slave配置成ASYNC_FLUSH的刷盤方式,主從之間配置成SYNC_MASTER的複製方式,這樣即使有一臺機器出故障,仍然能保證數據不丟,是個不錯的選擇。
負載均衡
Producer負載均衡
-
概念
Producer端,每個實例在發消息的時候,默認會輪詢所有的message queue發送,以達到讓消息平均落在不同的queue上。而由於queue可以散落在不同的broker,所以消息 就發送到不同的broker下
-
樣例圖
Consumer負載均衡
- 集羣模式
在集羣消費模式下,每條消息只需要投遞到訂閱這個topic的Consumer Group下的一個實例即可。RocketMQ採用主動拉取的方式拉取並消費消息,在拉取的時候需要明確指定拉取哪一條message queue。而每當實例的數量有變更,都會觸發一次所有實例的負載均衡,這時候會按照queue的數量和實例的數量平均分配queue給每個實例。
- AllocateMessageQueueAveragely算法(默認)
- AllocateMessageQueueAveragelyByCircle算法
- 廣播模式
由於廣播模式下要求一條消息需要投遞到一個消費組下面所有的消費者實例,所以也就沒有消息被分攤消費的說法。 在實現上,就是在consumer分配queue的時候,所有consumer都分到所有的queue。
- 注意事項
- 集羣模式下,queue都是隻允許分配只一個實例。這是由於如果多個實例同時消費一個queue的消息,由於拉取哪些消息是consumer主動控制的,那樣會導致同一個消息在不同的實例下被消費多次,所以算法上都是一個queue只分給一個 consumer實例,一個consumer實例可以允許同時分到不同的queue。
- 集羣模式下,需要控制讓queue的總數量大於等於consumer的數量。通過增加consumer實例去分攤queue的消費,可以起到水平擴展的消費能力的作用。而有實例下線的時候,會重新觸發負載均衡,這時候原來分配到的queue將分配到其他實例上繼續消費。 但是如consumer實例的數量比message queue的總數量還多的話,多出來的 consumer實例將無法分到queue,也就無法消費到消息,也就無法起到分攤負載的作用了
消息重試
順序消息的重試
- 概念
對於順序消息,當消費者消費消息失敗後,消息隊列 RocketMQ 會自動不斷進行消息重試(每次間隔時間爲 1 秒),這時,應用會出現消息消費被阻塞的情況。因此,在使用順序消息時,務必保證應用能夠及時監控並處理消費失敗的情況,避免阻塞現象的發生。
無序消息的重試
- 概念
對於無序消息(普通、定時、延時、事務消息),當消費者消費消息失敗時,您可以通過設置返回狀態達到消息重試的結果。
無序消息的重試只針對集羣消費方式生效;廣播方式不提供失敗重試特性,即消費失敗後,失敗消息不再重試,繼續消費新的消息。
- 重試次數
一條消息無論重試多少次,這些重試消息的MessageID不會改變
- 配置方式
1、配置重試方式
集羣消費方式下,消息消費失敗後期望消息重試,需要在消息監聽器接口的實現中明確進行配置(三種方式任選一種)
2、配置消息不重試方式
集羣消費方式下,消息失敗後期望消息不重試,需要捕獲消費邏輯中可能拋出的異常,最終返回Action.CommitMessage,此後這條消息將不會在重試
3、自定義消息最大重試次數
4、獲取消息重試次數
死信隊列
- 概念
當一條消息初次消費失敗,消息隊列 RocketMQ 會自動進行消息重試;達到最大重試次數後,若消費依然失敗,則表明消費者在正常情況下無法正確地消費該消息,此時,消息隊列 RocketMQ 不會立刻將消息丟棄,而是將其發送到該消費者對應的特殊隊列中。
在消息隊列 RocketMQ 中,這種正常情況下無法被消費的消息稱爲死信消息(Dead-Letter Message),存儲死信消息的特殊隊列稱爲死信隊列(Dead-Letter Queue)。
死信特性
- 死信消息特性
- 不會再被消費者正常消費
- 有效期與正常消息相同,均爲3天,3天后會被自動刪除。
- 死信隊列具有以下特性
- 一個死信隊列對應一個Group ID,而不是對應單個消費者實例
- 如果一個Group ID未產生死信消息,消息隊列RocketMQ不會爲其創建相應的死信隊列
- 一個死信隊列包含了對應Group ID產生的所有死信消息,不論該消息屬於哪個Topic
查看死信信息
- 控制檯查詢出現死信隊列主題信息
- 消息界面根據主題查詢死信消息
- 選擇重新發送消息
一條消息進入死信隊列,意味着某些因素導致消息者無法正常消費該消息,因此,通常需要對其進行特殊處理。排查可疑因素並解決問題後,可以在消息隊列RocketMQ控制檯重新發送該消息,讓消費者重新消費一次
消費冪等
- 概念
消息隊列RocketMQ消費者在接收到消息以後,有必要根據業務上的唯一key對消息做冪等處理的必要性
- 消息冪等必要性
處理方式