RocketMQ源码解析——Producer部分之消息发送过程DefaultMQProducer到DefaultMQProducerImpl(1)

发送消息入口DefaultMQProducer

前面说了RocketMQ消息生产者的启动逻辑,现在接着说对应的消息发送的逻辑。消息发送的入口在DefaultMQProducer类的send方法。这个方法有很多的重载方法对应的参数各不一样这里举例其中参数最多的方法进行说明

public void send(Collection<Message> msgs, MessageQueue mq, SendCallback sendCallback, long timeout)

方法的参数分别说明一下:

参数类型说明
msgsMessage消息对象
mqMessageQueue发送指定指定的queue
sendCallbackSendCallback发送回调方法
timeoutlong超时时间

消息发送实现DefaultMQProducerImpl

send方法对应的实现直接委托给了DefaultMQProducerImpl实现具体的细节。这个类中有两个默认的发送消息的实现,一个是同步的一个是异步的。两者的区别其实很简单,异步相对同步方法来说多了一个自定义回调实现。看一下两者对比

//异步实现
public void send(Message msg,
        SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException {
        send(msg, sendCallback, this.defaultMQProducer.getSendMsgTimeout());
    }
    
//同步实现   
public SendResult send(
        Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
		//默认消息超时时间3000毫秒,也就是5分钟
        return send(msg, this.defaultMQProducer.getSendMsgTimeout());
    }

但是两个方法的底层调用的还是同一个方法sendDefaultImpl。这个方法也是我们要重点分析的对象。先简单说一下方法的入参都是什么。

参数参数类型说明
msgMessage消息信息封装对象
communicationModeCommunicationMode消息发送类型,同步或者异步
sendCallbackSendCallback异步发送模式下的回调函数
timeoutlong消息发送超时时间
发送消息主方逻辑实现 sendDefaultImpl

这里只把正常逻辑的代码列出来,异常相关的逻辑忽略

private SendResult sendDefaultImpl(
        Message msg,
        final CommunicationMode communicationMode,
        final SendCallback sendCallback,
        final long timeout
    ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
		//确认客户端的状态
        this.makeSureStateOK();
		/**
		 * 消息校验,包含消息的topic校验和消息体的校验。
		 *  topic校验包含以下几点,topic的名字,长度以及是否为不准用的topic(SCHEDULE_TOPIC_XXXX)
		 *  消息体校验 消息体是不是空和消息体的长度
		 */

        Validators.checkMessage(msg, this.defaultMQProducer);
		//这次调用的id
        final long invokeID = random.nextLong();
        //开始时间戳
        long beginTimestampFirst = System.currentTimeMillis();
        long beginTimestampPrev = beginTimestampFirst;
        long endTimestamp = beginTimestampFirst;
        //从topic配置缓存中查找对应的已经发布的topic,这里可能会去nameServer去拉取配置
        TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
		//配置不为null,并且配置有效
        if (topicPublishInfo != null && topicPublishInfo.ok()) {
            boolean callTimeout = false;
            MessageQueue mq = null;
            Exception exception = null;
            SendResult sendResult = null;
            //交互模式是同步,那么失败重试的次数是默认的配置次数(默认为2)+1,如果是异步失败重试次数是1
            int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
            int times = 0;
            //记录重试时候发送消息目标Broker名字的数组
            String[] brokersSent = new String[timesTotal];
			//进行重试次数的循环发送消息逻辑
            for (; times < timesTotal; times++) {
            	//获取broker的名字,第一次为null,第二次为上次选择的broker名字
                String lastBrokerName = null == mq ? null : mq.getBrokerName();
                //根据topic配置选择一个MessageQueue
                MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
				//如果
                if (mqSelected != null) {
                    mq = mqSelected;
                    //添加选择的broker名称到已选集合中
                    brokersSent[times] = mq.getBrokerName();
                    try {
                        beginTimestampPrev = System.currentTimeMillis();
						//重试的次数之内,需要设置消息的topic
                        if (times > 0) {
                            //Reset topic with namespace during resend.
                            msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()));
                        }
                        long costTime = beginTimestampPrev - beginTimestampFirst;
						//这里的timeout,超时时间默认是3000毫秒
                        if (timeout < 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;
                        }

	//..........异常逻辑和相关日志...........//
}

这里对整个方法的逻辑步骤进行一个说明:

  1. 检查客户端状态(对应的生产者是否是运行状态),校验消息合法性,生成全局调用id(日志会用到)和相关的时间戳
  2. 根据消息的topic获取对应的消息路由配置信息,这里可能会去nameServer拉取对应的配置,稍后对这个方法进行一个简单的分析
  3. 根据不用的消息发送模式,计算消息失败重试的次数(同步模式是2+1,异步模式是1次。这里的2是生产端配置retryTimesWhenSendFailed的值)。
  4. 根据选择的BrokerName,选择一个消息的队列MessageQueue。这里选择队列的方法selectOneMessageQueue后面进行分析
  5. 选择号了队列之后,记录对应的发送BrokerName,在重试次数的范围内以及发送超时的限制时间之内进行消息发送,消息发送的逻辑在sendKernelImpl后面进行说明。还有一个容错策略先关逻辑方法updateFaultItem这里也后面进行说明
  6. 根据不同的消息发送模式进行处理,这里只有同步模式有处理逻辑,在同步逻辑发送失败的时候如果开启了retryAnotherBrokerWhenNotStoreOK(失败重试发送到另外的broker),则进行重试。这里的重试还是跟上面的重试有区别的,这里是调用内部发送实现并且有返回SendResult的情况,但是发送结果不是成功。

下面进行主流程内部一些方法的分析

根据topic获取消息发布信息tryToFindTopicPublishInfo
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
    	//从topic配置缓存中根据topic获取对应的信息topic配置信息
        TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
        if (null == topicPublishInfo || !topicPublishInfo.ok()) {
			//如果缓存没有则,创建一个然后保存到缓存中,
            this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
            //根据topic从nameserver中获取最新的topic配置,然后更新本地的缓存
            this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
			//获取对应的topic发布配置
            topicPublishInfo = this.topicPublishInfoTable.get(topic);
        }
        //包包含消息的路由配置,并且MessageQueue不是空,则直接返回
        if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
            return topicPublishInfo;
        } else {
            this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
            topicPublishInfo = this.topicPublishInfoTable.get(topic);
            return topicPublishInfo;
        }
    }

这个方法中关于updateTopicRouteInfoFromNameServer方法解析可以看上一篇的文章(Producer部分之生产者客户端MQClientInstance启动(2))末尾有分析。

关于selectOneMessageQueue方法,这个方法中包含了发送过程中的容错策略逻辑,这个方法还跟updateFaultItem也有紧密的管理,下篇文章进行分析。

发送消息前的准备sendKernelImpl

sendKernelImpl方法内其实主要是构建发送网络请求的请求头和一些钩子方法的实现。这里省略一部分代码逻辑。

private SendResult sendKernelImpl(final Message msg,
        final MessageQueue mq,
        final CommunicationMode communicationMode,
        final SendCallback sendCallback,
        final TopicPublishInfo topicPublishInfo,
        final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
        long beginStartTime = System.currentTimeMillis();
		//获取broker名称
        String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
		//缓存不存在,根据topic去nameserver获取
        if (null == brokerAddr) {
            tryToFindTopicPublishInfo(mq.getTopic());
            brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
        }

        SendMessageContext context = null;
        if (brokerAddr != null) {
        	//是否使用vip端口
            brokerAddr = MixAll.brokerVIPChannel(this.defaultMQProducer.isSendMessageWithVIPChannel(), brokerAddr);
            byte[] prevBody = msg.getBody();
            try {
				//批量消息在生成MessageBatch已经生成了唯一id,如果是单消息需要再生成一次
                //for MessageBatch,ID has been set in the generating process
                if (!(msg instanceof MessageBatch)) {
                    MessageClientIDSetter.setUniqID(msg);
                }
                boolean topicWithNamespace = false;
                //获取Client的nameSpace
                if (null != this.mQClientFactory.getClientConfig().getNamespace()) {
                    msg.setInstanceId(this.mQClientFactory.getClientConfig().getNamespace());
                    topicWithNamespace = true;
                }
                //消息标志
                int sysFlag = 0;
                boolean msgBodyCompressed = false;
                //是否对消息进行压缩,批量消息不支持压缩
                if (this.tryToCompressMessage(msg)) {
                	//加上标志
                    sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
                    msgBodyCompressed = true;
                }
                //获取消息的事务属性
                final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
				//如果是事务消息,则添加标记
                if (tranMsg != null && Boolean.parseBoolean(tranMsg)) {
                    sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;
                }
                //钩子方法调用
                if (hasCheckForbiddenHook()) {
                	//.....调用前的准备......//
                	this.executeCheckForbiddenHook(checkForbiddenContext);
                }
                 //消息发送钩子
                if (this.hasSendMessageHook()) {
               		 //.....调用前的准备......//
                	this.executeSendMessageHookBefore(context);
                }
                //构建消息请求头
                SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
                //.......构建请求头逻辑......//
                 requestHeader.setBatch(msg instanceof MessageBatch);


                //如果是重试类型的消息
                if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
					//获取消息重新消费的次数
                    String reconsumeTimes = MessageAccessor.getReconsumeTime(msg);
					//请求头带上重试次数
                    if (reconsumeTimes != null) {
                        requestHeader.setReconsumeTimes(Integer.valueOf(reconsumeTimes));
						//清除消息的内的重试次数属性
                        MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_RECONSUME_TIME);
                    }
                    //获取消息的最大重试次数
                    String maxReconsumeTimes = MessageAccessor.getMaxReconsumeTimes(msg);
					//请求头带上最大重试次数
                    if (maxReconsumeTimes != null) {
                        requestHeader.setMaxReconsumeTimes(Integer.valueOf(maxReconsumeTimes));
                        MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_MAX_RECONSUME_TIMES);
                    }
                }
				
				 SendResult sendResult = null;
                //消息的发送模式,异步或者同步
                switch (communicationMode) {
                	//异步
                    case ASYNC:
                        Message tmpMessage = msg;
                        boolean messageCloned = false;
                        if (msgBodyCompressed) {
                            //If msg body was compressed, msgbody should be reset using prevBody.
                            //Clone new message using commpressed message body and recover origin massage.
                            //Fix bug:https://github.com/apache/rocketmq-externals/issues/66
                            tmpMessage = MessageAccessor.cloneMessage(msg);
                            messageCloned = true;
                            msg.setBody(prevBody);
                        }
                        //如果配置存在Namespace则在消息前面加上Namespace
                        if (topicWithNamespace) {
                            if (!messageCloned) {
                                tmpMessage = MessageAccessor.cloneMessage(msg);
                                messageCloned = true;
                            }
                            msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQProducer.getNamespace()));
                        }

                        long costTimeAsync = System.currentTimeMillis() - beginStartTime;
                        if (timeout < costTimeAsync) {
                            throw new RemotingTooMuchRequestException("sendKernelImpl call timeout");
                        }
                        //异步的方式发送消息
                        sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
                            brokerAddr,
                            mq.getBrokerName(),
                            tmpMessage,
                            requestHeader,
                            timeout - costTimeAsync,
                            communicationMode,
                            sendCallback,
                            topicPublishInfo,
                            this.mQClientFactory,
                            this.defaultMQProducer.getRetryTimesWhenSendAsyncFailed(),
                            context,
                            this);
                        break;
                    case ONEWAY:
                    case SYNC:
                        long costTimeSync = System.currentTimeMillis() - beginStartTime;
                        if (timeout < costTimeSync) {
                            throw new RemotingTooMuchRequestException("sendKernelImpl call timeout");
                        }
                        //同步的方式发送消息
                        sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
                            brokerAddr,
                            mq.getBrokerName(),
                            msg,
                            requestHeader,
                            timeout - costTimeSync,
                            communicationMode,
                            context,
                            this);
                        break;
                    default:
                        assert false;
                        break;
                }
                //如果有发送的钩子方法,则进行调用
                if (this.hasSendMessageHook()) {
                    context.setSendResult(sendResult);
                    this.executeSendMessageHookAfter(context);
                }

                return sendResult;

		//......异常处理逻辑.......//
		}

 这个方法的逻辑其实很简单,内部没有执行消息发送的逻辑,只不过值是消息发送前的一些准备和一些外部扩展逻辑的扩展点。这里准备的点包含如下:

  1. 设置消息的唯一ID
  2. 设置消息的事务属性
  3. 如果是重试类型的消息,则设置重试的次数和最大重试次数到请求头,并清空消息的这两个属性

扩展点有如下:

  • CheckForbiddenHookcheckForbidden实现
  • SendMessageHooksendMessageBeforesendMessageAfter实现
发送消息的逻辑MQClientAPIImpl#sendMessage

发送消息的逻辑最后是通过MQClientAPIImpl来实现的,这个类之前在将MQClientInstance启动的时候提到过。这个类是MQ内部一些操作的api的实现,包括发送,消费消息和admin控制台的一些操作指令的实现,以及一些网络请求的处理。直接看最终实现逻辑

public SendResult sendMessage(
        final String addr,
        final String brokerName,
        final Message msg,
        final SendMessageRequestHeader requestHeader,
        final long timeoutMillis,
        final CommunicationMode communicationMode,
        final SendCallback sendCallback,
        final TopicPublishInfo topicPublishInfo,
        final MQClientInstance instance,
        final int retryTimesWhenSendFailed,
        final SendMessageContext context,
        final DefaultMQProducerImpl producer
    ) throws RemotingException, MQBrokerException, InterruptedException {
    	long beginStartTime = System.currentTimeMillis();
		//根据消息的类型和模式选择不同的请求code,然后封装到RemotingCommand
        RemotingCommand request = null;
        //获取消息的类型
        String msgType = msg.getProperty(MessageConst.PROPERTY_MESSAGE_TYPE);
        //如果是用于RPC模式的通信消息,则发对应的指令
        boolean isReply = msgType != null && msgType.equals(MixAll.REPLY_MESSAGE_FLAG);
        if (isReply) {
        	//是否对消息头进行字段的简化,就是用a,b,c这种字段名表示之前的消息头中的字段名
            if (sendSmartMsg) {
                SendMessageRequestHeaderV2 requestHeaderV2 = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader);
				//创建请求
                request = RemotingCommand.createRequestCommand(RequestCode.SEND_REPLY_MESSAGE_V2, requestHeaderV2);
            } else {
                request = RemotingCommand.createRequestCommand(RequestCode.SEND_REPLY_MESSAGE, requestHeader);
            }
        } else {
        	//正常的消息模式
            if (sendSmartMsg || msg instanceof MessageBatch) {
                SendMessageRequestHeaderV2 requestHeaderV2 = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader);
                request = RemotingCommand.createRequestCommand(msg instanceof MessageBatch ? RequestCode.SEND_BATCH_MESSAGE : RequestCode.SEND_MESSAGE_V2, requestHeaderV2);
            } else {
                request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader);
            }
        }
        request.setBody(msg.getBody());
        //消息的发送模式
        switch (communicationMode) {
        	//单途的
            case ONEWAY:
            	//进行消息发送,没有返回的
                this.remotingClient.invokeOneway(addr, request, timeoutMillis);
                return null;
			//异步方式
			case ASYNC:
                final AtomicInteger times = new AtomicInteger();
                long costTimeAsync = System.currentTimeMillis() - beginStartTime;
                if (timeoutMillis < costTimeAsync) {
                    throw new RemotingTooMuchRequestException("sendMessage call timeout");
                }
                this.sendMessageAsync(addr, brokerName, msg, timeoutMillis - costTimeAsync, request, sendCallback, topicPublishInfo, instance,
                    retryTimesWhenSendFailed, times, context, producer);
                return null;
           	//同步方式
            case SYNC:
                //消息准备的耗时
                long costTimeSync = System.currentTimeMillis() - beginStartTime;
				//如果超时了就抛错
                if (timeoutMillis < costTimeSync) {
                    throw new RemotingTooMuchRequestException("sendMessage call timeout");
                }
                return this.sendMessageSync(addr, brokerName, msg, timeoutMillis - costTimeSync, request);
            default:
                assert false;
                break;
        }

        return null;
    }

逻辑流程就是

  1. 根据消息的类型,是RPC的回复类型还是普通的类型构建不同的请求Code。以及是否发送简短消息(参数用a,b,c代替)来进行简化
  2. 根据同步或者异步的方式调用发送消息的API进行发送。这里面发送消息逻辑就是调用网络请求发送到Broker

这里只对同步发送的逻辑进行一个简单的讲解

	private SendResult sendMessageSync(
        final String addr,
        final String brokerName,
        final Message msg,
        final long timeoutMillis,
        final RemotingCommand request
    ) throws RemotingException, MQBrokerException, InterruptedException {
    	//同步发送的逻辑,这里就是通过对Netty客户端的调用来发送消息
        RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis);
        assert response != null;
        //返回信息的处理,包含事务消息相关事务id,消息的偏移量等的解析
        return this.processSendResponse(brokerName, msg, response,addr);
    }
	

invokeSync的逻辑在NettyRemotingClient类中这个类就是对Netty的一些扩展实现和内部逻辑的封装,作用就是网络请求的一些操作。这里不进行说明。只分析一下invokeSync

public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis)
        throws InterruptedException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException {
		//发送开始的时刻
        long beginStartTime = System.currentTimeMillis();
		//根据broker的地址,获取对应的netty的Channel
        final Channel channel = this.getAndCreateChannel(addr);
		//检查连接是否还是可用的
        if (channel != null && channel.isActive()) {
            try {
            	//调用前Rpc钩子方法调用
                doBeforeRpcHooks(addr, request);
				//调用前的耗时
                long costTime = System.currentTimeMillis() - beginStartTime;
				//如果超时了就报错
                if (timeoutMillis < costTime) {
                    throw new RemotingTimeoutException("invokeSync call timeout");
                }
				//调用netty进行消息发送
                RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis - costTime);
				//调用后的钩子方法调用
                doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response);
                return response;
          //.......异常处理逻辑、........//
        }
    }

整个消息在生产者这边的发送逻辑已经大概讲解完了,有一些具体的细节实现没有进行说明,因为细节比价多,可以自行查源码。后面会分析消息发送过程的容错相关,以及Broker端对消息的接收逻辑。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值