RocketMQ-Producer生产者解析


  • Producer 概念说明*
  • 初始化流程&流程图&相关类关系说明*
  • 消息发送过程*
  • 批量消息发送*
  • 发送顺序消息、延迟消息、事务消息*
  • 消息发送方式 同步、异步、单向区别和过程*
  • 消息发送如何进行负载*
  • 消息发送如何实现高可用*
  • 批量消息发送如何实现一致性*
  • 消息发送失败如何重试*
  • Producer 和 nameServ 通信机制*
  • Producer 和 broker 通信机制*
  • 消息发送异常机制*
  • Producer 配置讲解*

1 Producer 概念说明

本文讲解的是rocketmq中的生产者部分为了方便理解下图中带有颜色部分。


rocketmq cluster.png

producer: 消息生产者,主要作用用于发送消息。
producerGroup:
用来表示一个发送消息应用,一个Producer Group下包含多个Producer实例,可以是多台机器,也可以是一台机器的多个进程,或者一个进程的多个Producer对象。一个Producer Group可以发送多个Topic消息,
Producer Group作用如下:标识一类Producer 可以通过运维工具查询这个发送消息应用下有多个Producer实例,发送分布式事务消息时,如果Producer中途意外宕机,Broker会主动回调Producer Group内的任意一台机器来确认 事务状态
Topic:
标识一类消息的逻辑名字,消息的逻辑管理单位。无论消息生产还是消费,都需要指定Topic,建议一个应用一个topic,同时线上应该关闭程序自动创建topic的功能。
Tag:
RocketMQ支持给在发送的时候给topic打tag,同一个topic的消息虽然逻辑管理是一样的。但是消费topic1的时候,如果你订阅的时候指定的是tagA,那么tagB的消息将不会投递,建议同一个应用处理不同业务或场景使用。
Message Queue:
简称Queue或Q。消息物理管理单位。一个Topic将有若干个Q。若Topic同时创建在不通的Broker,则不同的broker上都有若干Q,消息将物理地存储落在不同Broker结点上,具有水平扩展的能力。
无论生产者还是消费者,实际的生产和消费都是针对Q级别。例如Producer发送消息的时候,会预先选择(默认轮询)好该Topic下面的某一条Q地发送;Consumer消费的时候也会负载均衡地分配若干个Q,只拉取对应Q的消息。每一条message queue均对应一个文件,这个文件存储了实际消息的索引信息。并且即使文件被删除,也能通过实际纯粹的消息文件(commit log)恢复回来。
Offset:
RocketMQ中,有很多offset的概念。但通常我们只关心暴露到客户端的offset。一般我们不特指的话,就是指逻辑Message Queue下面的offset。
注: 逻辑offset的概念在RocketMQ中字面意思实际上和真正的意思有一定差别,这点在设计上显得有点混乱。祥见下面的解释。
可以认为一条逻辑的message queue是无限长的数组。一条消息进来下标就会涨1,而这个数组的下标就是offset。
max offset:
字面上可以理解为这是标识message queue中的max offset表示消息的最大offset。但是从源码上看,这个offset实际上是最新消息的offset+1,即:下一条消息的offset。
min offset
标识现存在的最小offset。而由于消息存储一段时间后,消费会被物理地从磁盘删除,message queue的min offset也就对应增长。这意味着比min offset要小的那些消息已经不在broker上了,无法被消费。
consumer offset:
字面上,可以理解为标记Consumer Group在一条逻辑Message Queue上,消息消费到哪里即消费进度。但从源码上看,这个数值是消费过的最新消费的消息offset+1,即实际上表示的是下次拉取的offset位置
消费者拉取消息的时候需要指定offset,broker不主动推送消息, offset的消息返回给客户端。consumer刚启动的时候会获取持久化的consumer offset,用以决定从哪里开始消费,consumer以此发起第一次请求。
每次消息消费成功后,这个offset在会先更新到内存,而后定时持久化。在集群消费模式下,会同步持久化到broker,而在广播模式下,则会持久化到本地文件。

2 Producer 初始化流程&流程图&相关类关系说明


在发送消息之前我们先初始化相关类,这里主要用到的类是DefaultMQProducer

public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook,
    boolean enableMsgTrace, final String customizedTraceTopic) {
    this.namespace = namespace;
    this.producerGroup = producerGroup;
    defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
    //if client open the message trace feature
    if (enableMsgTrace) {
     ...       
    }
}

namespace:实例名称。
producerGroup:生产消息组。
rpcHook:注册一个发送钩子,在发送消息before和after需要处理的逻辑。
enableMsgTrace: 是否开启链路追踪。
customizedTraceTopic: 发送链路追踪Topic(默认值:RMQ_SYS_TRACE_TOPIC)
当然以上参数并非都是必选的,我们要根据实际情况选择不同参数。

3 Producer 启动流程&Shutdown流程&相关类关系说明

DefaultMQProducer.start() 启动一个消息生产者实例
DefaultMQProducer.shutdown() 关闭一个消息生产者实例
那我们先来看看启动一个生产者实例都做了哪些事情

@Override
public void start() throws MQClientException {
     //设置当前prdoucer group
    this.setProducerGroup(withNamespace(this.producerGroup));
    //调用 defaultMQProducerImpl start 方法
    this.defaultMQProducerImpl.start();
    if (null != traceDispatcher) { // 启动链路追踪
       ...
    }
}

this.defaultMQProducerImpl.start()

public void start() throws MQClientException {
    this.start(true);
}
public void start(final boolean startFactory) throws MQClientException {
    switch (this.serviceState) {
        case CREATE_JUST:
            ...
            break;
        case RUNNING:
        case START_FAILED:
        case SHUTDOWN_ALREADY:
            throw new MQClientException("The producer service state not OK, maybe started once, "
                + this.serviceState
                + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
                null);
        default:
            break;
    }
    this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
}

this.serviceState 状态默认是 ServiceState.CREATE_JUST, 那我们来看一下CREATE_JUST都做了什么事情

 ....
 this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);

boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
public boolean registerProducer(final String group, final DefaultMQProducerImpl producer) {
    ...     
 MQProducerInner prev = this.producerTable.putIfAbsent(group, producer);
    ...
}

获取一个MQClientInstance 如果没有则创建,然后把当前的producer放到MQClientInstance的内存producerTable 中.
private final ConcurrentMap producerTable = new ConcurrentHashMap();
producerTable对象里面存储producerGroupName和DefaultMQProducer的映射。key-value:

把当前producer 放入到 MQClientInstance table之后:

if (startFactory) { //
    mQClientFactory.start(); //mQClientFactory上边获取的MQClientInstance 实例
}

开始启动MQClientInstance

public void start() throws MQClientException {
    synchronized (this) {
        switch (this.serviceState) {
            case CREATE_JUST:
                .....
                break;
            case RUNNING:
                break;
            case SHUTDOWN_ALREADY:
                break;
            case START_FAILED:
                throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
            default:
                break;
        }
    }
}

感觉和之前 producer的start有点熟悉的感觉,细心的同学们可能发现了有synchronized 来做修饰,CREATE_JUST的状态做的事情

 ...
// 启动和netty远程通信
this.mQClientAPIImpl.start();
// 启动定时任务
this.startScheduledTask();
// 消费者使用线程
this.pullMessageService.start();
// 负载均衡线程
this.rebalanceService.start();
// 又调用 producer start  this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
 ...

上边说的定时任务都有哪些
(MQClientInstance说明一下,因为生产和消费者都会持有MQClientInstance所以在启动任务的时候会启动生产和消费相关的任务线程)

每两分钟执行一次寻址服务(NameServer地址)

if (null == this.clientConfig.getNamesrvAddr()) {
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            try {
MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr();
            } catch (Exception e) {
                log.error("ScheduledTask fetchNameServerAddr exception", e);
            }
        }
    }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
}

每30秒更新一次所有的topic的路由信息(topicRouteTable)

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        try {
MQClientInstance.this.updateTopicRouteInfoFromNameServer();
        } catch (Exception e) {
            log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", e);
        }
    }
}, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS);

每30秒移除离线的broker
每30秒发送一次心跳给所有的master broker

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        try {
            MQClientInstance.this.cleanOfflineBroker();
    MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
        } catch (Exception e) {
            log.error("ScheduledTask sendHeartbeatToAllBroker exception", e);
        }
    }
}, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS);

更新offset每5秒提交一次消费的offset,broker端为ConsumerOffsetManager负责记录,此offset是逻辑偏移量,比如说,consumerA@consumerAGroup 在broker_a的queue 0的消费队列共有10000条消息,目前消费到888,那么offset就是888.
因为producer和consumer内部都持有MQClientInstance实例,故MQClientInstance既有生产者逻辑,又有消费者逻辑

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

    @Override
    public void run() {
        try {
            MQClientInstance.this.persistAllConsumerOffset();
        } catch (Exception e) {
            log.error("ScheduledTask persistAllConsumerOffset exception", e);
        }
    }
}, 1000 * 10, this.clientConfig.getPersistConsumerOffsetInterval(), TimeUnit.MILLISECONDS);

每1分钟调整一次线程池,这也是针对消费者来说的,具体为如果消息堆积超过10W条,则调大线程池,最多64个线程;如果消息堆积少于8W条,则调小线程池,最少20的线程。

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

    @Override
    public void run() {
        try {
            MQClientInstance.this.adjustThreadPool();
        } catch (Exception e) {
            log.error("ScheduledTask adjustThreadPool exception", e);
        }
    }
}, 1, 1, TimeUnit.MINUTES);

以上就是producer start的流程,我们做个简单的总结:
1.获取当前的MQClientInstance实例,没有则新建
2.把当前的producer 注册到 MQClientInstance table 中
3.启动MQClientInstance,启动通信、定时()、消费线程(针对消费者)。

start 的事情做完了,那我们看看shutdwon的事情 ,shutdwon做的事情就是将之前start启动的线程停止掉和注册的实例删除掉
Producer

public void shutdown() {
    this.shutdown(true);
}
public void shutdown(final boolean shutdownFactory) {
    switch (this.serviceState) {
        case CREATE_JUST:
            break;
        case RUNNING:
            .....
            break;
        default:
            break;
    }
}

执行shutdown的时候,service 本身状态是RUNNING

this.mQClientFactory.unregisterProducer(this.defaultMQProducer.getProducerGroup()); //注销 producer
this.defaultAsyncSenderExecutor.shutdown(); // 停止发送消息线程池
if (shutdownFactory) {
    this.mQClientFactory.shutdown(); // 停止之前启动通信以及定时任务等
}
...

shutdown做的事情就是释放之前start启动的线程。

4 消息发送方式

同步
发送者向MQ执行发送消息API时,同步等待,直到消息服务器返回发送结(主要运用在比较重要一点消息传递/通知等业务)

异步
发送者向MQ执行发送消息API时,指定消息发送成功后的回掉函数,然后调用消息发送API后,立即返回,消息发送者线程不阻塞,直到运行结束,消息发送成功或失败的回调任务在一个新的线程中执行(通常用于对发送消息响应时间要求更高/更快的场景)

单向
消息发送者向MQ执行发送消息API时,直接返回,不等待消息服务器的结果,也不注册回调函数,简单地说,就是只管发,不在乎消息是否成功存储在消息服务器上(适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集)

发送前 (DefaultMQProducerImpl) 以上三种流程最后都会调用此方法

private SendResult sendDefaultImpl(
    Message msg,
    final CommunicationMode communicationMode,
    final SendCallback sendCallback,
    final long timeout
) 

1.路由topic 信息 TopicPublishInfo

    private boolean orderTopic = false;  是否是顺序消息
    private boolean haveTopicRouterInfo = false;
    private List messageQueueList = new ArrayList(); 该主题队列的消息队列
    private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); 每选择一次消息队列,该值会自增1,如果Integer.MAX_VALUE,则重置为0,用于选择消息队列
    private TopicRouteData topicRouteData;

2.根据TopicPublishInfo 选择 MessageQueue
根据路由信息选择消息队列,返回的消息队列按照broker、序号排序。举例说明,如果topicA在broker-a, broker-b上分别创建了4个队列,那么返回的消息队列:
[
{“broker-Name”: ”broker-a”, ”queueId”:0},
{“brokerName”: ”broker-a”, ”queueId”:1},
{“brokerName”:”broker-a”,”queueId”:2},
{“brokerName”:”broker-a”, ”queueId”:3},
{“brokerName”: ”broker-b”, ”queueId”:0},
{“brokerName”: ”broker-b”, ”queueId”:1},
{“brokerName”: ”broker-b”, ”queueId”:2},
{“brokerName”:”broker-b”, ”queueId”:3}
]
首先消息发送端采用重试机制,由retryTimesWhenSendFailed指定同步方式重试次数,异步重试机制在收到消息发送结构后执行回调之前进行重试。由retryTimes When Send-AsyncFailed指定,接下来就是循环执行,选择消息队列、发送消息,发送成功则返回,收到异常则重试。选择消息队列有两种方式。
1)sendLatencyFaultEnable=false,默认不启用Broker故障延迟机制。
2)sendLatencyFaultEnable=true,启用Broker故障延迟机制。
1.默认机制 sendLatencyFaultEnable=false,调用
TopicPublishInfo#selectOneMessageQueue

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

首先在一次消息发送过程中,可能会多次执行选择消息队列这个方法,lastBrokerName就是上一次选择的执行发送消息失败的Broker。第一次执行消息队列选择时,lastBrokerName为null,此时直接用sendWhichQueue自增再获取值,与当前路由表中消息队列个数取模,返回该位置的MessageQueue(selectOneMessageQueue()方法),如果消息发送再失败的话,下次进行消息队列选择时规避上次MesageQueue所在的Broker,否则还是很有可能再次失败。

2.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();
    }
}

首先对上述代码进行解读。
1)根据对消息队列进行轮询获取一个消息队列。
2)验证该消息队列是否可用,latencyFaultTolerance.isAvailable(mq.getBrokerName())是关键。3)如果返回的MessageQueue可用,移除latencyFaultTolerance关于该topic条目,表明该Broker故障已经恢复。

发送 消息发送API核心入口:DefaultMQProducerImpl#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 {
}

消息发送参数详解。
1)Message msg:待发送消息。
2)MessageQueue mq:消息将发送到该消息队列上。
3)CommunicationMode communicationMode:消息发送模式,SYNC、ASYNC、ONEWAY。
4)SendCallback sendCallback:异步消息回调函数。
5)TopicPublishInfo topicPublishInfo:主题路由信息
6)long timeout:消息发送超时时间。

DefaultMQProducerImpl#sendKernelImpl

String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
if (null == brokerAddr) {
    tryToFindTopicPublishInfo(mq.getTopic());
    brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
}

Step1:根据MessageQueue获取Broker的网络地址。如果MQClientInstance的brokerAddrTable未缓存该Broker的信息,则从NameServer主动更新一下topic的路由信息。如果路由更新后还是找不到Broker信息,则抛出MQClientException,提示Broker不存在

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

Step2 : 标记 sysFlag 是否压缩消息(大于4K),是否是事务性消息

if (hasCheckForbiddenHook()) {
    CheckForbiddenContext checkForbiddenContext = new CheckForbiddenContext();
    ....
}

if (this.hasSendMessageHook()) {
    context = new SendMessageContext();
    context.setProducer(this);
    ....
    this.executeSendMessageHookBefore(context);
}

Step3: 如果注册了消息发送钩子函数,则执行消息发送之前的增强逻辑。通过Defaul tMQProducerImpl#registerSendMessageHook注册钩子处理类,并且可以注册多个。

SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();

Step4 :构建消息发送请求包。主要包含如下重要信息:生产者组、主题名称、默认创建主题Key、该主题在单个Broker默认队列数、队列ID(队列序号)、消息系统标记(MessageSysFlag)、消息发送时间、消息标记(RocketMQ对消息中的flag不做任何处理,供应用程序使用)、消息扩展属性、消息重试次数、是否是批量消息等

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

Step5:根据消息发送方式,同步、异步、单向方式进行网络传输发送到broker

DefaultMQProducerImpl#sendKernelImpl

if (this.hasSendMessageHook()) {
    context.setSendResult(sendResult);
    this.executeSendMessageHookAfter(context);
}

Step6: 如果注册了消息发送钩子函数,执行after逻辑。注意,就算消息发送过程中发生RemotingException、MQBrokerException、InterruptedException时该方法也会执行。

在此整个发送消息的大概逻辑就结束了,那接下来我们一起总结一下整个发送消息的逻辑的。
1.获取发送topic信息,先从本地获取,如果没有从nameserver获取并更新到本地缓存,topic信息包括broker和发送mq队列列表的信息。
2.从topic信息里边拿到mq队列集合并选择一个进行发送,选择的方式有两种,第一种是轮询所有的队列进行负载均衡选择一个发送,另一种方式是如果broker开启了Broker故障延迟机制 ,则有效的规避一段时间不在进行当前broker选择队列。

故障延迟机制主要解决的问题应该是某个Broker单机的failover,或者是某个broker瞬时压力过大,导致接口超时,从而需要路由到别的broker进行消息发送。
3.发送前进行消息的标记是否压缩和是否是事务性消息构建发送消息体,进行网络传输。

5 批量消息发送

批量消息发送是将同一主题的多条消息一起打包发送到消息服务端,减少网络调用次数,提高网络传输效率。当然,并不是在同一批次中发送的消息数量越多性能就越好,其判断依据是单条消息的长度,如果单条消息内容比较长,则打包多条消息发送会影响其他线程发送消息的响应时间,并且单批次消息发送总长度不能超过DefaultMQProducer#maxMessageSize(4 M)。批量消息发送要解决的是如何将这些消息编码以便服务端能够正确解码出每条消息的消息内容。

DefaultMQProducer#send

public SendResult send(
    Collection msgs) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    return this.defaultMQProducerImpl.send(batch(msgs));
}

DefaultMQProducer#batch

private MessageBatch batch(Collection msgs) throws MQClientException {
    MessageBatch msgBatch;
    try {
        msgBatch = MessageBatch.generateFromList(msgs);
        for (Message message : msgBatch) {
            Validators.checkMessage(message, this);
            MessageClientIDSetter.setUniqID(message);
            message.setTopic(withNamespace(message.getTopic()));
        }
        msgBatch.setBody(msgBatch.encode());
    } catch (Exception e) {
        throw new MQClientException("Failed to initiate the MessageBatch", e);
    }
    msgBatch.setTopic(withNamespace(msgBatch.getTopic()));
    return msgBatch;
}

首先在消息发送端,调用batch方法,将一批消息封装成MessageBatch对象。Message-Batch继承自Message对象,MessageBatch内部持有List messages。这样的话,批量消息发送与单条消息发送的处理流程完全一样。MessageBatch只需要将该集合中的每条消息的消息体body聚合成一个byte[]数值,在消息服务端能够从该byte[]数值中正确解析出消息即可

MessageBatch#encode

public byte[] encode() {
    return MessageDecoder.encodeMessages(messages);
}

MessageDecoder#encodeMessage

public static byte[] encodeMessages(List messages) {
    //TO DO refactor, accumulate in one buffer, avoid copies
    List encodedMessages = new ArrayList(messages.size());
    int allSize = 0;
    for (Message message : messages) {
        byte[] tmp = encodeMessage(message);
        encodedMessages.add(tmp);
        allSize += tmp.length;
    }
    byte[] allBytes = new byte[allSize];
    int pos = 0;
    for (byte[] bytes : encodedMessages) {
        System.arraycopy(bytes, 0, allBytes, pos, bytes.length);
        pos += bytes.length;
    }
    return allBytes;
}

在消息发送端将会按照上述结构进行解码,然后整个发送流程与单个消息发送没什么差异,就不一一介绍了

6 发送顺序消息、延迟消息、事务消息

顺序消息
Rocketmq能够保证消息严格顺序,但是Rocketmq需要producer保证顺序消息按顺序发送到同一个queue中,比如购买流程(1)下单(2)支付(3)支付成功,
这三个消息需要根据特定规则将这个三个消息按顺序发送到一个queue
如何实现把顺序消息发送到同一个queue:
一般消息是通过轮询所有队列发送的,顺序消息可以根据业务比如说订单号orderId相同的消息发送到同一个队列, 或者同一用户userId发送到同一队列等等
messageQueueList [orderId%messageQueueList.size()]
messageQueueList [userId%messageQueueList.size()]

延迟消息:
RocketMQ 支持发送延迟消息,但不支持任意时间的延迟消息的设置,仅支持内置预设值的延迟时间间隔的延迟消息。
预设值的延迟时间间隔为:1s、 5s、 10s、 30s、 1m、 2m、 3m、 4m、 5m、 6m、 7m、 8m、 9m、 10m、 20m、 30m、 1h、 2h
在消息创建的时候,调用 setDelayTimeLevel(int level) 方法设置延迟时间。broker在接收到延迟消息的时候会把对应延迟级别的消息先存储到对应的延迟队列中,等延迟消息时间到达时,会把消息重新存储到对应的topic的queue里面

事务消息 // TODO

7 消息发送方式 同步、异步、单向区别

1.同步发送 ,需要同时等待有返回值,主要运用在比较重要一点消息传递/通知等业务。
SendResult sendResult = producer.send(message);
2.异步发送,异步线程发送出去消息,速度快,sendCallback 拿到返回信息,通常用于对发送消息响应时间要求更高/更快的场景。

producer.send(message, new SendCallback() {
           @Override
           public void onSuccess(SendResult sendResult) {
           }
           @Override
           public void onException(Throwable throwable) {
           }
       });

3.oneway 方式,只管发送,不在意是否成功,日志处理一般这样,/只发送消息,不等待服务器响应,只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。

8 消息发送如何进行负载

通过上文 4 消息发送方式 有一步操作是选择消息对立进行发送,通过轮询所有队列的机制实现负载。

9 消息发送如何实现高可用

通过上文 4 消息发送方式 选择消息队列的时候有开启Broker故障延迟机制 策略来规避一直发送失败的broker中的队列提升高可用。

10 批量消息发送如何实现一致性

将批量消息统一进行编码 发送到body,消费端根据编码规则进行解码。

11 消息发送失败如何重试

SYNC 重试3 其他1

int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
String[] brokersSent = new String[timesTotal];
for (; times < timesTotal; times++) {
   .....
}

sendOneway不可能对远程状态码重试,因为它不会收到任何值,所以会重试本地错误重试,也就是 1次
本地的错误重试包括RemotingException、MQClientException

send同步/异步都会做broker错误码MQBrokerException
重试,oneway不会。

private void sendMessageAsync(
    final String addr,
    final String brokerName,
    final Message msg,
    final long timeoutMillis,
    final RemotingCommand request,
    final SendCallback sendCallback,
    final TopicPublishInfo topicPublishInfo,
    final MQClientInstance instance,
    final int retryTimesWhenSendFailed,
    final AtomicInteger times,
    final SendMessageContext context,
    final DefaultMQProducerImpl producer
) throws InterruptedException, RemotingException {
   .....
    catch (Exception e) {
    producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), true);
    *onExceptionImpl*(brokerName, msg, 0L, request, sendCallback, topicPublishInfo, instance,
        retryTimesWhenSendFailed, times, e, context, false, producer);
}

}

这里调用了processSendResponse,这个会抛出broker错,在catch里会重试错误,onExceptionImpl里做了递归重试处理,超过一定次数就不再重试,默认为3次。

12 Producer 和 nameServ&broker 通信机制

与nameserver关系

  • 连接
    • 单个生产者者和一台nameserver保持长连接,定时查询topic配置信息,如果该nameserver挂掉,生产者会自动连接下一个nameserver,直到有可用连接为止,并能自动重连。
  • 轮询时间
    • 默认情况下,生产者每隔30秒从nameserver获取所有topic的最新队列情况,这意味着某个broker如果宕机,生产者最多要30秒才能感知,在此期间,发往该broker的消息发送失败。该时间由DefaultMQProducer的pollNameServerInteval参数决定,可手动配置。
  • 心跳
    • 与nameserver没有心跳

与broker关系

  • 连接
    • 单个生产者和该生产者关联的所有broker保持长连接。
  • 心跳
    • 默认情况下,生产者每隔30秒向所有broker发送心跳,该时间由DefaultMQProducer的heartbeatBrokerInterval参数决定,可手动配置。broker每隔10秒钟(此时间无法更改),扫描所有还存活的连接,若某个连接2分钟内(当前时间与最后更新时间差值超过2分钟,此时间无法更改)没有发送心跳数据,则关闭连接。
  • 连接断开
    • 移除broker上的生产者信息

13 消息发送异常机制

针对异常机制做重试处理

14 Producer 配置讲解 //TODO

你可能感兴趣的:(RocketMQ-Producer生产者解析)