消息隊列——RocketMQ示例
1. 簡介
- 定位:分佈式消息中間件、消息隊列
- 語言:Java
- 性能:10萬級吞吐量,ms級時效性
- 可靠性:分佈式架構,可靠性非常高
- 其他:由阿里在2016年貢獻至Apache基金會,已成爲頂級項目。歷經雙十一考驗,能夠處理萬億級別的消息。
2. 集羣架構與工作流程
- 集羣架構示意圖(來自互聯網)
- 工作流程
- 首先,啓動NameServer集羣,負責管理Broker、Producer、Consumer的連接、負責管理Topic的元信息。(作用類似於Kafka中的ZooKeeper)
- 接着,啓動Broker集羣,註冊到NameServer集羣中,保持心跳。一個集羣可以由多個Broker組組成,一個Broker組(由BrokerName標識)包括Master、Slave(由BrokerId標識),Master負責接收生產的數據,Slave負責備份Master的數據。(同Kafka差異較大)
- 同步:數據生產到Master,並將數據同步到Slave後,才向生產端迴應Ack
- 異步:數據生產到Master後,馬上向生產端迴應Ack,不管Slave數據是否已經同步
- 創建一個Topic,Topic可以手動指定將數據存儲到哪些Broker,也可以自動分配。(沒有Topic的話,發送時也能自動創建)
- Producer生產數據時,會與NameServer集羣保持長連接,定期獲取對應Topic的信息,找到對應Master節點併發送數據。注意,Producer只能向Broker中的Master生產數據。
- Consumer消費數據時,會與NameServer集羣保持長連接,定期獲取對應Topic的信息,找到對應Master、Slave節點並獲取數據(包括Push、Pull)。注意,Consumer可以從Master或Slave消費數據(由Broker配置決定)。
- Push方式: 由Broker推消息到Consumer
- Pull方式: 由Consumer主動從Broker拉數據
- 消息順序:一個Broker內有多個MessageQueue,消息在一個MessagQueue內是有序的。(和Kafka類同,Kafka的消息在一個Partition內有序)
3. 簡單示例
- Maven依賴導包
<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.6.0</version> </dependency>
- 生產者 (同步、異步、單向)
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; /** * Description: RocketMQ 簡單的Producer示例 (同步、異步、單向) * <br/> * Date: 2020/1/6 17:28 * * @author ALion */ public class SimpleProducer { public static void main(String[] args) throws Exception { // 創建Producer // 實例化Producer對象,並指定生產者組名爲producer_name DefaultMQProducer producer = new DefaultMQProducer("producer_name"); // 指定NameServer集羣地址,多個用;分隔 producer.setNamesrvAddr("192.168.1.110:9876;192.168.1.111:9876;192.168.1.112:9876"); // 啓動Producer producer.start(); // 開始生產數據 for (int i = 0; i < 100; i++) { // 構建消息對象,包括Topic、Tag、MessageBody // Topic: 主題 // Tag: 一個主題下面可以分多個Tag。可以理解爲一個業務功能(Topic)下有多種消息,Tag用於對消息分類。 // MessageBody: 你要發送的消息,因爲網絡傳輸需要字節碼,所以要轉換一下 Message msg = new Message( "Topic_Test", "Tag_A", ("Hello RocketMQ " + i).getBytes() ); // 【同步發送方式】 // 發送消息到RocketMQ集羣 SendResult sendResult = producer.send(msg); // 打印結果 System.out.printf("%s%n", sendResult); // 【異步發送方式】 // producer.send(msg, new SendCallback() { // @Override // public void onSuccess(SendResult sendResult) { // // 發送成功時調用 // System.out.printf("%s%n", sendResult); // } // // @Override // public void onException(Throwable throwable) { // // 發送失敗時調用 // throwable.printStackTrace(); // } // }); // 【單向發送方式】(也是異步,但是不獲取響應) // producer.sendOneway(msg); } // 關閉Producer producer.shutdown(); } }
- 消費者 (Push方式、Pull方式)
import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer; 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.common.message.MessageExt; import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; import java.util.List; /** * Description: RocketMQ 簡單的Consumer示例 (Push方式、Pull方式) * <br/> * Date: 2020/1/6 17:58 * * @author ALion */ public class SimpleConsumer { public static void main(String[] args) throws Exception { // 創建Consumer // 實例化Consumer對象,並指定消費者組名爲push_consumer_name // 【Push方式】 由Broker推消息到Consumer DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer("push_consumer_name"); // 指定NameServer集羣地址,多個用;分隔 pushConsumer.setNamesrvAddr("192.168.1.110:9876;192.168.1.111:9876;192.168.1.112:9876"); // 設置訂閱的Topic與Tag // 訂閱多個Tag,用||分隔,例如 "Tag_A || Tag_B"、"*" pushConsumer.subscribe("Topic_Test", "Tag_A"); // 設定消費模式 CLUSTERING與BROADCASTING // CLUSTERING: 負載均衡(默認)。多個消費者消費時,分別消費所有消息的一部分。 // BROADCASTING: 廣播模式。多個消費者消費時,都全部消費。 // pushConsumer.setMessageModel(MessageModel.CLUSTERING); // 註冊一個回調監聽,用於接收消息 pushConsumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) { for (MessageExt messageExt : list) { // 接到的消息是字節碼,需要解碼 byte[] body = messageExt.getBody(); System.out.println(new String(body)); } // 回覆 Broker "消費成功" return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); // 啓動Consumer pushConsumer.start(); // 【Pull方式】 由Consumer主動從Broker拉數據 // DefaultLitePullConsumer pullConsumer = new DefaultLitePullConsumer("pull_consumer_name"); // pullConsumer.setNamesrvAddr("192.168.1.110:9876;192.168.1.111:9876;192.168.1.112:9876"); // pullConsumer.subscribe("Topic_Test", "Tag_A"); // while (true) { // List<MessageExt> messageExts = pullConsumer.poll(1000); // 超時時間1000ms // for (MessageExt messageExt : messageExts) { // byte[] body = messageExt.getBody(); // System.out.println(new String(body)); // } // } } }
4. 有序消息示例
- 原理:將需要保證順序的消息發送到同一MessageQueue,即可保證有序
- 生產者
import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.MessageQueueSelector; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageQueue; import java.util.List; /** * Description: RocketMQ 有序Producer示例 * <br/> * Date: 2020/1/6 17:28 * * @author ALion */ public class OrderedProducer { public static void main(String[] args) throws Exception { // 創建Producer DefaultMQProducer producer = new DefaultMQProducer("producer_name"); producer.setNamesrvAddr("192.168.1.110:9876;192.168.1.111:9876;192.168.1.112:9876"); producer.start(); // 開始生產數據 for (int i = 0; i < 100; i++) { // orderId 消息的唯一標識 // 用實際生產環境的用戶id等替代該值,能夠保證該用戶數據的有序性 int orderId = i % 10; Message msg = new Message( "Topic_Ordered", "Tag_A", "KEY" + i, // 標識唯一一條消息 ("Hello RocketMQ " + i).getBytes() ); // 發送消息到RocketMQ集羣 SendResult sendResult = producer.send( msg, // MessageQueueSelector用於決定消息發送到哪個隊列 new MessageQueueSelector() { @Override public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { // arg 即 orderId // 同一個orderId的數據應該發送到同一個MessageQueue int idx = (Integer) arg % mqs.size(); return mqs.get(idx); } }, orderId ); // 打印結果 System.out.printf("%s%n", sendResult); } // 關閉Producer producer.shutdown(); } }
- 消費者
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; import org.apache.rocketmq.common.message.MessageExt; import java.util.List; /** * Description: RocketMQ 有序Consumer示例 * <br/> * Date: 2020/1/6 19:35 * * @author ALion */ public class OrderedConsumer { public static void main(String[] args) throws MQClientException { // 創建Consumer DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name"); // 設置從最開始的位置消費,默認是最後的位置開始(CONSUME_FROM_LAST_OFFSET) consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); // 訂閱並監聽消息 consumer.subscribe("Topic_Ordered", "Tag_A"); // 注意,這裏需要傳 MessageListenerOrderly consumer.registerMessageListener(new MessageListenerOrderly() { @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { for (MessageExt messageExt : msgs) { System.out.println( Thread.currentThread().getName() + " Receive New Messages: " + new String(messageExt.getBody()) ); } return ConsumeOrderlyStatus.SUCCESS; } }); // 啓動Consumer consumer.start(); } }
5. 事務消息示例
- 事務機制示意圖(來自互聯網)
- 生產者生產消息到RocketMQ集羣
- RocketMQ集羣返回Ack,表示生產成功
- 生產者處理本地事務
- 事務的狀態
- 如果成功,提交 LocalTransactionState.COMMIT_MESSAGE
- 如果失敗,回滾 LocalTransactionState.ROLLBACK_MESSAGE
- 仍在處理中, 未知 LocalTransactionState.UNKNOW
- RocketMQ集羣在未收到COMMIT_MESSAGE或ROLLBACK_MESSAGE前,會定時回查生產者
- 確認消息事務狀態(提交、回滾、未知),直到狀態變爲提交或回滾
- 生產者
import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.client.producer.TransactionMQProducer; import org.apache.rocketmq.common.message.Message; /** * Description: RocketMQ 事務Producer示例 * <br/> * Date: 2020/1/6 19:51 * * @author ALion */ public class TransactionProducer { public static void main(String[] args) throws Exception { // 創建Producer TransactionMQProducer producer = new TransactionMQProducer("transaction_producer_name"); producer.setNamesrvAddr("192.168.1.110:9876;192.168.1.111:9876;192.168.1.112:9876"); // 設置事務監聽器,用於RocketMQ集羣來回查事務處理狀態 producer.setTransactionListener(new MyTransactionListener()); producer.start(); // 開始生產數據 for (int i = 0; i < 100; i++) { // i 能除盡5時,給一個亂碼,用於模擬事務失敗回滾 String suffix = i % 5 == 0 ? "!@#" : i + ""; String body = "Hello RocketMQ ," + suffix; Message msg = new Message( "Topic_Transaction", "Tag_A", body.getBytes() ); // 發送消息到RocketMQ集羣 // null表示對所有消息進行事務控制 SendResult sendResult = producer.sendMessageInTransaction(msg, null); // 打印結果 System.out.printf("%s%n", sendResult); } // 關閉Producer producer.shutdown(); } }
- 事務處理監聽器
import org.apache.rocketmq.client.producer.LocalTransactionState; import org.apache.rocketmq.client.producer.TransactionListener; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageExt; import java.util.concurrent.ConcurrentHashMap; /** * Description: 事務處理監聽器 * <br/> * Date: 2020/1/6 20:16 * * @author ALion */ public class MyTransactionListener implements TransactionListener { // 設置一個Map,用於標識每條消息的事務狀態 // value=0 表示處理失敗 // value=1 表示處理成功 // value=其他值 表示未知 private ConcurrentHashMap<String, Integer> transactionMap = new ConcurrentHashMap<>(); @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { // 處理事務 // 一開始數據還在處理,先設置一個其他值(例如3),表示未知 transactionMap.put(msg.getTransactionId(), 3); // 可以直接阻塞式處理 // 也可以開子線程(或線程池)進行異步處理 // 處理完後需要更新transactionMap中的狀態 new Thread(() -> doSomething(msg, arg)).start(); // 還在處理中,先返回未知狀態 return LocalTransactionState.UNKNOW; } private void doSomething(Message msg, Object arg) { String content = new String(msg.getBody()); String[] fields = content.split(","); try { String txt = fields[0]; int number = Integer.parseInt(fields[1]); System.out.println("txt = " + txt + ", number = " + number); // 解析成功,修改狀態爲1,事務提交 transactionMap.put(msg.getTransactionId(), 1); } catch (NumberFormatException e) { e.printStackTrace(); // 解析失敗,修改狀態爲0,事務回滾 transactionMap.put(msg.getTransactionId(), 0); } } @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { // 如果executeLocalTransaction返回 LocalTransactionState.UNKNOW // MQ將會定時回查該方法,以確定事務狀態 Integer integer = transactionMap.get(msg.getTransactionId()); if (integer != null) { switch (integer) { case 0: return LocalTransactionState.COMMIT_MESSAGE; case 1: return LocalTransactionState.ROLLBACK_MESSAGE; default: return LocalTransactionState.UNKNOW; } } return LocalTransactionState.UNKNOW; } }