RocketMQ之Producer篇:可靠的消息传输

一、消息数据结构

RMQ的消息封装在org.apache.rocketmq.common.message类中,属性:

    private String topic;                   //消息所属topic
    private int flag;                       //消息flag
    private Map properties; //扩展属性
    private byte[] body;                    //消息体
    private String transactionId;

Message的基础属性包含消息所属的topic、消息的flag、扩展的属性、消息体等;

Message的Flag中定义的内容:

    public final static int COMPRESSED_FLAG = 0x1;
    public final static int MULTI_TAGS_FLAG = 0x1 << 1;
    public final static int TRANSACTION_NOT_TYPE = 0;
    public final static int TRANSACTION_PREPARED_TYPE = 0x1 << 2;
    public final static int TRANSACTION_COMMIT_TYPE = 0x2 << 2;
    public final static int TRANSACTION_ROLLBACK_TYPE = 0x3 << 2;

二、生产者启动流程

DefaultMQProducerImpl是默认的消息生产者实现类,它实现MQAdmin的接口,了解消息生产者就可以直接从该类入手;
消息生产者的启动,我们可以从DefaultMQProducerlmpl的start方法跟踪:
RocketMQ之Producer篇:可靠的消息传输_第1张图片

step1:检查productGroup是否符合要求;并改变生产者的instanceName为进程ID

    this .checkConfig();
    if (!this.defaultMQProducer.getProducerGroup().equals(
        MixAll.CL ENT INNER PRODUCER GROUP)) {
            this.defaultMQProducer.changeinstanceNameToPID();
    }    

step2:创建MQClientInstance实例。整个JVM实例中只存在一个MQClientManager实例,维护一个MQClientInstance缓存表ConcurrentMap factoryTable,也就是同一个clientId只会创建一个MQClientInstance。

    this.mQClientFactory = MQClientManager.getinstance().
    getAndCreateMQClientinstance(this.defaultMQProducer, rpcHook); 
    public MQClientInstance getAndCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) {
        String clientId = clientConfig.buildMQClientId();
        MQClientInstance instance = this.factoryTable.get(clientId);
        if (null == instance) {
            instance =
                new MQClientInstance(clientConfig.cloneClientConfig(),
                    this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook);
            MQClientInstance prev = this.factoryTable.putIfAbsent(clientId, instance);
            if (prev != null) {
                instance = prev;
                log.warn("Returned Previous MQClientInstance for clientId:[{}]", clientId);
            } else {
                log.info("Created new MQClientInstance for clientId:[{}]", clientId);
            }
        }
        return instance;
    }

step3:向MQClientInstance注册,将当前生产者加入到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);
    }

step4:启动MQClientInstance,如果MQClientInstance已经启动,则本次启动不会真正执行。

三、消息发送流程

RocketMQ之Producer篇:可靠的消息传输_第2张图片

同样的,消息的发送我们也可以从DefaultProducerImpl的send方法入手,

3.1、消息长度验证

验证消息是否符合规范,包括topicName、body不能为空、length不能为0且最大为410241024。

     public static void checkMessage(Message msg, DefaultMQProducer defaultMQProducer)
        throws MQClientException {
        if (null == msg) {
            throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message is null");
        }
        // topic
        Validators.checkTopic(msg.getTopic());

        // body
        if (null == msg.getBody()) {
            throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body is null");
        }

        if (0 == msg.getBody().length) {
            throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body length is zero");
        }

        if (msg.getBody().length > defaultMQProducer.getMaxMessageSize()) {
            throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL,
                "the message body size over max value, MAX: " + defaultMQProducer.getMaxMessageSize());
        }
    }

3.2、查找主题路由信息

获取主题的路由信息,查找要发送的具体的Broker节点

    private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
        TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
        if (null == topicPublishInfo || !topicPublishInfo.ok()) {
            this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
            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;
        }
    }

生产者中如果缓存了topic的路由信息则直接返回,否则向Namesrv查询,再未查询到则尝试用默认主题createTopicKey去查询,都查询步到则会抛出异常。
Producer关于更新和维护路由缓存操作都在updateTopicRoutInfoFromNameServer方法中。

3.3、选择消息队列

根据路由信息选择消息队列,返回的消息体按照broker、序号排序。
首先采用重试机制,由retryTimesWhenSendFailed指定同步方式重试次数,异步由retryTimesWhenSendAsyncFailed指定,然后使用循环执行的方式,选择消息队列 、发送消息,发送成功则返回,收到异常则重试。
选择消息队列有2种方式:

  1. sendLatencyFaultEnable=false ,默认不启用 Broker 故障延迟机制,调用 TopicPublishlnfo的slectOneMessageQueue
    public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
        if (lastBrokerName == null) {
            return selectOneMessageQueue();
        } else {
            int index = this.sendWhichQueue.getAndIncrement();
            for (int i = 0; i < this.messageQueueList.size(); i++) {
                int pos = Math.abs(index++) % this.messageQueueList.size();
                if (pos < 0)
                    pos = 0;
                MessageQueue mq = this.messageQueueList.get(pos);
                if (!mq.getBrokerName().equals(lastBrokerName)) {
                    return mq;
                }
            }
            return selectOneMessageQueue();
        }
    }

    public MessageQueue selectOneMessageQueue() {
        int index = this.sendWhichQueue.getAndIncrement();
        int pos = Math.abs(index) % this.messageQueueList.size();
        if (pos < 0)
            pos = 0;
        return this.messageQueueList.get(pos);
    }
  1. sendLatencyFaultEnable=true ,启用 Broker 障延迟机制,调用MQFaultStrategy#selectOneMessageQueue
    public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
        if (this.sendLatencyFaultEnable) {
            try {
                int index = tpInfo.getSendWhichQueue().getAndIncrement();
                for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
                    int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
                    if (pos < 0)
                        pos = 0;
                    MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
                    if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
                        if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
                            return mq;
                    }
                }

                final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
                int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
                if (writeQueueNums > 0) {
                    final MessageQueue mq = tpInfo.selectOneMessageQueue();
                    if (notBestBroker != null) {
                        mq.setBrokerName(notBestBroker);
                        mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
                    }
                    return mq;
                } else {
                    latencyFaultTolerance.remove(notBestBroker);
                }
            } catch (Exception e) {
                log.error("Error occurred when selecting message queue", e);
            }

            return tpInfo.selectOneMessageQueue();
        }

        return tpInfo.selectOneMessageQueue(lastBrokerName);
    }

3.4、消息发送

消息发送的入口在DefaultMQProducerimpl的sendKernerlmpl方法中:

  1. 根据 MessageQueue 获取 Broker 的网络地址 如果 MQC!ientln tance
    broker ddrTable 禾缓存该 Broke 的信息,则从 NameServer 主动更新一 top ic 的路由信
    如果路由更新后还 找不到 Broker 信息,则抛出 MQCli entExc tio 口,提示 Bro er
    存在
    String brokerAddr = this.mQClientFactory.findBrokerAddressinPublish(mq.getBrokerN ne());
    if (null == brokerAddr) {
        tryToFindTopicPublishinfo(mq.getTopic());
        brokerAddr = this.mQClientFactory.findBrokerAddressinPublish
            (mq.getBrokerN ne ()); 
    }
  1. 为消息分配全局唯 ,如果消息 默认超 4K(compressMsgBodyOverHowmuch),
    会对消息体采用 zip 压缩,并设置消息的系统标记为 MessageSysFlag.CO 1PRESSED FLAG
    如果是事务 Prepared 消息,则设 消息的系统标记为 MessageSysF ag .TRANSACTION_
    PREPARED TYPE
    //for MessageBatch,ID has been set in the generating process
    if (!(msg instanceof MessageBatch)) {
        MessageClientIDSetter.setUniqID(msg);
    }

    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;
    }

  1. 如果 册了消息发送钩子函数, 则执行消息发送之前的增强逻辑 通过 DefaultMQProducerlmpl registerSendMessageHook 注册钩子处理类,并且可以注册多个 简单看下钩子处理类接口
    if (this.hasSendMessageHook()) {
        context = new SendMessageContext();
        context.setProducer(this);
        context.setProducerGroup(this.defaultMQProducer.getProducerGroup());
        context.setCommunicationMode(communicationMode);
        context.setBornHost(this.defaultMQProducer.getClientIP());
        context.setBrokerAddr(brokerAddr);
        context.setMessage(msg);
        context.setMq(mq);
        String isTrans = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
        if (isTrans != null && isTrans.equals("true")) {
            context.setMsgType(MessageType.Trans_Msg_Half);
        }

        if (msg.getProperty("__STARTDELIVERTIME") != null || msg.getProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL) != null) {
            context.setMsgType(MessageType.Delay_Msg);
        }
        this.executeSendMessageHookBefore(context);
    }
  1. 构建消息发送请求 主要包含如下重要信息:生产者组、主题名称、默认
    创建主题 Key 、该主题在单个 Broker 默认 队列数、队 ID (队列序号)、消息系统标
    ( MessageSysFlag 消息发 时间 、消息标记( RocketMQ 对消息中的 flag 不做任何处理
    供应用程序使用) 消息扩展属性 、消息重试次数、是否是批量消息等
    SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
    requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
    requestHeader.setTopic(msg.getTopic());
    requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey());
    requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums());
    requestHeader.setQueueId(mq.getQueueId());
    requestHeader.setSysFlag(sysFlag);
    requestHeader.setBornTimestamp(System.currentTimeMillis());
    requestHeader.setFlag(msg.getFlag());
    requestHeader.setProperties(MessageDecoder.messageProperties2String(msg.getProperties()));
    requestHeader.setReconsumeTimes(0);
    requestHeader.setUnitMode(this.isUnitMode());
    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);
        }
    }
  1. 根据 发送方式,同步、异步、单式进行网络传输,消息发送在MQClientAPIImpl的sendMessage()方法中
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();
        RemotingCommand request = null;
        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. 如果注册了消息发送钩子函数,执行 after 逻辑。注意,就算消息发送过程中发生
    RemotingException MQBrokerException InterruptedException时,该方法也会执行
    if (this.hasSendMessageHook()) {
        context.setException(e);
        this.executeSendMessageHookAfter(context);
    }

四、批量消息发送

批量消息是将同一主题的多条消息打包发送到消息服务端,减少网络消耗;然而,并非同一批次消息越多越好,判断依据是单挑消息的长度,如果长度过长则打包发送回影响其他线程发送消息的响应时间。
批量消息发送要解决怎样将这些消息编码以便服务端能正确解码出每条消息的消息内容:
RMQ编码多条消息的类是RemotingCommand类:

    private int code;                                   //请求命令编码,请求命令类型
    private LanguageCode language = LanguageCode.JAVA;  //指定Java
    private int version = 0;                            //版本号
    private int opaque = requestId.getAndIncrement();   //客户端请求序号
    private int flag = 0;                               //标记
    private String remark;                              //描述
    private HashMap extFields;          //扩展属性
    private transient CommandCustomHeader customHeader;//每个请求对应的请求头信息

批量消息的发送会调用batch方法,将一批消息封装成MessageBatch对象,messageBatch内部的List messages来存储消息,批量消息发送和单挑消息发送的流程是一样的,MessageBath会将集合中的每条消息的消息体聚合成一个byte[]数组,那么消息服务端能正确解析出消息就可。

    public static byte[] encodeMessage(Message message) {
        //only need flag, body, properties
        byte[] body = message.getBody();
        int bodyLen = body.length;
        String properties = messageProperties2String(message.getProperties());
        byte[] propertiesBytes = properties.getBytes(CHARSET_UTF8);
        //note properties length must not more than Short.MAX
        short propertiesLength = (short) propertiesBytes.length;
        int sysFlag = message.getFlag();
        int storeSize = 4 // 1 TOTALSIZE
            + 4 // 2 MAGICCOD
            + 4 // 3 BODYCRC
            + 4 // 4 FLAG
            + 4 + bodyLen // 4 BODY
            + 2 + propertiesLength;
        ByteBuffer byteBuffer = ByteBuffer.allocate(storeSize);
        // 1 TOTALSIZE
        byteBuffer.putInt(storeSize);

        // 2 MAGICCODE
        byteBuffer.putInt(0);

        // 3 BODYCRC
        byteBuffer.putInt(0);

        // 4 FLAG
        int flag = message.getFlag();
        byteBuffer.putInt(flag);

        // 5 BODY
        byteBuffer.putInt(bodyLen);
        byteBuffer.put(body);

        // 6 properties
        byteBuffer.putShort(propertiesLength);
        byteBuffer.put(propertiesBytes);

        return byteBuffer.array();
    }

你可能感兴趣的:(分布式,RocketMQ)