RocketMQ 应用入门
一、RocketMQ基本介绍
阿里原来有一个自研的MQ,kafka开源之后,参考kafka用java开发了MetaQ,所以很多设计思想和kafka有很多相似的地方,后面改名字叫RocketMQ,2012年开源。2017年9月成为了Apache的顶级项目。目前RocketMQ有两个版本一个是开源的社区版本,一个商业的云服务版本。RocketMQ在阿里内部应用非常广泛的,而且经历过双十一万亿级别的消息,TPS可以达到几十万。
二、RocketMQ架构
2.1、broker
RocketMQ的服务或者说一个进程,叫做broker,broker的作用就是存储和转发消息。RocketMQ单机能承受10万QPS。
为了提升broker的可用性(单点故障问题),以及提升服务器的性能实现负载均衡,通常都是集群部署。
这个和kafka 或者 redis cluster 一样,RocketMQ集群的每个broker 节点保存总数据的一部分,因此和实现横向扩展。和kafka一样为了提高可靠性防止数据丢失,每个broker 可以有自己的副本。默认情况下读写都发生在master上,不过和kafka不同的是RocketMQ的副本slave也可以参与读负载 slaveReadEnable=true
,但是默认brokerId=1的slave才会参与读负载,
2.2、topic
topic是用于消息按照主题划分,和kafka不同的是topic是一个逻辑的概念,消息不是按照topic划分存储的。
producer 把消息发向指定的topic,consumer订阅这个topic就可以收到对应的消息,和kafka一样,如果topic不存在会自动创建。
topic跟生产者和消费者都是多对多的关系, 一个生产者可以发送消息到多个topic,一个消费者可以订阅多个topic。
2.2.1、Tag
Tag:消息标签,用于在同一个主题下面区分不同类型的消息
2.3、NameServer
当不同的消息存储在不同的broker上,生产者和消费者对应broker的选择,或者路由选择是一个非常关键的问题。
NameServer作用
- 路由,生产者发一条消息,应该发到哪个broker上,消费者接收消息从哪个broker获取消息。
- 服务端增减,如果broker增减或者减少了,客户端怎么知道。
- 客户端增加,生产者或者消费者加入,怎么知道有哪些broker。
所以和分布式调用场景需要一个注册中心一样,在RocketMQ中需要一个角色来管理broker的信息。就像kafka中是用zk 来管理的一样。
RocketMQ是自己实现了一个注册服务,这个就是NameServer,它就是RocketMQ的路由中心,每个NameServer节点都保存着全量的路由信息,为了保证高可用,NameServer自身需要集群部署,有点像Euraka。
简单来说,就是broker会在NameServer上注册自己,生产者和消费者在NameServer中发现broker。
NameServer作为路由中心怎么工作?
每个broker在启动时候 ,都会根据配置遍历NameServer的列表,与每个NameServer建立TCP长链接,然后注册自己的信息,之后每隔30s发送心跳信息(主动注册),如何broker挂掉了,不发送心跳,所以除了主动注册之外,还要定时探活,每个NameServer每隔10s检查一下各个broker的最近一次心跳时间,如果发现某个broker超过120s都没有发送心跳,就认为这个broker挂了,会将其从路由信息里移除。
为什么不用zk?
实际上在早期的版本,服务管理也使用zk,和kafka一样,但是 在3.x去掉了zk的依赖,采用了自己的NameServer。
RocketMQ的架构设计决定只需要一个轻量级的元数据服务器就足够了,只需要保持最终一致性,而不需要zk这样的cp解决方案,不需要在依赖另一个中间件,从而减少整体维护成本。
NameServer如何保持一致?
NameServer之间互相不通信,没有主从之分。
- 服务注册:如果新增了broker,broker每隔30s会向所有的NameServer发送心跳信息,所以还是可以保持一致。
- 服务剔除:如果broker挂掉了,怎么从所有的NameServer中移除呢,NameServer定时任务每隔10s扫描broker列表,如果某个broker心跳的最新时间戳超过当前时间120s就会剔除。所以还是可以保持一致。
- 路由发现:如果broker增减,客户端怎么获取最新的broker呢?
- 生产者,发送第一条消息的时候,根据topic从NameServer获取路由信息
- 消费者,消费者一般是订阅固定的topic,在启动时就要获取broker信息
- 所以建立连接后,需要生产者和消费者定期更新路由信息(定时任务默认30s获取一次)
如果NameServer全部挂掉了,也没有关系,客户端肯定要缓存broker的信息,不能完全依赖NameServer。
2.4、producer
生产者发送消息,会定时从NameServer拉取路由信息,然后根据路由信息与指定的broker建立TCP 长链接,从而将消息发到broker上,发送逻辑一致的producer可以组成一个group。
同意支持批量发送,不过list 要自己传进去,写数据只能操作master节点。
2.5、consumer
所有消费者,通过NameServer获取topic路由信息,连接到对应的broker消费信息,消费逻辑一致的consumer可以组成一个group,这里和kafka逻辑一样,消息会在consumer之间负载。
由于master和slave都可以读消息,因此consumer会与master和slave都建立连接。
消费者由两种消费方式:
- 集群消息,消息轮询消费
- 广播消息,全部接收相同的信息
从消费模型来说,支持push和pull 两种模式
- pull:consumer轮询从broker拉取信息
- 普通的轮询(polling),不管服务器数据有无更新,客户端定长时间拉取一次数据,可能什么都没有。缺点:大部分时候没有数据,无效的请求大大的浪费服务器资源,而且定时请求时候,会导致消息延迟。
- 长轮询(long polling),客户端发起长轮询,如果此时服务端没有数据,会hold请求,直到服务端有数据,或者等待一定时间后超时才返回,返回会客户端又会立即再次发起下一次long polling。缺点:long polling 解决了轮询的问题,但是会造成服务器在挂起的时候比较耗内存。
- push:是broker推送消息给consumer,push模式实际上是基于pull模式实现的,pushconsumer会注册messageListener监听器,pull取到数据后唤醒messageListener的consumerMessage()方法来消费,感觉就像是推送过来的一样。
2.6、Message Queue
RocketMQ 支持多master的架构。这个就就是类似kafka的partition分片思想。
不过一个broker,RocketMQ 只有一个存储文件(commitlog),并不是kafka那样按照topic分开存储。那这样数据根据什么分布呢?
RocketMQ 里面设计了Message Queue的逻辑概念(作用和partition 类似)。在创建topic的时候会指定读写队列的数量, writeQueueNums、readQueueNums。
写队列的数量决定有几个Message Queue,读队列的数量 决定有几个线程来消费这些Message Queue(只是用来负载的)。
- topic:它是哪个topic的队列
- brokerName:它是哪个broker上
- queueId:在topic中的队列号,代表第几个分片
磁盘上看到的队列数量,是由写队列决定的,而且所有的master节点上的个数是一样的,但是存储的数据不一样。比如;集群中有两个master,topic中有2个写队列、一个读队列,那么两台集群的consumer queue目录会出现2个队列,一共四个队列。也就是总队列数= 写队列*节点数,而且读写队列数量最好一致,否则会出现消费不了的情况。
三、RocketMQ 开发
官方提供了java客户端的API;
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.3.0</version>
</dependency>
官方下载的源码example中有示例代码:
生产者代码示例
private static String topic;
private DefaultMQProducer producer;
private RocketMQProducerHelper(String nameServer, String topicName, String groupID) {
try {
if (nameServer != null && topicName != null && groupID != null) {
topic = topicName;
producer = new DefaultMQProducer(groupID);
producer.setRetryTimesWhenSendFailed(4);
producer.setRetryAnotherBrokerWhenNotStoreOK(true);
producer.setNamesrvAddr(nameServer);
producer.start();
LOG.info("producer started...");
} else {
LOG.error("parameter init error");
throw new Exception("parameter init error");
}
} catch (Exception e) {
LOG.error("producer init error...");
throw new RuntimeException(e);
}
}
public SendResult send(String topic, String tag, String key, byte[] data, final MessageQueueSelector selector) {
SendResult sendResult = null;
try {
Message msg;
if (tag == null || tag.length() == 0) {
msg = new Message(topic, data);
} else if (key == null || key.length() == 0) {
msg = new Message(topic, tag, data);
} else {
msg = new Message(topic, tag, key, data);
}
if (selector != null) {
sendResult = producer.send(msg, new MessageQueueSelector() {
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
return selector.select(mqs, msg, arg);
}
}, key);
} else {
sendResult = producer.send(msg);
}
} catch (Exception e) {
e.printStackTrace();
LOG.error("Send message error");
}
return sendResult;
}
代码解读:
- 多个发送同一类消息的生产者称之为一个生产者组。
- 生产者需要通过NameServer获取所有Broker的路由信息,多个用分号隔开。 跟Redis的哨兵一样,通过哨兵获取服务端地址。
- Message代表一条消息,必须指定 Topic,代表消息的分类,是一个逻辑概 念,比如订单类的消息、商品类的消息。
- Tags:可选,用于消费端过滤消息,是对消息用途的细分,比如Topic是订 单类的消息,tag可以是create,update,cancel。
- Keys:消息关键词。如果有多个,用空格隔开。RocketMQ可以根据这些 key快速检索到消息,相当于消息的索引。
- SendResult是发送结果的封装,包括消息状态、消息Id、选择的队列等等, 只要不抛异常,就代表发送是成功的。
消费者代码示例
import org.apache.hadoop.hbase.util.Bytes;
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 java.util.List;
public class Consumer {
public static void main(String[] args) throws MQClientException {
String nameServer = "localhost:9876";
String groupID = "groupid";
String topic = "test";
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupID);
consumer.setNamesrvAddr(nameServer);
consumer.subscribe(topic, "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
for(MessageExt msg : msgs) {
String result = Bytes.toString(msg.getBody());
System.out.println("--------------------msg="+result);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
}
}
代码解读:
- 消费者组是消费同一topic的消费者分组。
- 消费者需要从NameServer拿到topic的queue所在的Broker地址,多个用 分号隔开。
- Consumer可以以两种模式启动,广播(Broadcast)和集群(Cluster), 广播模式下,一条消息会发送给所有Consumer,集群模式下消息只会发送给一 个Consumer。
- 订阅可以使用通配符,topic名字后面的参数跟生产者消息的tag就对应起来 了,可以使用通配符,*代表所用;||隔开多个。
- return ConsumeConcurrentlyStatus.CONSUME_SUCCESS,就是发送ACK,告诉broker消费成功了,可以更新offset;
**3.1、SpringBoot集成RocketMQ **
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.4</version>
</dependency>
也很简单,第一、依赖pom;第二、写配置(yaml、config);第三步、写注解使用
生产者只要注入RocketMQTemplate
就可发送消息,有三种类型:
- 1、同步(syncSend方法),会在broker发回响应之后才发下一个msg。适用于msg很重要对响应时间不敏感的场景
- 2、异步(asyncSend方法),发送之后不等broker响应,接着发下一个msg,需要实现异步发送回调的接口SendCallback,通过回调接口接收服务器的响应,并对响应结果进行处理,适用于msg很重要且对响应时间非常敏感的场景
- 3、单向(sendOneWay方法),只负责发消息,不等服务器响应也没有回调,适用于对可靠性要求不高的场景比如日志收集
消费者只要加上@RocketMQMessageListener 注解监听消息
注解属性:
- selectorExpression 过滤消息
- MessageModel 消息模型,分别对应广播消息和集群消息
- consumeMode 消费模型,
- 分别是并发消费和有序消费
- 区别在于有序消费需要对处理的队列加锁,确保只允许一个消费线程处理,并发消费有多少线程则取决于配置的线程池大小