RocketMQ用法及原理解析(Producer篇)

一、基本用法

基本用法主要有同步发送,异步发送和指定队列发送,具体见下。这里主要介绍

// 1. 创建生产者对象,指定生产者group。生产者group的目的是当MQ需要回查事务状态时,会选择group中的任意一个producer进行会擦汗
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");

//2. 设置NameServer的地址
producer.setNamesrvAddr("localhost:9876;localhost:9878");

//3. 是否启用broker容错规避机制
// 默认不启用,如果启用,则发送消息失败时,会暂时规避发送失败的Broker
producer.setSendLatencyFaultEnable(true);

// 4.设置同步发送失败重试次数,默认是2次,这里重试的次数是在超时时间内,也就是说
// 无论重试多少次,总耗时都不会超过超时时间
producer.setRetryTimesWhenSendFailed(10);

// 5. 启动生产者,全局整个进程只有一个MQClientInstance实例,可以有多个生产消费者,每个生产消费者注册到MQClientInstance实例上
producer.start();

// 6. 创建一个消息对象,设置主题,tag和消息体
Message msg = new Message("topic1" /* Topic */,
    "TagA" /* Tag */,
    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);


//7.1 同步发送
SendResult sendResult = producer.send(msg);

//7.2 异步发送
  producer.send(msg, new SendCallback() {
    @Override
    public void onSuccess(SendResult sendResult) {
        System.out.printf("%s%n", sendResult);


    }
    // 当Broker返回错误时才会调用,超时不会
    @Override
    public void onException(Throwable e) {
        System.out.printf("%s%n", e);
    }
});

// 7.3 局部顺序消息,指定队列发送
producer.send(msg, new MessageQueueSelector() {
        @Override
        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
            MessageQueue q = mqs.get(0);
            System.out.println(q);
            return mqs.get(0);
        }
        // 这个i参数就是传给MessageQueueSelector接口的select方法的第三个参数
    }, i);

二、消息发送原理

消息发送主要包含以下几步:

  1. 启动,实例化MQClientInstance或将生产者注册到MQClientInstance。
  2. 查找主题路由信息。
  3. 从路由信息中,选择要发送的消息队列。
  4. 发送消息。

2.1 生产者启动

MQClientInstance一般全局只有一个,创建生产者时会将生产者加入到MQClientInstance中进行管理,以便后续调用网络请求,心跳检测等。

启动的关键代码在DefaultMQProducerImpl类中:
this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);

关键对象是MQClientInstance,它记录了主题路由信息,同时连接着通信客户端,进行网请求。

2.2 查找主题路由信息

2.2.1 几个关键的元数据:

每个消息发送者的每个主题都依赖一个元数据:TopicPublishInfo:

  1. TopicPublishInfo:
    属性:
    • orderTopic是否顺序消息
    • List messageQueueList 该主题队列的消息队列,MessageQueue有三个属性:topic,brokerName和queueId
    • sendWhichQueue,用于选择消息队列,每选择一次消息队列就自增一。
    • TopicRouteData 主题路由数据,主要包含:
      • List topic队列,一个topic可能有多个Broker,每个Broker有多个队列,每个队列是一个QueueData
      • List topic分布的broker元数据,一个BrokderData代表一个Broker

2.2.2 查找过程

  1. 使用主题查询NameSever,获取TopicRouteData对象,然后跟本地TopicRouteData比较,如果变更了,则向下执行。

  2. 更新MQClientInstance中的本地broker缓存,将TopicRouteData封装成TopicPublishInfo。

  3. 封装过程是遍历QueueData列表,找到所有可写的QueueData和合法的QueueData,创建MessageQueue,在topicRouteData2TopicPublishInfo方法中创建了messageQueueList。

  4. 然后更新MQClientInstance管理的生产者的TopicPublishInfo。

2.3 选择消息队列(MessageQueue)

  1. 根据TopicPublishInfo对象中的sendWhichQueue值从messageQueueList队列中选择一个MessageQueue,每选择一次消息队列加一。
  2. 选择消息队列采用重试机制,由retryTimesWhenSendFailed配置指定同步方式进行重试。每次失败后将选择下一个MessageQueue。
  3. 如果开启了Broker故障延迟机制,则将该Broker暂时排除在消息队列选择范围中。
  4. 当指定Selector时,就是从messageQueueList队列中选一个,这个是Selector中的一个参数

2.4 发送消息

发送消息有三种方式,同步发送,异步发送和单向发送。

发送消息的状态结果有4种,分别是:

  1. FLUSH_DISK_TIMEOUT。
  2. FLUSH_SLAVE_TIMEOUT.
  3. SLAVE_NOT_AVAILABLE.
  4. SEND_OK
    这四种状态需要结合配置的刷盘策略(同步刷盘,异步刷盘)和主从同步策略来分析。

同步和异步发送的区别:

  1. 同步发送时,需要阻塞等待netty中的 channel.writeAndFlush(request).addListener()方法中对ResponseFuture设置结果,
    而异步发送是在接收到服务端的响应后,在SimpleChannelInboundHandler类中的channelRead0方法中触发,EventLoop线程中执行回调函数或者在EventLoop线程中将任务放到一个ExecutorService中执行,不会同步等待结果。

  2. 同步发送重试会使用retryTimesWhenSendFailed进行重试,就算是服务端超时了也会重试,是在外层。 而异步重试必须是收到了的服务端的异常响应包才会重试,受到retryTimesWhenSendAsyncFailed参数影响。

三、关键源码

3.1 启动源码

启动主要是MQClientInstance对象的实例化和注册,MQClientInstance封装了RocketMQ网络处理API,是生产者消费者和NameServer,Broker打交道的网络通道。
DefaultMQProducerImpl类中的start方法

 public void start(final boolean startFactory) throws MQClientException {
        switch (this.serviceState) {
            case CREATE_JUST:
                this.serviceState = ServiceState.START_FAILED;

                this.checkConfig();

                if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
                    this.defaultMQProducer.changeInstanceNameToPID();
                }
                // 这里使用单例模式获得了MQClientInstance对象
                this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);
                // 同时将当前生产者注册到MQClientInstance对象中
                boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
                if (!registerOK) {
                    this.serviceState = ServiceState.CREATE_JUST;
                    throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup()
                        + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
                        null);
                }
                
               //....后面代码省略

3.2 查找主题路由元素

这一块是贯穿了整个发送过程中的关键代码,是DefaultMQProducerImpl类中的sendDefaultImpl方法

  // 一、查找主题路由信息
    TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
    if (topicPublishInfo != null && topicPublishInfo.ok()) {
        boolean callTimeout = false;
        MessageQueue mq = null;
        Exception exception = null;
        SendResult sendResult = null;
        // 注意这里,外层同步发送时会考虑getRetryTimesWhenSendFailed,异步时不会重试
        int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
        int times = 0;
        String[] brokersSent = new String[timesTotal];
        for (; times < timesTotal; times++) {
            System.out.println("同步重试第:{}此, 一共:{}次"  + times + "  "  + timesTotal);
            log.info("同步重试第:{}此, 一共:{}次", times, timesTotal);
            String lastBrokerName = null == mq ? null : mq.getBrokerName();
            // 二、选择消息队列
            MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
            if (mqSelected != null) {
                mq = mqSelected;
                brokersSent[times] = mq.getBrokerName();
                try {
                    beginTimestampPrev = System.currentTimeMillis();
                    if (times > 0) {
                        //Reset topic with namespace during resend.
                        msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()));
                    }
                    long costTime = beginTimestampPrev - beginTimestampFirst;
                    System.out.println("超时时间是"+ timeout);
                    // 如果重试后调用超时则直接break不再重试
                    if (timeout < costTime) {
                        System.out.println("已经超时了,耗时"+costTime);
                        callTimeout = true;
                        break;
                    }
                    // 三、执行消息发送
                    sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
                    endTimestamp = System.currentTimeMillis();
                    this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
                    // 四、处理返回结果
                    switch (communicationMode) {
                        case ASYNC:
                            return null;
                        case ONEWAY:
                            return null;
                        case SYNC:
                            if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
                                if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
                                    continue;
                                }
                            }

                            return sendResult;
                        default:
                            break;
                    }
                } catch (RemotingException e) {
                    endTimestamp = System.currentTimeMillis();
                    this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                    log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                    log.warn(msg.toString());
                    exception = e;
                    continue;
                } catch (MQClientException e) {
                // 后面代码省略.....

从上面代码可以看出,查找路由信息的方法是在tryToFindTopicPublishInfo中,它返回了TopicPublishInfo元数据。

private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
    // 1. 从缓存中获取
    TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
    // 2. 如果缓存中无有效路由信息,则从NameServer中获取
    if (null == topicPublishInfo || !topicPublishInfo.ok()) {
        this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
        // 3. 这个方法是由MQClientInstance去调用NameServer同时更新所有生产者和消费者的路由信息,这个过程需要上锁进行同步
        this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
        topicPublishInfo = this.topicPublishInfoTable.get(topic);
    }

    if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
        return topicPublishInfo;
    } else {
        this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
        topicPublishInfo = this.topicPublishInfoTable.get(topic);
        return topicPublishInfo;
    }
}

Namserver是有多个的,每次选一个,就算每个NameServer存的路由信息不一致也没关系。

3.3 选择消息队列

在3.2中我们可以看到这一段代码,可以看到,同步发送时这里用到了getRetryTimesWhenSendFailed来计算重试次数,异步时只执行一次。每次都调用selectOneMessageQueue来获取一个MessageQueue元数据,它代表一个Broker的一个队列。同时整体重试时间不超过timeout,如果超过直接break。

 // 注意这里,外层同步发送时会考虑getRetryTimesWhenSendFailed,异步时不会重试
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
String[] brokersSent = new String[timesTotal];
for (; times < timesTotal; times++) {
    log.info("同步重试第:{}此, 一共:{}次", times, timesTotal);
    String lastBrokerName = null == mq ? null : mq.getBrokerName();
    // 二、选择消息队列
    MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
    if (mqSelected != null) {
        mq = mqSelected;
        brokersSent[times] = mq.getBrokerName();
        try {
            beginTimestampPrev = System.currentTimeMillis();
            if (times > 0) {
                //Reset topic with namespace during resend.
                msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()));
            }
            long costTime = beginTimestampPrev - beginTimestampFirst;
            System.out.println("超时时间是"+ timeout);
            // 如果重试后调用超时则直接break不再重试
            if (timeout < costTime) {
                System.out.println("已经超时了,耗时"+costTime);
                callTimeout = true;
                break;
            }
    //.... 省略循环体后面的代码

这里selectOneMessageQueue有三种方式,一种是开启了Broker规避,一种是未开启。如果未开启则很简单,只是根据sendWhichQueue值从messageQueueList队列中选择一个MessageQueue,每选择一次消息队列加一,依次遍历所有的MessageQueue。
如果开始了Broker规避,则会跳过失败的Broker。

另外一种是直接用户根据参数指定选择某一个队列,也就是直接从messageQueueList选择一个队列进行消息发送。

为什么要规避:
Broker不可用后,路由信息短期内还是包含这个Broker的,因为

  1. 首先NameServer检测Broker是否可用是有间隔的,一般是10秒。
  2. NameServer检测到Broker挂了之后不会马上推送消息给客户端,而是客户端每隔30秒更新一次路由信息,也就是Broker挂了之后客户端最快要等30秒才能将这个Broker从客户端移除。
  3. 所以一次消息发送失败后,可以将Broker暂时排除在消息队列的选择范围中。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
RocketMQ是一款开源的分布式消息队列系统,它的工作原理如下: 1. Producer发送消息:Producer将消息发送到RocketMQ中,消息可以是单向发送、同步发送或异步发送。 2. NameServer注册:NameServer是RocketMQ的一个重要组件,用于管理Broker集群的路由信息。Producer在发送消息前需要向NameServer注册,获取Broker的路由信息。 3. Broker接收消息:Broker是RocketMQ的核心组件,用于存储和分发消息。当Broker接收到Producer发送的消息时,会将消息存储到磁盘中,并返回消息存储的结果。 4. Consumer订阅消息:Consumer在接收消息前需要向NameServer注册,获取Broker的路由信息,并订阅想要接收的消息。 5. Broker分发消息:当Consumer订阅了某个Topic的消息后,Broker会将该Topic的消息分发给对应的Consumer。 6. Consumer消费消息:Consumer接收到消息后,可以根据业务逻辑进行处理,处理完成后向Broker发送消费结果。 7. Broker更新消息状态:当Broker接收到Consumer发送的消费结果后,会更新消息的状态,标记该消息已被消费。 总的来说,RocketMQ的工作原理就是Producer向Broker发送消息,Broker存储和分发消息,Consumer订阅消息并消费消息。其中,NameServer用于管理Broker的路由信息,实现了Broker的动态扩展和负载均衡。RocketMQ具有高可靠性、高吞吐量和灵活的分布式部署等特点,适用于大规模分布式系统的消息通信。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值