消息队列——RocketMQ示例
- Docker安装:RocketMQ的Docker镜像部署
- Java代码:消息队列——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内有序)
- 存储:RocketMQ一个Broker节点上,消息数据实际存储在CommitLog文件中(默认超过1GB后,生成下一个CommitLog文件),一个CommitLog文件被多个ConsumeQueue共用。ConsumeQueue中不存实际的消息数据,只记录消息在CommitLog文件中的位置相关信息(offset/size/tagCode)。而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(); } }
- 生产者-带ACL认证
import org.apache.rocketmq.acl.common.AclClientRPCHook; import org.apache.rocketmq.acl.common.SessionCredentials; // ... DefaultMQProducer producer = new DefaultMQProducer( "groupB", new AclClientRPCHook(new SessionCredentials("RocketMQ", "12345678")) );
- 消费者 (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"); // pullConsumer.setAutoCommit(false); // pullConsumer.start(); // while (true) { // List<MessageExt> messageExts = pullConsumer.poll(1000); // 超时时间1000ms // for (MessageExt messageExt : messageExts) { // byte[] body = messageExt.getBody(); // System.out.println(new String(body)); // } // pullConsumer.commitSync(); } }
- 消费者-带ACL认证
import org.apache.rocketmq.acl.common.AclClientRPCHook; import org.apache.rocketmq.acl.common.SessionCredentials; import org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragely; // ... DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer( "push_consumer_name", new AclClientRPCHook(new SessionCredentials("rocketmq2","12345678")), new AllocateMessageQueueAveragely());
- 获取MQ上的Topic有哪些
MQClientAPIImpl mqClientAPIImpl = MQClientManager.getInstance() .getOrCreateMQClientInstance(pullConsumer.cloneClientConfig()) .getMQClientAPIImpl(); mqClientAPIImpl.start(); TopicList topicList = mqClientAPIImpl.getTopicListFromNameServer(1000);
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; } }