RocketMQ 作爲阿里開源的一款高性能,高吞吐量的分佈式消息中間件
-
特點:
- 支持Broker和Consumer端消息過濾
- 支持發佈訂閱模型,和點對點
- 支持pull和push‘兩種消息模式
- 單一隊列百萬消息,億級消息堆積
- 支持單master節點,多master節點 ,多master多slave節點
- 任意一點都是高可用,水平拓展,生產端和消費端,隊列都可以分佈式
- 消息失敗重試機制,支持特定level的定時消息
- 新版本底層採用Netty
- 4.3x版本支持分佈式事務
- 適合金融類業務,高可用性跟蹤和審計功能 -
基礎概念
- Producer:消息生產者
- Producer Group:消息生產者組,發送同類消息的一個消息生產者組
- Consumer:消費者
- Consumer Group:消費同類消息的多個實例
- Topic:主題,topic是邏輯管理單位,一個topic下可以有多個queue
- Tag:標籤,子主題,對topic的進一步細化,用於區分同一個主題下的不同業務的消息
- Message:消息,每個message必須指定一個queue
- Broker:MQ程序,接受生產的消息,提供給消費者消費的程序
- NameServer:給生產和消費者提供路由消息,提供輕量級的服務發現,路由,元數據信息,可以多個部署,互相獨立(類似zk,但比zk輕量級)
- Offset:偏移量,可以理解爲消息進度
- Commit log:消息存儲會寫在Commit Log文件裏面 -
心跳機制
- 單個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分鐘都沒有發心跳,則斷開連接。
一個簡單MQ例子:
依賴:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.3.0</version>
</dependency>
producer端簡單demo:
@Component
public class MQtestProducer {
private String producerGroup = "test1_gropup";
private String nameServerAddr = "127.0.0.1:9876";
private DefaultMQProducer producer;
public MQtestProducer(){
producer = new DefaultMQProducer(producerGroup);
producer.setNamesrvAddr(nameServerAddr);
start();
}
public void start(){
try {
this.producer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}
public DefaultMQProducer getProducer(){
return this.producer;
}
/**
* 一般在應用上下文,使用上下文監聽器,進行關閉
*/
public void shutdown(){
this.producer.shutdown();
}
}
Consumer端簡單demo:
public class MQtestConsumer {
private DefaultMQPushConsumer consumer;
private String consumerGroup = "test_consumer_group";
private String nameServerAddr = "127.0.0.1:9876";
private static final String topic = "mq_test_topic1";
public MQtestConsumer(){
try {
consumer = new DefaultMQPushConsumer(consumerGroup);
consumer.setNamesrvAddr(nameServerAddr);
consumer.setMessageModel(MessageModel.CLUSTERING);
//訂閱
consumer.subscribe(topic,"*");
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context)->{
try {
Message msg = msgs.get(0);
String topic = msg.getTopic();
String tags = msg.getTags();
String body = new String(msg.getBody());
System.out.println("topic:"+topic+"tags:"+tags+"body:"+body);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}catch (Exception e){
e.printStackTrace();
//todo 如果大於三次 就不在消費,消費失敗處理
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});
consumer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}
}
Controller層:
@RequestMapping("/api/test1")
public Object test1() throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
Message message = new Message(topic,"taga", ("hello wrold rocketmq = ").getBytes() );
SendResult sendResult = testproducer.getProducer().send(message);
System.out.println(sendResult);
return new HashMap<>();
}
消息隊列生產者核心配置:
-
compressMsgBodyOverHowmuch :消息超過默認字節4096後進行壓縮
-
retryTimesWhenSendFailed : 失敗重發次數
-
maxMessageSize : 最大消息配置,默認128k
-
topicQueueNums : 主題下面的隊列數量,默認是4
-
autoCreateTopicEnable : 是否自動創建主題Topic, 開發建議爲true,生產要爲false
-
defaultTopicQueueNums : 自動創建服務器不存在的topic,默認創建的隊列數
-
autoCreateSubscriptionGroup: 是否允許 Broker 自動創建訂閱組,建議線下開發開啓,線上關閉
-
brokerClusterName : 集羣名稱
-
brokerId : 0表示Master主節點 大於0表示從節點
-
brokerIP1 : Broker服務地址
-
brokerRole : broker角色 ASYNC_MASTER/ SYNC_MASTER/ SLAVE
-
deleteWhen : 每天執行刪除過期文件的時間,默認每天凌晨4點
-
flushDiskType :刷盤策略, 默認爲 ASYNC_FLUSH(異步刷盤), 另外是SYNC_FLUSH(同步刷盤)
-
listenPort : Broker監聽的端口號
-
mapedFileSizeCommitLog : 單個conmmitlog文件大小,默認是1GB
-
mapedFileSizeConsumeQueue:ConsumeQueue每個文件默認存30W條,可以根據項目調整
-
storePathRootDir : 存儲消息以及一些配置信息的根目錄 默認爲用戶的 ${HOME}/store
-
storePathCommitLog:commitlog存儲目錄默認爲${storePathRootDir}/commitlog
-
storePathIndex: 消息索引存儲路徑
-
syncFlushTimeout : 同步刷盤超時時間
-
diskMaxUsedSpaceRatio : 檢測可用的磁盤空間大小,超過後會寫入報錯
消息發送狀態
- 消息發送有同步和異步
-同步:SYNC
應用場景:重要通知郵件,報名短信通知等- 異步:ASYNC
應用場景:註冊成功後通知積分系統發佈優惠卷等 - 單向發送:ONEWAY 無需等待響應
應用場景 日誌收集,適用於某些耗時短,但可靠性不高的場景 - 對比
- 異步:ASYNC
發送方式 | 發送tps | 發送發送結果反饋反饋 | 可靠性 |
---|---|---|---|
同步發送 | 快 | 有 | 不丟失 |
異步發送 | 快 | 有 | 不丟失 |
單向發送 | 快 | 無 | 可能丟失 |
//同步:
SendResult sendResult = producer.send(msg);
//異步:
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
//todo 當成功
}
@Override
public void onException(Throwable e) {
//todo 當異常情況
}
});
//單向發送oneway:
SendResult sendResult = producer.sendOneway(msg);
- Broker 消息投遞狀態講解
- FLUSH_DISK_TIMEOUT
沒有在規定時間完成刷盤會出現這個錯誤
- FLUSH_SLAVE_AVALABLE
從模式下,broker是SYNC_MASTER,但是沒有找到被配置成Slave的Broker
- SEND_OK
發送成功,沒有發生上面的三種問題
消息重試及處理
-
producer重試:
- 消息重投
- 如果網絡比較差改多幾次 -
consumer重試:
- 原因:消息處理異常,broker端到consumer端問題,網絡原因,消費處理失敗等
- 注:重試間隔時間配置,默認每條消息最多重試16次messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
-超過重試次數人工補償
-一條消息的MessageID和key一直不變
-消費重試指針對集羣消費方式生效:廣播方式不提供失敗重試特性,即消費失敗後,失敗消息不再重試,繼續消費新消息
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context)->{
try {
Message msg = msgs.get(0);
String topic = msg.getTopic();
String tags = msg.getTags();
String body = new String(msg.getBody());
System.out.println("topic:"+topic+"tags:"+tags+"body:"+body);
if(true)throw new Exception();
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}catch (Exception e){
e.printStackTrace();
//todo 如果大於三次 就不在消費
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});
consumer.start();
順序消息
- 什麼是順序消息: 消費的生產 和消費順序一致,
- 順序發佈:對於指定的一個Topic,客戶端按照一定的先後順序發送消息
- 順序消費:對於指定的一個Topic,按照一定的先後順序接受消息,即先發送的消息一定會先被客戶端收到
- 順序消息不支持異步的發送方式,不支持廣播模式
生產者保證發送消息有序,且發送到同一個Topic的同個queue裏面,可以根據MessageQueueSelector裏面自定義策略,根據同個業務id放置到同個queue裏賣弄,如訂單號通過取模運算再放到selecto中,同一個模的值都會投遞到同一個queue
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Long id = (Long) arg;
long index = id % mqs.size();
return mqs.get((int)index);
}
-
消費端要在保證消費同個topic裏的同個隊列,不應該用MessageListenerConcurrently,應該使用MessageListenerOrderly,自帶單線程消費消息,不能在Consumer端再使用更多線程去消費,消費端分配到的queue數量是固定的,集羣會所鎖住當前消費的隊列集合的消息,所以保證順序消費
-
MessageListenerOrderly
Conusmer會平均分配queue數量,並不是簡單禁止併發處理,而是每個ConsumerQueue加個鎖,消費每個消費錢,需要獲得這個消息所在的Queue的鎖,
消息隊列消費者核心配置
- consumerFromWhere配置
- CONSUME_FROM_FIRSR_OFFSET: 初次從消息隊列頭部開始消費,即歷史消息全部消費一邊,後續再啓動接着上次笑得的進度開始消費
- CONSUME_FROM_LAST_OFFSET:默認策略,初次從該隊列尾開始消費,即跳過歷史消息,後續再啓動接着上次消費的進度開始消費
- CONSUME_FROM_TIMESTAMP:從某個時間點開始消費,默認是半個小時以前,後續啓動接着上次消費的嫉妒開始消費 - allocateMessageQueueStratrgy:
- 負載均衡策略算法,即消費者分配到queue的算法,默認值是AllocateMEssageQueueAveragely即取模平均分配
- LocalFileOffsetStore和RemoteBrokerOffsetStor 廣播模式默認使用LocalFIleOffsetStore集羣模式默認是用RemoteBrokerOffsetStore - consumerThreadMin:最小消費線程池數量
- consumeThreadMax:最大消費線程池數量
- pullBatchSize:消費者去broker拉去消息時,一次拉取多少條消息,可選配置
- consumeMessageBatchMaxSize:單次消費時一次性消費多少條消息,批量消費接口才有用,可選配置
- messageModel:消費模式,默認集羣模式–CLUSTERING 廣播模式-BROADCASRING
PullConsumer模式和PushConsumer模式
模式 | 優缺點 |
---|---|
push | 實時性高,但消費端能力不同,如果push推送過快,消費端會出現很多問題 |
pull | 消費者從broker拉取,主動權在消費端,可控性好,但是間隔時間不好設置,間隔太短,則空請求,間隔太長,則消息不能及時處理 |
- 長輪詢:Client請求broker時,broker會保持當前連接一段時間,默認15s,如果這段時間有消息到達,則立刻返回consumer,沒有消息的話,超過15s,則返回空,再進行重新請求,主動權再Consumer,broker即使有大量的雄安錫,也不會主動提送Consumer,缺點則是broker要保持consumer的請求,佔用資源
消息偏移量Offset
- message queue是無限長得數組,一條消息進來下標就會漲1,下標就是offset,消息在某個MessageQueue裏的位置,通過offset的值可以定位到這條消息,或者指示Consumer從這條消息開始向後處理
- Message Queue中的MaxOffset表示消息的最大offset,maxoffset並不是最新的那條消息的offset,而是最新消息的offset+1,minoffset則是現存在的最小offset
- 作用:主要是用來記錄消息的偏移量,集羣模式下采用RemoteBrokerOffsetStore,broker控制offset的值。廣播模式下采用LocalFileOffsetStore,消費端存儲
CommitLog
- CommitLog是消息文件的存儲地址,每個文件默認大小爲1G
- ConsumeQueue是邏輯隊列,而CommitLog是真正的存儲消息文件的,存儲的是指向物理存儲的地址
- Broker裏面有一個Topic,一個Topic裏面有多個MessageQueue,而一個MessageQueue對應一個ConsumeQueue,ConsumeQueue裏面記錄的是消息再CommitLog裏面的物理存儲地址
- 默認地址/store/consumequeue/{topicName}/fileName
RocketMQ高效原因–>ZeroCopy
- RocketMQ高效的原因:
- CommitLog順序寫,存儲了MessageBody,message key,tag等i西南西
- ConsumeQueue隨機讀+操作系統的PageCache + 零拷貝技術ZeroCopy
將文件讀取併發送傳統做法:
調用read,將文件拷貝到kernel內核態,cpu控制內核太的數據copy到用戶態,然後寫的時候,用戶態的數據先copy到socket的buffer中,最後將socket buffer的數據copy到網卡設備中,如圖:
-ZeroCopy 請求kernel直接把硬盤讀出的數據傳輸給socket,而不是再經過應用程序,zerocopy提高了性能,減少了不必要的內核緩衝區跟用戶緩衝區間的拷貝,從而減少了cpu的開銷,kernel和user模式的上下文切換,達到性能的提升
- RocketMQ分佈式事務
1 Producer向broker端發送消息
2 服務端接受到消息後,將消息持久化後,向發送方ack確認消息已經發送成功,此時消息是半消息狀態,
3 發送方開始執行本地事務邏輯
4 發送方根據本地事務執行結果向服務端提交二次確認(commit或者rollback),服務端收到conmit狀態則將半消息標記爲可投遞,訂閱方最終收到該消息,服務端收到rollback狀態則刪除半消息,訂閱方則不會接受該消息
5 斷網或者應用重啓得特殊狀態下,狀態4提交得二次確認狀態可能丟失,經過固定時間後,broker會對該消息進行回查
6 發送方收到消息回查後,需要檢查對應消息得本地事務執行得最終狀態
7 發送方根據檢查得到的本地事務的最終狀態再次提交二次確認,服務端仍然按照步驟4對半消息進行操作
- 半消息
暫時不能投遞的消息,producer已經將消息成功發送到broker端,但是broker沒有收到producer的二次確認,此消息被標記爲暫時不能投遞狀態,即爲半消息 - 消息狀態
COMMIT_MESSAGE:提交事務消息,消費者可以消費此消息
ROLLBACK_MESSAGE:回滾事務消息
UNKOW:broker需要回查確認消息的狀態
–未完待續