RocketMQ 应用入门

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 消费模型,
    • 分别是并发消费和有序消费
    • 区别在于有序消费需要对处理的队列加锁,确保只允许一个消费线程处理,并发消费有多少线程则取决于配置的线程池大小
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值