前言
本文主要側重點在 java 的客戶端,具體的如何搭建 RocketMQ 服務端不在本文討論範圍內
一、一些概念
1.1、rocketMQ 的組成
- name server 用來保存 Broker 相關 Topic 等元信息並給 Producer ,提供 Consumer 查找 Broker 信息。
- producer 消息生產者,負責產生消息
- consumer 消息消費者,負責消費消息
- push consumer:Consumer的一種,應用通常向Consumer對象註冊一個Listener接口,一旦收到消息,Consumer對象立刻回調Listener接口方法
- pull consumer :Consumer的一種,應用通常主動調用Consumer的拉消息方法從Broker拉消息,主動權由應用控制
- Producer Group:多個 producer 可以同屬於一個 Producer Group
- Consumer Group:多個consumer 可以同屬於一個 Consumer Group,同個分組中的消費者一定要訂閱相同的topic
- broker:消息中轉角色,負責存儲消息
1.2、消費方式
廣播消費和集羣消費
- 廣播消費:一條消息被多個Consumer消費,即使這些 Consumer屬於同一個 Consumer Group,消息也會被Consumer Group 中的每個 Consumer都消費一次。
- 集羣消費:一個Consumer Group中的Consumer實例平均分攤消費消息。例如某個Topic有9條消息,其中一個Consumer Group有3個 實例(可能是3個進程或3臺機器),那麼每個實例只消費其中的3條消息。
1.3、消費者發送消息的方式
- 同步可靠:客戶端阻塞,等待服務端響應結果,成功後,客戶端繼續運行
- 異步可靠:客戶端不阻塞,設置一個鉤子,待服務端響應成功後,調用鉤子
- 單向:不關心服務端的響應結果,直接發給服務端就完事了
1.4、消息類型
- 普通消息
- 順序消息
- 延時消息
- 批量消息
二、四種消息
2.1、普通消息的發送和消費
public class Test {
public static String PRODUCER_GROUP_NAME = "producer_group_1";
public static String CONSUMER_GROUP_NAME = "consumer_group_1";
public static String NAME_SERVER_HOST = "127.0.0.1:9876";
public static String TOPIC_NAME = "TEST_TOPIC";
public static void main(String[] args) throws Exception {
// 創建生產者實例
DefaultMQProducer producer = new DefaultMQProducer();
// 設置name server 信息
producer.setNamesrvAddr(NAME_SERVER_HOST);
// 設置 producer group
producer.setProducerGroup(PRODUCER_GROUP_NAME);
// 啓動生產者實例
producer.start();
// 啓動兩個消費者
openConsumer("xfz1");
openConsumer("xfz2");
// 循環發送消息
for (int i = 1; i < 40; i++) {
Message msg = new Message();
msg.setTopic(TOPIC_NAME);
msg.setTags("tag1");
msg.setBody(("消息:" + i).getBytes());
// 使用生產者實例發送消息
producer.send(msg);
}
// 銷燬生產者
producer.shutdown();
}
public static void openConsumer(String name) {
// 開啓一個消費者線程
new Thread(() -> {
// 創建消費者實例
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
// 設置name server
consumer.setNamesrvAddr(NAME_SERVER_HOST);
// 設置消費者組
consumer.setConsumerGroup(CONSUMER_GROUP_NAME);
// 設置消費者name
consumer.setInstanceName(name);
// 訂閱 topic
try {
consumer.subscribe(TOPIC_NAME, "tag1");
} catch (MQClientException e) {
e.printStackTrace();
}
// 設置鉤子
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for(MessageExt msg :msgs){
System.out.println("消費者:"+ consumer.getInstanceName()+" 消費了消息:"+new String(msg.getBody()));
}
// 睡眠500ms
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 啓動consumer
try {
consumer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}).start();
}
}
注意點:
- DefaultMQProducer、DefaultMQPushConsumer 都繼承了 ClientConfig ,就是說,這兩個類可以直接使用和設置 rocketMQ 的客戶端配置,在上面的例子中,我只設置了name server 的host
- 發送消息時需要設置消息的 topic、tag、body
- DefaultMQPushConsumer 在有消息時,會調用我們設置好的鉤子,即 MessageListenerConcurrently 的 consumeMessage方法
- Consumer 要設置實例名稱
程序結果:
2.2、有序消息
2.2.1、rocketMQ 是如何做到有序的?
要了解這個問題,需要明白
- producer 和 message、 broker、queue 的關係
- consumer 和 message、broker、queue 的關係
producer 和 message、 broker、queue 的關係
broker 是消息 queue 的提供者,而 broker 支持分佈式,即集羣部署。當一個生產者實例發出了一條消息時,需要先選擇一個 broker 的一個 queue。
consumer 和 message、broker、queue 的關係
而消費時,多個queue是同時拉取提交到消費者。
rocketMQ 如何做到有序
我們知道,同一個 queue 是可以做到 FIFO 的,如果我們想辦法把所有消息投遞到同一個 broker 的同一個 queue,自然消息就變成有序的了。但是,只是使用同一個 broker 的同一個 queue 就可以了嗎?並不是,因爲同一個 queue 的消息有可能被多個消費者的消費,而每個消費者又有可能有多個線程在消費,爲了保持 queue 中的消息順序消費,我們還需要保證同一時刻,只有一個消費者的一個消費線程在消費 queue。
消息置入同一個 queue 方案:rocketMQ 支持在生產者投遞消息時,設置隊列的選擇算法,即含有 MessageQueueSelector 的 send 方法。
保證同一時刻,只有一個消費者的一個消費線程在消費:使用 DefaultMQPushConsumer 的 MessageListenerOrderly 這個監聽器處理消息。
2.2.2、全局有序
所有消息全部進入同一個隊列,消費時,只有一個消費者的一個線程可以消費這個隊列的消息
生產者設置 MessageQueueSelector,選擇 queue
// 使用生產者實例發送消息
try {
// producer.send(msg);
producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
return mqs.get(0);
}
},null);
} catch (Exception e) {
e.printStackTrace();
}
這裏,select 返回的 queue,即爲消息即將投遞的 queue,我們直接把第一個 queue 返回,相當於所有的消息直接投遞到第一個 queue
消費者順序消費
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for (MessageExt msg : msgs) {
System.out.println("消費者:" + consumer.getInstanceName() + " 消費了消息:" + new String(msg.getBody()) + " 消費線程:" + Thread.currentThread().getName()+" 消息所在隊列id:"+msg.getQueueId());
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
全局有序demo代碼
public class Test3 {
public static String PRODUCER_GROUP_NAME = "producer_group_1";
public static String CONSUMER_GROUP_NAME = "consumer_group_1";
public static String NAME_SERVER_HOST = "127.0.0.1:9876";
public static String TOPIC_NAME = "TEST_TOPIC";
public static void main(String[] args) throws Exception {
// 啓動生產者
createProducer("prd1");
Thread.sleep(5000);
// 啓動兩個消費者
createConsumer("xfz1");
createConsumer("xfz2");
createConsumer("xfz3");
}
public static void createConsumer(String name) {
// 開啓一個消費者線程
new Thread(() -> {
// 創建消費者實例
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
// 設置name server
consumer.setNamesrvAddr(NAME_SERVER_HOST);
// 設置消費者組
consumer.setConsumerGroup(CONSUMER_GROUP_NAME);
// 設置消費者name
consumer.setInstanceName(name);
consumer.setConsumeThreadMax(1);
consumer.setConsumeThreadMin(1);
// 訂閱 topic
try {
consumer.subscribe(TOPIC_NAME, "tag1");
} catch (MQClientException e) {
e.printStackTrace();
}
// 設置鉤子
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for (MessageExt msg : msgs) {
System.out.println("消費者:" + consumer.getInstanceName() + " 消費了消息:" + new String(msg.getBody()) + " 消費線程:" + Thread.currentThread().getName()+" 消息所在隊列id:"+msg.getQueueId());
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 啓動consumer
try {
consumer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}).start();
}
public static void createProducer(String name) {
new Thread(() -> {
// 創建生產者實例
DefaultMQProducer producer = new DefaultMQProducer();
// 設置name server 信息
producer.setNamesrvAddr(NAME_SERVER_HOST);
// 設置 producer group
producer.setProducerGroup(PRODUCER_GROUP_NAME);
// 設置生產者名稱
producer.setInstanceName(name);
// 啓動生產者實例
try {
producer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
// 循環發送消息
for (int i = 1; i < 10000; i++) {
Message msg = new Message();
msg.setTopic(TOPIC_NAME);
msg.setTags("tag1");
msg.setBody(("消息" + i).getBytes());
// 使用生產者實例發送消息
try {
// producer.send(msg);
producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
if (((Integer)arg & 1) == 1) {
// 奇數
return mqs.get(0);
}else {
// 偶數
return mqs.get(1);
}
}
}, i);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
2.2.3、局部有序
全局有序是局部有序的一種特殊情況,全局有序是所有的消息全部進入同一個 queue,這樣所有的消息都會有序;而局部有序是某個場景下的一部分消息(比如id是偶數的消息,id小於1000的消息等等場景)進入同一個 queue,這樣就可以保證在同一個場景下(同一個 queue)中,消息是有序的。
下面寫一個demo,將 id 是奇數的消息和 id 是偶數的消息分別放入 queue,並消費
生產者設置 MessageQueueSelector,選擇 queue
奇數放入第一個 queue,偶數放入第二個 queue。
producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
if ((num & 1) == 1) {
// 奇數
return mqs.get(0);
}else {
// 偶數
return mqs.get(1);
}
}
}, null);
2.3、延時消息
RocketMQ 支持發送延遲消息,但不支持任意時間的延遲消息的設置,僅支持內置預設值的延遲時間間隔的延遲消息。
預設值的延遲時間間隔爲:1s、 5s、 10s、 30s、 1m、 2m、 3m、 4m、 5m、 6m、 7m、 8m、 9m、 10m、 20m、 30m、 1h、 2h
message 設置延時
msg.setDelayTimeLevel(3);
2.4、批量消息
批量發送消息可提高傳遞小消息的性能。同時也需要滿足以下特徵
- 批量消息要求必要具有同一topic、相同消息配置
- 不支持延時消息
- 建議一個批量消息最好不要超過1MB大小
代碼:
List<Message> msgs = new ArrayList<>();
for (int i = 1; i < 40; i++) {
Message msg = new Message();
msg.setTopic(TOPIC_NAME);
msg.setTags("tag1");
msg.setBody(("消息:" + i).getBytes());
msgs.add(msg);
}
// 使用生產者實例發送消息
producer.send(msgs);
三、最佳實踐
3.1、消息發送最佳實踐
3.1.1、一個應用儘可能用一個Topic
一個應用儘可能用一個Topic,消息子類型用tags來標識,tags可以由應用自由設置。只有發送消息設置了tags,消費方在訂閱消息時,纔可以利用tags在broker做消息過濾。
3.1.2、打印合適的消息日誌
消息發送成功或者失敗,要打印消息日誌,務必要打印sendresult和key字段。
3.1.3、發送成功的幾個狀態
send消息方法,只要不拋異常,就代表發送成功。但是發送成功會有多個狀態,在sendResult裏定義。
- SEND_OK
消息發送成功 - FLUSH_DISK_TIMEOUT
消息發送成功,但是服務器刷盤超時,消息已經進入服務器隊列,只有此時服務器宕機,消息纔會丟失 - FLUSH_SLAVE_TIMEOUT
消息發送成功,但是服務器同步到Slave時超時,消息已經進入服務器隊列,只有此時服務器宕機,消息纔會丟失 - SLAVE_NOT_AVAILABLE
消息發送成功,但是此時slave不可用,消息已經進入服務器隊列,只有此時服務器宕機,消息纔會丟失
3.1.4、消息發送的重試
3.1.4.1、只有同步發送纔會進行失敗重試,重新投遞消息
rocketMQ 有3種類型的發送
- 同步
投遞消息的線程阻塞,等待獲取投遞消息的結果,對應 send(Message msg) - 異步
投遞消息的線程不阻塞,等投遞結果返回時,調用設置好的鉤子,對應 send(Message msg, SendCallback sendCallback) - oneway
投遞消息的線程不阻塞,不關心投遞結果,對應 sendOneway(Message msg)
只有同步發送時,纔會在消息投遞失敗時,進行重試
3.2、消息消費最佳實踐
3.2.1、一個 consumer group 只訂閱一個 topic
rocketMQ 文檔中明確指出,強烈建議一個 consumer group 只訂閱一個 topic。
但是,能不能多個 consumer group 訂閱同一個 topic 呢?答案是可以的。
多個 consumer group 訂閱同一個 topic 時,Topic的一條消息會廣播給所有訂閱的ConsumerGroup,就是每個ConsumerGroup都會收,但是在一個ConsumerGroup內部給個Consumer是負載消費消息的,翻譯一下就是:一條消息在一個group內只會被一個Consumer消費
3.2.2、消費端需要做到冪等
rocketmq 無法避免消息的重複消費,消費端需要做冪等處理。處理方式:
- 1、增加唯一性約束,如果出現重複插入,則直接報錯,不進行處理即可
- 2、加鎖(悲觀鎖或者樂觀鎖)
比如,如果希望更新一個字段,這個時候可以加悲觀鎖:
直接在jvm 層面加鎖或者加分佈式鎖控制,或者在mysql 做select for update
也可以加樂觀鎖:使用MVCC