RocketMQ源码分析——消息发送的核心原理与高可用

3 篇文章 0 订阅


在梳理源码前,先简单思考下发送消息中的核心点:

  1. Producer要想发送消息,首先就得拿到 topic 的路由信息,路由信息在哪里呢?结合前面 Broker 启动和 NameServer 启动的分析,我们知道 topic 是持久化在 Broker 端的,同时会在 NameServer 端的路由管理器中缓存;
  2. 拿到 topic 路由信息后,就要选择一个队列,那么如何选择呢,这里就涉及到负载均衡策略,采用了轮询的策略,同时还有一些故障规避机制,如某个 broker 不可用导致重试,那么在重试时就不会再选择该 broker 发送了;
  3. 确定了队列后,接下来就是具体的消息发送,这里需要了解消息的结构,发送的方式(单向、同步、异步、批量、延迟、事务等,当然本节不会都涉及到,仅以最基本的几个方式为例,如果有时间,后面会专门分析下)、发送失败时如何重试、同步异步重投的差异等;
  4. 发送到 Broker 后,Broker 肯定会对消息进行存储,结合上一节对 Broker 的介绍,我们知道 Broker 端会存储到 commitlog 文件中,并且有个定时任务,会定时将 commitlog 中的消息进行分发到 consumeQueue 和 indexFile 中(消息存储相关会在下节讲解)

文章较长,可以先看最后的总结有大体的认识。

我们以 example 模块下的最基本的Producer发送为例:org.apache.rocketmq.example.quickstart.Producer

以下是消息发送的代码:

public class Producer {
    public static void main(String[] args) throws MQClientException, InterruptedException {

        /*
         * 初始化一个 Producer 并指定发送组
         */
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");

        /*
         * Specify name server addresses.
         * <p/>
         *
         * Alternatively, you may specify name server addresses via exporting environmental variable: NAMESRV_ADDR
         * <pre>
         * {@code
         * producer.setNamesrvAddr("name-server1-ip:9876;name-server2-ip:9876");
         * }
         * </pre>
         */
       //设置 nameserver 地址
        producer.setNamesrvAddr("127.0.0.1:9876");
        /*
         * 启动 producer 实例
         */
        producer.start();

        for (int i = 0; i < 1; i++) {
            try {

                /*
                 * 创建一个消息,执行 topic、tag、消息体等(tag 是用于过滤的,本节忽略)
                 */
                Message msg = new Message("TopicTest" /* Topic */,
                    "TagA" /* Tag */,
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
                );
                //messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
               //这里是用于延迟消息的发送,本节忽略
              //  msg.setDelayTimeLevel(2);
                /*
                 * 进行同步发送,返回发送结果
                 */
                SendResult sendResult = producer.send(msg);

                System.out.printf("%s%n", sendResult);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        /*
         * Shut down once the producer instance is not longer in use.
         */
        producer.shutdown();
    }
}

一、DefaultMQProducer

先来了解下这个核心的发送类DefaultMQProducer

其提供了如下的发送方式:

在这里插入图片描述

1.1 消息发送方式

1.1.1 同步发送

  • SendResult send(final Message msg)

    最简单的消息发送,发送到哪个队列由负载均衡算法决定,同步返回发送结果

  • SendResult send(final Message msg, final long timeout)

    同步发送,指定了超时时间,超时抛异常

  • SendResult send(final Message msg, final MessageQueue mq)

    同步发送,指定topic 队列

  • SendResult send(final Message msg, final MessageQueue mq, final long timeout)

    同步发送,指定 topic 队列和超时时间

下面每种方式基本都提供了指定 topic 队列和超时的接口,就不一一列举了,仅列举一个

1.1.2 异步发送

异步发送肯定是带有一个回调接口的:

  • void send(final Message msg, final SendCallback sendCallback)

    异步发送,立即返回,发送结果在回调接口中接收,如:

    producer.send(msg, new SendCallback() {
                        @Override
                        public void onSuccess(SendResult sendResult) {
                            System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
                        }
    
                        @Override
                        public void onException(Throwable e) {
                            System.out.printf("%-10d Exception %s %n", index, e);
                            e.printStackTrace();
                        }
                    }
    

    发送后不等待结果立即返回,消息发送结果会通过回调接口进行通知

1.1.3 批量发送

SendResult send(final Collection<Message> msgs)

同步批量发送

接收一个消息集合

1.1.4 单向发送

void sendOneway(final Message msg)

  • 没有回调
  • 没有返回结果
  • 发出去后无从得知是否发送成功

1.1.5 自定义负载策略

SendResult send(final Message msg, final MessageQueueSelector selector, final Object arg)

  • MessageQueueSelector

    该接口用于自定义负载策略,即按自己设计的规则选择 topic 队列进行发送

  • arg

    用于实现负载的自定义参数

示例:

for (int i = 0; i < 3; i++) {
                int orderId = i;

                for(int j = 0 ; j <3 ; j ++){
                    Message msg =
                            new Message("OrderTopicTest", "order_"+orderId, "KEY" + orderId,
                                    ("order_"+orderId+" step " + j).getBytes(RemotingHelper.DEFAULT_CHARSET));
                    SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                        @Override
                        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                            Integer id = (Integer) arg;
                            int index = id % mqs.size();
                            return mqs.get(index);
                        }
                    }, orderId);

                    System.out.printf("%s%n", sendResult);
                }
            }

传入的参数 orderId,会传入public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg)的 arg 参数中,mqs 是待选择的队列,该例中通过对 orderId 取模确定选择哪个队列发送。

从这个示例代码也可以看出,这也是保证RocketMQ 消息局部顺序消费的方式,在 producer 端,对于同一个 orderId 都保证发送到了同一个 MessageQueue,而在消费端,通过MessageListenerOrderly可以保证同一个队列的消息被顺序的消费,这样就可以保证被发送到一个队列中的消息被最终的顺序消费掉。

消费的示例如下:

        consumer.registerMessageListener(new MessageListenerOrderly() {//主要是这个MessageListenerOrderly监听器
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                for(MessageExt msg:msgs){
                    System.out.println("收到消息内容 "+new String(msg.getBody()));
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });

1.2 核心属性

  //producer 组,仅在事务消息中有效,回查事务状态时会随机选择该组中的任何一个 producer 发起的事务回查请求
 	private String producerGroup;

    /**
     * 默认的 topicKey,isAutoCreateTopicEnable开启时默认创建的系统topic
     */
    private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC;

    /**
     * 默认每个主题下的队列数
     */
    private volatile int defaultTopicQueueNums = 4;

    /**
     * 默认发送超时时间
     */
    private int sendMsgTimeout = 3000;

    /**
     *  消息大小超过该值时进行压缩
     */
    private int compressMsgBodyOverHowmuch = 1024 * 4;

    /**
     *
     * 同步模式下,默认失败重试次数
     */
    private int retryTimesWhenSendFailed = 2;

    /**
     * 异步模式下,默认失败重试次数
     */
    private int retryTimesWhenSendAsyncFailed = 2;

    /**
     * 发送消息后,如果没有存储成功,是否选择其他 broker 进行重试
     */
    private boolean retryAnotherBrokerWhenNotStoreOK = false;

    /**
     *  最大消息大小,默认4M
     */
    private int maxMessageSize = 1024 * 1024 * 4; // 4M

二、Producer 启动

producer.start()开始:

 public void start() throws MQClientException {
		//设置生产组名
        this.setProducerGroup(withNamespace(this.producerGroup));
    	
        this.defaultMQProducerImpl.start();
        if (null != traceDispatcher) {
            try {
                traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
            } catch (MQClientException e) {
                log.warn("trace dispatcher start failed ", e);
            }
        }
    }

最终的入口如下:

public void start(final boolean startFactory) throws MQClientException {
        switch (this.serviceState) {
            case CREATE_JUST:
                this.serviceState = ServiceState.START_FAILED;
                //1.检查生产者组是否符合要求
                this.checkConfig();
                //1.1修改当前的instanceName为当前进程ID
                if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
                    this.defaultMQProducer.changeInstanceNameToPID();
                }
                //获取MQ实例
                this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(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);
                }
					//创建并默认默认的系统 topic:TBW102
              //topicPublishInfoTable维护了 topic和 TopicPublishInfo关系,TopicPublishInfo中保存了 topic 的队列路由等信息
                this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());
                //启动实例
                if (startFactory) {
                    mQClientFactory.start();
                }

                log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(),
                    this.defaultMQProducer.isSendMessageWithVIPChannel());
                this.serviceState = ServiceState.RUNNING;
                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.timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                try {
                    RequestFutureTable.scanExpiredRequest();
                } catch (Throwable e) {
                    log.error("scan RequestFutureTable exception", e);
                }
            }
        }, 1000 * 3, 1000);
    }

下面分布讲解:

首先是检查生产组名,并将当前的 instanceName 设为进程 ID

2.1 创建MQClientInstance实例

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

MQClientManager是单例的,

MQClientManager#getOrCreateMQClientInstance

//DefaultMQProducer继承了ClientConfig
public MQClientInstance getOrCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) {
 	//客户端实例 id:ip@进程号
      String clientId = clientConfig.buildMQClientId();
 		//factoryTable 是客户端实例的缓存
      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;
  }
1. 生成客户端 id(ip@进程号@unitName),unitName 为可选的
2. 从本地缓存取实例,如果没有则创建,数据结构为  ConcurrentHashMap
3. 初始化客户端实例后放入到本地缓存,因此同一个客户端 id 只会产生一个实例

初始化客户端实例时,会初始化客户端的 Netty网络相关组件(用于 producer 或 consumer 与 Nameserver 和 Broker 进行网络通信)、rebalance 服务、pullMessage等服务,如下:

 public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) {
        this.clientConfig = clientConfig;
        this.instanceIndex = instanceIndex;
    	//初始化netty 客户端配置类        
        this.nettyClientConfig = new NettyClientConfig();
  this.nettyClientConfig.setClientCallbackExecutorThreads(clientConfig.getClientCallbackExecutorThreads());
        this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS());
    
    		//网络请求处理器,前面的文章讲过,会根据RequestCode来处理不同的网络请求
        this.clientRemotingProcessor = new ClientRemotingProcessor(this);
    	//对 Netty客户端和请求处理器进行了封装,初始化时会向内部的NettyRemotingClient注册不同的RequestCode
        this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig);

        if (this.clientConfig.getNamesrvAddr() != null) {
           //更新下 nameServer 地址
            this.mQClientAPIImpl.updateNameServerAddressList(this.clientConfig.getNamesrvAddr());
        }

        this.clientId = clientId;

        this.mQAdminImpl = new MQAdminImpl(this);

        this.pullMessageService = new PullMessageService(this);

        this.rebalanceService = new RebalanceService(this);

        this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP);
        this.defaultMQProducer.resetClientConfig(clientConfig);

        this.consumerStatsManager = new ConsumerStatsManager(this.scheduledExecutorService);

     
    }

2.2 注册Producer 到MQClientInstance

拿到客户端实例后,会将当前生产者注册到MQClientInstance,方便后续处理:

 private final ConcurrentMap<String/* group */, MQProducerInner> producerTable = new ConcurrentHashMap<String, MQProducerInner>();  public boolean registerProducer(final String group, final DefaultMQProducerImpl producer) {     ....        MQProducerInner prev = this.producerTable.putIfAbsent(group, producer);        ...        return true;    }

2.3 启动MQClientInstance

这一部分可以简单看下,该部分都多的是和消息消费相关的服务,后面讲消费的时候再细看。

 mQClientFactory.start();
public void start() throws MQClientException {        synchronized (this) {            switch (this.serviceState) {                case CREATE_JUST:                    this.serviceState = ServiceState.START_FAILED;                    // If not specified,looking address from name server                    if (null == this.clientConfig.getNamesrvAddr()) {                        this.mQClientAPIImpl.fetchNameServerAddr();                    }                    //内部启动 Netty 客户端实例                    this.mQClientAPIImpl.start();                    // Start various schedule tasks                    this.startScheduledTask();                    // Start pull service                    this.pullMessageService.start();                    // Start rebalance service                    //K2 客户端负载均衡                    this.rebalanceService.start();                    // Start push service                    this.defaultMQProducer.getDefaultMQProducerImpl().start(false);                    log.info("the client factory [{}] start OK", this.clientId);                    this.serviceState = ServiceState.RUNNING;                    break;                case START_FAILED:                    throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);                default:                    break;            }        }    }
  1. 启动 Netty 客户端实例

  2. 创建定时任务

    • 30s 一次根据 topic 从Nameserver拉取路由信息,更新到MQClientInstance中的topicRouteTable,key 为 topic ,value 为topic 路由信息TopicRouteData;同时路由中还会返回 Broker 的信息,更新到MQClientInstancebrokerAddrTable中:

       private final ConcurrentMap<String/* Broker Name */, HashMap<Long/* brokerId */, String/* address */>> brokerAddrTable =        new ConcurrentHashMap<String, HashMap<Long, String>>();
      

      【如果当前实例是 Producer,则 topic 是 Producer 要发送的 topic,如果当前实例是消费者,则 topic 为当前消费者订阅的 topic】

    • 清除离线 broker和定时发送心跳给所有的 broker【30s 一次】

      清除离线 broker :遍历MQClientInstantce本地所有的 broker 地址,然后再遍历本地所有的路由信息,如果没有一个路由中存在该 broker 地址,则将该 broker 从本地移除;

      定时发送心跳:组装HeartbeatData,向 Broker 发送心跳,Broker 会返回版本信息,然后更新到MQClientInstance中的brokerVersionTable【key:brokerName,value:version】

    • 定时持久化 consumer 的 offset【5s 一次】,这是消费者端处理的,暂时不看

  3. 启动拉消息服务,最终启动PullMessageService线程

  4. 启动客户端负载均衡服务,最终启动RebalanceService线程

​ Producer 启动完成后,就到了发送消息的部分了。

三、消息发送

消息对象 Message

Message 完整的构造如下:

public Message(String topic, String tags, String keys, int flag, byte[] body, boolean waitStoreMsgOK) ;

设置了消息的 如下信息:

  • topic

    主题

  • tags

    标签,用于消息过滤,存到了 Message 的 proerty 中

  • keys

    一些索引键,用于快速检索消息,也是存储到 property 中

  • flag

    消息 flag,定义在MessageSysFlag

  • body,消息体

  • watiStorrremsgOK

    是否等待消息存储完成后返回

其他的,还可以设置消息的延迟时间。

接着来到消息的发送入口:

DefaultMQProducer#send

public SendResult send(        Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {   //消息校验        Validators.checkMessage(msg, this);        msg.setTopic(withNamespace(msg.getTopic()));        return this.defaultMQProducerImpl.send(msg);    }

DefaultMQProducerImpl#send(Message)

public SendResult send(
    Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
   //设置超时时间,默认3s
    return send(msg, this.defaultMQProducer.getSendMsgTimeout());
}
 public SendResult send(Message msg,
        long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
        return this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout);
    }

3.1 消息校验

如上,消息发送前会先记性消息校验:

 public static void checkMessage(Message msg, DefaultMQProducer defaultMQProducer)
        throws MQClientException {
    		//1.非空校验
        if (null == msg) {
            throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message is null");
        }
        // 2.topic校验:非空、不能超过最大的长度TOPIC_MAX_LENGTH=127
        Validators.checkTopic(msg.getTopic());
    	 //系统预设了SCHEDULE_TOPIC_XXXX系统 topic 不允许发送消息
        Validators.isNotAllowedSendTopic(msg.getTopic());

        // body
        if (null == msg.getBody()) {
            throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body is null");
        }
			//消息体必须大于0
        if (0 == msg.getBody().length) {
            throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body length is zero");
        }
		//不能超过 producer 设置的最大消息大小,默认4M
        if (msg.getBody().length > defaultMQProducer.getMaxMessageSize()) {
            throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL,
                "the message body size over max value, MAX: " + defaultMQProducer.getMaxMessageSize());
        }
    }

主要对消息的基础信息进行校验:

  • 非空校验(msg 对象、topic 和消息体)
  • topic 不能超过最大长度127
  • 消息体大小必须大于0且小于配置的最大消息大小,默认1024 * 1024 * 4; // 4M

同步发送消息,默认超时时间为3s,

消息发送最终会来到

DefaultMQProducerImpl#sendDefaultImpl,由于方法较长,这里按步骤截取代码分析。

3.2 获取 topic 信息

        TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
 //找路由表的过程都是先从本地缓存找,本地缓存没有,就去NameServer上申请。
    private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
       //1.先从本地缓存查找
        TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
        if (null == topicPublishInfo || !topicPublishInfo.ok()) {
            this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
            //2.本地缓存没有,则Producer向NameServer获取更新Topic的路由信息,将查询到的 topic 信息更新到 MQClientInstance 中,并根据路由信息创建TopicPublishInfo更新到各个 Producer 实例的本地缓存中
            this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
            //3.再从本地缓存中查找2中更新的Topic路由信息
            topicPublishInfo = this.topicPublishInfoTable.get(topic);
        }

        if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
           //查到了,就直接返回
            return topicPublishInfo;
        } else {
           //4.否则使用默认的主题从 nameserver 查询,这里如果没有开启自动创建主题,则直接抛异常
            this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
            topicPublishInfo = this.topicPublishInfoTable.get(topic);
            return topicPublishInfo;
        }
    }

大体逻辑就是上面的1-4步,这里梳理下:

  • 先从 Producer 本地路由缓存查询,如果有直接返回;
  • 否则,根据 topic 去从 NameServer 查询路由信息,最终会更新到 Producer 的本地路由,如果查到了,则直接返回;
  • 否则,根据默认的 topic 从 NameServer 查询默认 topic,根据默认 topic 的参数创建队列等,如果没有开启自动创建主题参数,则会直接抛异常

这里先看下从 NameServer 拉取 topic 的代码:

3.2.1 从 NameServer 拉取路由

public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault,
        DefaultMQProducer defaultMQProducer) {
                    TopicRouteData topicRouteData;
                    if (isDefault && defaultMQProducer != null) {
                       //2.如果第一次 topic 没有查到,则查询默认 topic:TBW102,如果未开启AutoCreateTopicEnable,则这里查不到默认主题,就会直接抛异常
                       
                        topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(),
                            1000 * 3);
                        if (topicRouteData != null) {
                            for (QueueData data : topicRouteData.getQueueDatas()) {
                               //将默认的路由信息的读写队列数量改为 producer 默认的队列数:4个
                                int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
                                data.setReadQueueNums(queueNums);
                                data.setWriteQueueNums(queueNums);
                            }
                        }
                    } else {
                       //1,第一次直接根据当前 topic 从 NameServer 查
                        topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);
                    }
                    
		.................        

        return false;
    }

从 NameServer 拉取路由信息可以去 NameServer 端网络请求处理器中了解,前面也介绍过,不赘述,RequestCode 为:GET_ROUTEINFO_BY_TOPIC

查询到路由信息TopicRouteData后,主要做了以下几件事:

  1. 查询本地缓存的对应路由,判断是否改变,如果没有改变,则直接返回 false;

  2. 否则,更新 MQClientInstance 的 Broker 地址表;

  3. 根据TopicRouteData创建TopicPublishInfo

     TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
       publishInfo.setHaveTopicRouterInfo(true);
    ####
    public static TopicPublishInfo topicRouteData2TopicPublishInfo(final String topic, final TopicRouteData route) {
            TopicPublishInfo info = new TopicPublishInfo();
            info.setTopicRouteData(route);
            ......
                List<QueueData> qds = route.getQueueDatas();
                Collections.sort(qds);
                for (QueueData qd : qds) {
                    if (PermName.isWriteable(qd.getPerm())) {//只要有写权限的
                         //省略的是从TopicRouteData的 Broker 列表中找到当前队列所在的 BrokerData
    							.........
                        //如果不是 master则跳过
                        if (!brokerData.getBrokerAddrs().containsKey(MixAll.MASTER_ID)) {
                            continue;
                        }
    
                        for (int i = 0; i < qd.getWriteQueueNums(); i++) {
                           //转换为MessageQueue保存到TopicPublishInfo中,其中 i 为 queueId
                            MessageQueue mq = new MessageQueue(topic, qd.getBrokerName(), i);
                            info.getMessageQueueList().add(mq);
                        }
                    }
                }
    
                info.setOrderTopic(false);
         
    
            return info;
        }
    
    
    • TopicRouteData被放到了TopicPublishInfo中
    • TopicRouteData中有写权限的的QueueData列表,根据写队列数量,转为MessageQueue列表保存到TopicPublishInfo中【过滤掉不在 master 的队列】,MessageQueue 的 queueId 为 序列号
  4. 找到所有 producer 实例,将TopicPublishInfo更新到 Producer 的本地缓存

  5. TopicRouteData中所有有读权限的队列,根据读队列数量创建对应的MessageQueue列表,更新到各个消息者实例的本地topic 订阅信息表中【也是到后面消费消息的时候看】

3.3 选择 MessageQueue

首先计算重试次数:

int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;

同步模式下,默认失败重试次数为retryTimesWhenSendFailed=2次,加上第一次发送,总共会进行3次发送

其他模式下,如异步模式、单向模式等只会发送一次,如果失败会在异步请求回调结果的函数中进行重投,重投次数由retryTimesWhenSendAsyncFailed决定,详见3.5.4.2一节。

MessageQueue mq = null;for (; times < timesTotal; times++) {                String lastBrokerName = null == mq ? null : mq.getBrokerName();                //计算把消息发到哪个MessageQueue中。                MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);    ..... }

lastBrokerName用于记录上一次失败的 broker,第一次时为空,selectOneMessageQueue用于计算本次 producer 选择哪个 MessageQueue 进行发送。

public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {        return this.mqFaultStrategy.selectOneMessageQueue(tpInfo, lastBrokerName);    }    public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {        //这个sendLatencyFaultEnable默认是关闭的,Broker故障延迟机制,表示一种发送消息失败后一定时间内不在往同一个Queue重复发送的机制        if (this.sendLatencyFaultEnable) {           //1.开启故障延迟机制            ..............        }			//2.未开启故障延迟机制时选择 MessageQueue 的方式        return tpInfo.selectOneMessageQueue(lastBrokerName);    }

这里分两种处理方式:

  1. 当开启了sendLatencyFaultEnable时,表示使用 Broker 的故障延迟机制
  2. 否则走默认的选择机制

3.3.1 默认选择机制(未开启故障延迟)

默认情况下,走TopicPublishInfo#selectOneMessageQueue进行队列选择:

public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
   //lastBrokerName为上一次失败的 brokerName
        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)) {
                   //规避上一次的 broker
                    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);
    }

说明:

  • lastBrokerName记录了上一次执行失败的 brokerName

  • 第一次时直接通过sendWhichQueue进行递增,与当前消息队列数量进行取模获取对应的队列;

  • 如果此次失败了,则再下一次重试时会传入上次失败的 brokerName

    以同样的方式自增+取模获取某个 MessageQueue,但是会规避掉上次失败的 Broker,选择其他的 Broker 进行发送。

那么为什么 Broker 不可用后这里还是会有该 broker 的信息呢?

结合前文 NameServer 和Broker 启动流程以及2.3节中的心跳发送,简单回顾下:

  • Broker 启动后,会每隔30s 向 NameServer 发送一次心跳,更新 NameServer 端 Broker 的活跃信息;
  • NameServer每10s 扫描一次本地的不活跃 broker超过120s 未收到 broker 心跳才会将其移除;
  • 而 NameServer 移除后,Producer 不会立即感知,Producer 端会每30s 向 NameServer请求拉取一次 topic 信息,其中会包含 broker 的信息

因此 从Broker 不可用到被检测移除并更新到 Producer 端,中间会存在几十秒的间隔期,通过规避上次失败的 broker 可以提升本次发送消息的成功率。

默认机制下,我们可以看到其实是会规避上次不可用的 Broker,但是仅仅是此次规避,下一次可能会还用之前不可用的 broker,那么如果开启了 Broker 故障延迟机制是怎么做的呢?【看第五部分】

3.4 消息发送0

3.3选择到此次要发送的队列后,就终于准备消息发送了:

 if (mqSelected != null) {
                    mq = mqSelected;
                    //根据MessageQueue去获取目标节点的信息。
                    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;
                       //处理超时默认3s
                        if (timeout < costTime) {
                            callTimeout = true;
                            break;
                        }
                        //实际发送消息的方法
                        sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
                       //结束时间
                        endTimestamp = System.currentTimeMillis();
                       ///后面都是发送消息的后续处理,包括重试、Broker 古扎那个延迟机制更新失败条目等
                        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()) {
                                      //retryAnotherBrokerWhenNotStoreOK:选择其他 broker重试,如果没有返回存储成功,这时会选其他 broker重试
                                        continue;
                                    }
                                }

                                return sendResult;
                            default:
                                break;
                        }
                    } catch (RemotingException e) {
                       .....
                        continue;
                    } catch (MQClientException e) {
                       .....
                        continue;
                    } catch (MQBrokerException e) {
                        endTimestamp = System.currentTimeMillis();
                       //broker 故障延迟机制处理
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                      ...
                        exception = e;
                        switch (e.getResponseCode()) {
                            case ResponseCode.TOPIC_NOT_EXIST:
                            case ResponseCode.SERVICE_NOT_AVAILABLE:
                            case ResponseCode.SYSTEM_ERROR:
                            case ResponseCode.NO_PERMISSION:
                            case ResponseCode.NO_BUYER_ID:
                            case ResponseCode.NOT_IN_CURRENT_UNIT:
                              //这些 broker 异常都继续走外层循环重试
                                continue;
                            default:
                                if (sendResult != null) {
                                   //其他情况下就直接返回结果,如果结果为空,则抛异常
                                    return sendResult;
                                }

                                throw e;
                        }
                    } catch (InterruptedException e) {
                       ...
                    }
                }

分三步:

  • 计算超时,超过3s 则记录超时,break 到外层会抛出超时异常RemotingTooMuchRequestException("sendDefaultImpl call timeout"
  • 执行发送消息this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime)
  • 得到返回结果,处理故障延迟机制,以及异常处理(重试或直接返回)

这里主要看下消息的发送部分:

3.5 真正消息发送

this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime)

参数如下:

 private SendResult sendKernelImpl(final Message msg,
        final MessageQueue mq,
        final CommunicationMode communicationMode,
        final SendCallback sendCallback,
        final TopicPublishInfo topicPublishInfo,
        final long timeout)
  • Message
  • MessageQueue:选择的目标队列
  • CommunicationMode:发送模式:SYNC、ASYNC、ONEWAY 三种
  • SendCallback:异步发送时的回调
  • TopicPublishInfo:topic 路由信息
  • timeout:超时时间

3.5.1 查询 broker 地址

{
   long beginStartTime = System.currentTimeMillis();
	//寻找Broker地址。找不到就去NameServer上获取。
	String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
	if (null == brokerAddr) {
    tryToFindTopicPublishInfo(mq.getTopic());
    brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
   if (brokerAddr != null){
   	...
   }
           throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null);

}
  1. 从 MQClientInstance 的本地 broker 表查询;
  2. 如果没有,则尝试从 NameServer获取,并更新到MQClientInstance的本地 broker 表【就是前面的从 NameServer 更新路由信息的逻辑,路由中会携带 broker 信息,更新到本地缓存】;
  3. 如果还没有,说明此 broker 不可用,则抛异常,会在外层 for 循环捕获,进行重试

3.5.2 前置处理(生成唯一 id、压缩消息体、钩子等)

 if (brokerAddr != null) {            brokerAddr = MixAll.brokerVIPChannel(this.defaultMQProducer.isSendMessageWithVIPChannel(), brokerAddr);            byte[] prevBody = msg.getBody();            try {                //for MessageBatch,ID has been set in the generating process               //批量消息,在此之前已经设置了 ID,这里就不设置了                               if (!(msg instanceof MessageBatch)) {                    MessageClientIDSetter.setUniqID(msg);                }                boolean topicWithNamespace = false;                if (null != this.mQClientFactory.getClientConfig().getNamespace()) {                   //设置实例 id                    msg.setInstanceId(this.mQClientFactory.getClientConfig().getNamespace());                    topicWithNamespace = true;                }                int sysFlag = 0;                boolean msgBodyCompressed = false;                if (this.tryToCompressMessage(msg)) {//3.尝试压缩消息体                   //记录消息标记为压缩                    sysFlag |= MessageSysFlag.COMPRESSED_FLAG;                    msgBodyCompressed = true;//记录被压缩了                }                               final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);                if (tranMsg != null && Boolean.parseBoolean(tranMsg)) {//事务消息                   //记录消息标记为事务的 prepared消息                    sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;                }                                if (this.hasSendMessageHook()) {                    //钩子处理                    context = new SendMessageContext();                    ......................                    this.executeSendMessageHookBefore(context);                }            } }
  1. 为消息生成唯一 ID
  2. 对消息体进行压缩
    • 批量消息的话不处理,返回 false
    • 如果消息体大小超过compressMsgBodyOverHowmuch=4KB,则进行zip压缩(java.util.zip.Deflater
    • 如果压缩成功,则记录标记为COMPRESSED_FLAG
  3. 如果是事务的 prepared消息,则记录为TRANSACTION_PREPARED_TYPE
  4. 如果注册了发送消息的钩子,则这里执行钩子函数

3.5.3 封装消息发送请求头

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());					//property 信息,包含了 tag、keys 等信息                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);                    }                }

3.5.4 执行发送

 SendResult sendResult = null;
                              switch (communicationMode) {
                    case ASYNC:
                      ....
                        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;
                }

最终都会来到入口:MQClientAPIImpl#sendMessage

 public SendResult sendMessage(
        final String addr,//broker 地址
        final String brokerName,//broker 名
        final Message msg,//消息
        final SendMessageRequestHeader requestHeader,//请求头
        final long timeoutMillis,//超时时间
        final CommunicationMode communicationMode,//发送模式
        final SendCallback sendCallback,//回调接口,同步模式下为空
        final TopicPublishInfo topicPublishInfo,//topic 路由
        final MQClientInstance instance,
        final int retryTimesWhenSendFailed,//重试次数
        final SendMessageContext context,
        final DefaultMQProducerImpl producer//producer 实例
    ) throws RemotingException, MQBrokerException, InterruptedException {
        long beginStartTime = System.currentTimeMillis();
        RemotingCommand request = null;
        String msgType = msg.getProperty(MessageConst.PROPERTY_MESSAGE_TYPE);
        boolean isReply = msgType != null && msgType.equals(MixAll.REPLY_MESSAGE_FLAG);
        if (isReply) {
            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 {
               //默认的消息都来到这里,RequestCode为SEND_MESSAGE
                request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader);
            }
        }
        request.setBody(msg.getBody());

    //根据不同的模式进行发送处理
        switch (communicationMode) {
            case ONEWAY:
               ....
            case ASYNC:
                ....
            case SYNC:
                ...
            default:
                assert false;
                break;
        }

        return null;
    }

这里普通的消息的请求码都是SEND_MESSAGE,接着根据不同的发送模式进行处理,我们分别来看:

3.5.4.1 同步发送
 switch (communicationMode) {
            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;
 private SendResult sendMessageSync(
        final String addr,
        final String brokerName,
        final Message msg,
        final long timeoutMillis,
        final RemotingCommand request
    ) throws RemotingException, MQBrokerException, InterruptedException {
        RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis);
        assert response != null;
        return this.processSendResponse(brokerName, msg, response);
    }

最终发送请求的地方就是this.remotingClient.invokeSync,这里通过 Netty 客户端发起网络请求,都是 Netty 固定处理方式,不用关注;

拿到 resp 后就是处理发送结果,封装SendResult,我们等会再看,先看 Broker 端接收该发送消息请求的地方:

根据RequestCode.SEND_MESSAGE,我们可以定位到 Broker 端处理接收非批量消息的请求处理位置为:

org.apache.rocketmq.broker.processor.SendMessageProcessor#asyncSendMessage

 private CompletableFuture<RemotingCommand> asyncSendMessage(ChannelHandlerContext ctx, RemotingCommand request,
                                                                SendMessageContext mqtraceContext,
                                                                SendMessageRequestHeader requestHeader) {
       
     	final RemotingCommand response = preSend(ctx, request, requestHeader);
        final SendMessageResponseHeader responseHeader = (SendMessageResponseHeader)response.readCustomHeader();

        if (response.getCode() != -1) {
            return CompletableFuture.completedFuture(response);
        }
    ..
 }

这里有个 preSend 预发送的处理,里面主要做了两件事,一个是判断当前时间是否小于 broker 接收请求处理的时间,如果小于,说明 broker 还未开始接收请求,返回错误;还有一个是调用的父类的消息检查方法:super.msgCheck(ctx, requestHeader, response);

3.5.4.1.1 消息检查与 topic 创建
protected RemotingCommand msgCheck(final ChannelHandlerContext ctx,
        final SendMessageRequestHeader requestHeader, final RemotingCommand response) {
   //1.检查 broker 是否有写权限,以及 topic 是否为顺序 topic
         if (!PermName.isWriteable(this.brokerController.getBrokerConfig().getBrokerPermission())
            && this.brokerController.getTopicConfigManager().isOrderTopic(requestHeader.getTopic())) {
            response.setCode(ResponseCode.NO_PERMISSION);
            response.setRemark("the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1()
                + "] sending message is forbidden");
            return response;
        }
			//2.topic 校验,和 Producer 发送前的校验一样,非空、不能超过最大长度127等
        if (!TopicValidator.validateTopic(requestHeader.getTopic(), response)) {
            return response;
        }
   //一样
        if (TopicValidator.isNotAllowedSendTopic(requestHeader.getTopic(), response)) {
            return response;
        }
			//3.本主题配置管理器中获取该主题的配置信息
        TopicConfig topicConfig =
            this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic());
        if (null == topicConfig) {//如果没有
            int topicSysFlag = 0;
            if (requestHeader.isUnitMode()) {
                if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                    topicSysFlag = TopicSysFlag.buildSysFlag(false, true);
                } else {
                    topicSysFlag = TopicSysFlag.buildSysFlag(true, false);
                }
            }

            log.warn("the topic {} not exist, producer: {}", requestHeader.getTopic(), ctx.channel().remoteAddress());
           //3.1 如果没有则通过默认主题创建一个 topic,并持久化到本地 topic 配置,并同步更新到 NameServer 端的 topic 配置
            topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageMethod(
                requestHeader.getTopic(),
                requestHeader.getDefaultTopic(),
                RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
                requestHeader.getDefaultTopicQueueNums(), topicSysFlag);

           //3.3 重试时创建对应的重试 topic
            if (null == topicConfig) {
                if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                    topicConfig =
                        this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(
                            requestHeader.getTopic(), 1, PermName.PERM_WRITE | PermName.PERM_READ,
                            topicSysFlag);
                }
            }

            if (null == topicConfig) {
                response.setCode(ResponseCode.TOPIC_NOT_EXIST);
                response.setRemark("topic[" + requestHeader.getTopic() + "] not exist, apply first please!"
                    + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
                return response;
            }
        }
			.....
        return response;
    }

  1. 检查 broker 是否有写权限,如果没有写权限,且是顺序消息,则返回错误;

  2. 按 producer 发送前的topic 检查再检查一遍;

  3. 从 topic 管理器获取 topic 配置(Broker 启动时从本地 store/config/topics.json 加载的)

    • 如果没有,则根据默认主题创建 topic 配置,做法如下:

      • 从本地获取默认主题配置信息,然后判断是否开启了自动创建主题,如果开启了,则按照默认主题的写队列数、权限、topicSysFlag 等创建目标主题配置;如果未开启,则返回空到上层,会返回主题不存在错误信息;
      • 如果创建成功,则先更新 Broker 端主题配置管理器的本地主题表,并更新本地 DataVersion 版本戳,接着持久化到磁盘 topics.json 中,最后通过 broker 心跳同步到 Nameserver 端
      • 还记得前文将 Broker 注册的相关逻辑吗?在 NameServer 端,会根据 DataVersion 来判断topic 是否发生变化,如果变化,就会更新 NameServer 端的 topic队列缓存数据。
    • topicConfig 的主要属性如下:

      • order:是否顺序消息
      • perm:权限值
      • readQueueNums、writeQueueNums:读写队列数量
      • topicName:主题名
      • topicSysFlag:一个标识
3.5.4.1.2 DLQ 死信处理
if (!handleRetryAndDLQ(requestHeader, response, request, msgInner, topicConfig)) {
            return CompletableFuture.completedFuture(response);
        }

我们知道,如果消息重试次数超过了最大次数,则消息会自动进入死信队列 DLQ,系统会为其创建一个 DLQ主题,名称为:%DLQ%+消费组名,简单看下处理方式:

private boolean handleRetryAndDLQ(SendMessageRequestHeader requestHeader, RemotingCommand response,
                                      RemotingCommand request,
                                      MessageExt msg, TopicConfig topicConfig) {
        String newTopic = requestHeader.getTopic();
              //1.如果包含重试 topic 前缀
        if (null != newTopic && newTopic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
           //2.获取消费组名
            String groupName = newTopic.substring(MixAll.RETRY_GROUP_TOPIC_PREFIX.length());
           //3.获取订阅组的配置
            SubscriptionGroupConfig subscriptionGroupConfig =
                this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(groupName);
            if (null == subscriptionGroupConfig) {
                response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST);
                response.setRemark(
                    "subscription group not exist, " + groupName + " " + FAQUrl.suggestTodo(FAQUrl.SUBSCRIPTION_GROUP_NOT_EXIST));
                return false;
            }
				//4.获取订阅组的最大重试次数
            int maxReconsumeTimes = subscriptionGroupConfig.getRetryMaxTimes();
           //5.版本大于3.4.9时,最大重试次数从请求头里取,请求头里的最大值是 producer 发送时传入的
            if (request.getVersion() >= MQVersion.Version.V3_4_9.ordinal()) {
                maxReconsumeTimes = requestHeader.getMaxReconsumeTimes();
            }
           //6.获取当前的重试次数
            int reconsumeTimes = requestHeader.getReconsumeTimes() == null ? 0 : requestHeader.getReconsumeTimes();
           //7.如果超过了最大重试次数则创建 DLQ 队列
            if (reconsumeTimes >= maxReconsumeTimes) {
               //8.topic 名:%DLQ%+订阅组名
                newTopic = MixAll.getDLQTopic(groupName);
                int queueIdInt = Math.abs(this.random.nextInt() % 99999999) % DLQ_NUMS_PER_GROUP;
               //9.创建 DLQ topic 配置 
               topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(newTopic,
                    DLQ_NUMS_PER_GROUP,
                    PermName.PERM_WRITE, 0
                );
               //10.更新当前消息的 topic为 DLQ topic
                msg.setTopic(newTopic);
                msg.setQueueId(queueIdInt);
                if (null == topicConfig) {
                    response.setCode(ResponseCode.SYSTEM_ERROR);
                    response.setRemark("topic[" + newTopic + "] not exist");
                    return false;
                }
            }
        }
        int sysFlag = requestHeader.getSysFlag();
        if (TopicFilterType.MULTI_TAG == topicConfig.getTopicFilterType()) {
            sysFlag |= MessageSysFlag.MULTI_TAGS_FLAG;
        }
        msg.setSysFlag(sysFlag);
        return true;
    }

这里可以看到,当是重试消息的时候才会处理,因此我们正常发送的消息是不会进入到这里的,那么重试消息是哪里发送的呢?

其实是在消费者消费失败的时候,会进行重试,此时会使用RETRY主题加消费组作为新的topic进行发送,就会进入这里的DLQ处理。

接着会比较是否超过最大重试次数(默认16次,可在消费者端设置MaxReconsumeTimes参数修改),如果超过了,则创建DLQ死信Topic,将消息丢到这里

3.5.4.1.3 存储消息

接着,会将请求头信息转为MessageExtBrokerInner对象,包含消息请求的各个元数据信息,如属性、发送时间戳、produer 地址、store 地址、重试次数、集群名等。

然后会从 消息的 property 中取TRAN_MSG的值,如果为 true,则为事务的 prepare 消息,如果 broker的配置rejectTransactionMessage为 true 的话,则拒绝接收事务消息,返回错误,否则调用TransactionalMessageService存储事务的半消息。

如果为普通消息,则调用DefaultMessageStore进行消息存储。

这里消息存储均略过,后面讲消息存储的时候细讲。

消息存储后会返回消息存储结果,broker 根据存储结果响应本次接收消息的处理结果,大概有以下几个:

 switch (putMessageResult.getPutMessageStatus()) {
            // Success
            case PUT_OK:
                sendOK = true;
       //存储成功的
                response.setCode(ResponseCode.SUCCESS);
                break;
            case FLUSH_DISK_TIMEOUT://刷盘超时的
                response.setCode(ResponseCode.FLUSH_DISK_TIMEOUT);
                sendOK = true;
                break;
       //刷新到 SLAVE 节点超时的
            case FLUSH_SLAVE_TIMEOUT:
                response.setCode(ResponseCode.FLUSH_SLAVE_TIMEOUT);
                sendOK = true;
                break;
       //SLAVE 不可用的
            case SLAVE_NOT_AVAILABLE:
                response.setCode(ResponseCode.SLAVE_NOT_AVAILABLE);
                sendOK = true;
                break;
       .....还有一些其他的状态,可以到org.apache.rocketmq.broker.processor.SendMessageProcessor#handlePutMessageResult查看
 }

Broker 端接收消息发送的处理就到这了,我们回过头看下同步发送消息的位置,看看是怎么同步发送的,再看看收到 Broker 返回的发送结果做了什么。

3.5.4.1.4 同步发送原理
 public RemotingCommand invokeSyncImpl(final Channel channel, final RemotingCommand request,
        final long timeoutMillis){ 
    //将请求保存到ResponseFuture中,其中 null 参数为处理 Netty 异步请求的回调接口,同步发送时为 null,后面异步发送时传递了InvokeCallback回调接口
			final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, null, null);
   //保存到本地
            this.responseTable.put(opaque, responseFuture);
            final SocketAddress addr = channel.remoteAddress();
            channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture f) throws Exception {
                    if (f.isSuccess()) {
                        responseFuture.setSendRequestOK(true);
                        return;
                    } else {
                        responseFuture.setSendRequestOK(false);
                    }

                    responseTable.remove(opaque);
                    responseFuture.setCause(f.cause());
                    responseFuture.putResponse(null);
                    log.warn("send a request command to channel <" + addr + "> failed.");
                }
            });
            // 这里就是同步方式的处理,阻塞等待请求的响应,内部就是调用countDownLatch的超时等待方法
            RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis);
          if (null == responseCommand) {
                if (responseFuture.isSendRequestOK()) {//响应成功,但是没有响应结果,说明等待超时
                    throw new RemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis,
                        responseFuture.getCause());
                } else {
                   //否则就是请求失败
                    throw new RemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause());
                }
            }
			//这里说明请求成功,返回请求结果
            return responseCommand;
}
  • 同步发送消息的原理是通过ResponseFuture内部的CountDownLatch来实现,CountDownLatch初始化为1

  • 发送请求后就调用countDownLatch.await(timeoutMillis, TimeUnit.MILLISECONDS)进行超时等待

  • 而 Netty 这边收到响应后,会根据请求 id 获取本地的ResponseFuture对象,调用putResponse保存响应结果,同时会在putResponse中调用一次countDownLatch.countDown(),此次如果没有超时,则就会从waitResponse中返回。

    public void putResponse(final RemotingCommand responseCommand) {
            this.responseCommand = responseCommand;
            this.countDownLatch.countDown();
        }
    
3.5.4.1.5 发送结果处理
 private SendResult sendMessageSync(
        final String addr,
        final String brokerName,
        final Message msg,
        final long timeoutMillis,
        final RemotingCommand request
    ) throws RemotingException, MQBrokerException, InterruptedException {
        RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis);
        assert response != null;
    //拿到请求结果后,这里开始封装返回结果
        return this.processSendResponse(brokerName, msg, response);
    }

最终返回的是SendResult对象,也就是我们调用 producer 后返回的发送结果对象。

  private SendResult processSendResponse(
        final String brokerName,
        final Message msg,
        final RemotingCommand response
    ) throws MQBrokerException, RemotingCommandException {
        SendStatus sendStatus;
     //获取发送结果,消息刷盘状态
        switch (response.getCode()) {
            case ResponseCode.FLUSH_DISK_TIMEOUT: {
                sendStatus = SendStatus.FLUSH_DISK_TIMEOUT;
                break;
            }
            case ResponseCode.FLUSH_SLAVE_TIMEOUT: {
                sendStatus = SendStatus.FLUSH_SLAVE_TIMEOUT;
                break;
            }
            case ResponseCode.SLAVE_NOT_AVAILABLE: {
                sendStatus = SendStatus.SLAVE_NOT_AVAILABLE;
                break;
            }
            case ResponseCode.SUCCESS: {
                sendStatus = SendStatus.SEND_OK;
                break;
            }
            default: {
                throw new MQBrokerException(response.getCode(), response.getRemark());
            }
        }

        SendMessageResponseHeader responseHeader =
                (SendMessageResponseHeader) response.decodeCommandCustomHeader(SendMessageResponseHeader.class);

        //If namespace not null , reset Topic without namespace.
        String topic = msg.getTopic();
        if (StringUtils.isNotEmpty(this.clientConfig.getNamespace())) {
            topic = NamespaceUtil.withoutNamespace(topic, this.clientConfig.getNamespace());
        }

        MessageQueue messageQueue = new MessageQueue(topic, brokerName, responseHeader.getQueueId());
		//获取消息唯一ID
        String uniqMsgId = MessageClientIDSetter.getUniqID(msg);
        if (msg instanceof MessageBatch) {
            StringBuilder sb = new StringBuilder();
            for (Message message : (MessageBatch) msg) {
               //批量消息拼接消息 ID
                sb.append(sb.length() == 0 ? "" : ",").append(MessageClientIDSetter.getUniqID(message));
            }
            uniqMsgId = sb.toString();
        }
     //封装SendResult
        SendResult sendResult = new SendResult(sendStatus,//发送结果状态
                uniqMsgId,//唯一 ID
                responseHeader.getMsgId()//消息 id
                , messageQueue, responseHeader.getQueueOffset()/**队列 offset**/;
        sendResult.setTransactionId(responseHeader.getTransactionId());//如果事务消息的话,这里会设置事务 id
        String regionId = response.getExtFields().get(MessageConst.PROPERTY_MSG_REGION);
        String traceOn = response.getExtFields().get(MessageConst.PROPERTY_TRACE_SWITCH);
        if (regionId == null || regionId.isEmpty()) {
            regionId = MixAll.DEFAULT_TRACE_REGION_ID;
        }
        if (traceOn != null && traceOn.equals("false")) {//消息轨迹开关
            sendResult.setTraceOn(false);
        } else {
            sendResult.setTraceOn(true);
        }
        sendResult.setRegionId(regionId);
        return sendResult;
    }

这里很简单,就是将返回结果封装为SendResult对象返回给 Producer,主要包括此次发送的结果、消息 id、队列 offset、事务 id等信息。

回到消息发送的最外层:

for (; times < timesTotal; times++) { 
   //每次都会重新选择队列,且会尽量避开上次不可用的 broker
    MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
   ....
	switch (communicationMode) {
         case SYNC:
           if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
              //如果发送结果不成功,即存储不成功,且retryAnotherBrokerWhenNotStoreOK=true,则进行重试,直到超过最大发送次数(1次初始发送+2次重投=3次)
               if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
                          continue;
                     }
             }
				return sendResult;
 			default:
             break;
   }
}
  • 发送不成功时,retryAnotherBrokerWhenNotStoreOK必须为 true 才会重投。否则直接返回
3.5.4.2 异步发送

异步发送无非是比同步发送多了一个回调,发送成功后也无需等待发送结果,发送结果通过回调接口进行触发。

回调中有两个方法,一个是成功时的回调,一个是失败时的回调,其中失败回调就是在调用DefaultMQProducerImpl 的 send 方法中,将发送方法 try 住,在 catch 中调用 sendCallback 的 onException 方法

 public void send(final Message msg, final SendCallback sendCallback, final long timeout)
        ... {
        final long beginStartTime = System.currentTimeMillis();
        ExecutorService executor = this.getAsyncSenderExecutor();
        try {
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    long costTime = System.currentTimeMillis() - beginStartTime;
                    if (timeout > costTime) {
                        try {
                            sendDefaultImpl(msg, CommunicationMode.ASYNC, sendCallback, timeout - costTime);
                        } catch (Exception e) {
                           //这里调用异常回调
                            sendCallback.onException(e);
                        }
                    } else {
                       //超时异常
                        sendCallback.onException(
                            new RemotingTooMuchRequestException("DEFAULT ASYNC send call timeout"));
                    }
                }

            });
        } catch (RejectedExecutionException e) {
            throw new MQClientException("executor rejected ", e);
        }

    }

回到3.5.4的 switch 分支,找到异步发送消息的代码:

 switch (communicationMode) {
            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;
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 {
        final long beginStartTime = System.currentTimeMillis();
   //异步调用消息发送,Netty 通过 ResponseFuture 实现
        this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() {
            @Override
            public void operationComplete(ResponseFuture responseFuture) {
               //调用成功后会进入该回调
                long cost = System.currentTimeMillis() - beginStartTime;
               //拿到响应
                RemotingCommand response = responseFuture.getResponseCommand();
               ....

                if (response != null) {
                    try {
                       //解析拿到发送结果
                        SendResult sendResult = MQClientAPIImpl.this.processSendResponse(brokerName, msg, response);
                        assert sendResult != null;
                        if (context != null) {
                            context.setSendResult(sendResult);
                            context.getProducer().executeSendMessageHookAfter(context);
                        }

                        try {
                           //**在这里进行成功的回调,producer 端就可以收到了
                            sendCallback.onSuccess(sendResult);
                        } catch (Throwable e) {
                        }
                       
							//故障延迟机制更新条目(见第五节)
                    producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), false);

                    } catch (Exception e) {
                       //异常时更新失败条目(见第五节)
                       producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), true);

                          //如果出现异常了,则在onExceptionImpl进行重试,如果超过重试次数或是不可重试异常,则直接调用sendCallback的 onException 回调
                        onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance,
                            retryTimesWhenSendFailed, times, e, context, 
                                        false/**这个参数标识此异常是否需要重试**/, producer);
                    }
                } else {
                    ....
                    if (!responseFuture.isSendRequestOK()) {
                        MQClientException ex = new MQClientException("send request failed", responseFuture.getCause());
                       //下面的都是一些可重试的异常
                        onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance,
                            retryTimesWhenSendFailed, times, ex, context, true, producer);
                    } .....
                }
            }
        });
    }

异步消息要点:

  • 消息发送的最外层使用了 try-catch 包裹,抛出的异常会在这里捕获并调用回调函数的 onException接口处理;

  • 消息使用 Netty 进行异步发送,实现如下:

     this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() {
         @Override
                public void operationComplete(ResponseFuture responseFuture) {
                }
     })
    

    【了解】Netty 发送请求后,将请求信息和回调接口InvokeCallback保存在ResponseFuture中,并将ResponseFuture缓存到本地表中,在operationComplete中接收发送结果,Netty 通过 Channel 发送收到响应后,会根据请求 id 拿到对应的ResponseFuture并拿到里面的回调函数InvokeCallback调用operationComplete进行处理;

  • 如果请求成功,则调用sendCallback.onSuccess(sendResult);通知 Producer 成功

  • 如果未发送成功,则会进行重投重投次数由retryTimesWhenSendFailed参数决定,该参数可以在发送消息前通过 Producer 设置;

  • 如果超过最大重投次数,则调用sendCallback.onException(sendResult); 通知异常

  • 否则进行消息重新投递,异步消息重投仍然会选用同一个 broker

3.5.4.3 单向发送

单向发送更简单,producer 发送出去后,不需要等待返回结果,因此不关心是否发送成功,也不用重试。

核心 Netty 发送单向消息请求代码如下:

  public void invokeOnewayImpl(final Channel channel, final RemotingCommand request, final long timeoutMillis)
        ...{
     ....
            try {
                channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture f) throws Exception {
                        once.release();
                        if (!f.isSuccess()) {
                           //没有成功只是打印了 warn 日志
                            log.warn("send a request command to channel <" + channel.remoteAddress() + "> failed.");
                        }
                    }
                });
            } catch (Exception e) {
                log.warn("write send a request command to channel <" + channel.remoteAddress() + "> failed.");
                throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e);
            }
        } else {
            if (timeoutMillis <= 0) {
                throw new RemotingTooMuchRequestException("invokeOnewayImpl invoke too fast");
            } else {
                String info = String.format(
                    "invokeOnewayImpl tryAcquire semaphore timeout, %dms, waiting thread nums: %d semaphoreAsyncValue: %d",
                    timeoutMillis,
                    this.semaphoreOneway.getQueueLength(),
                    this.semaphoreOneway.availablePermits()
                );
                log.warn(info);
                throw new RemotingTimeoutException(info);
            }
        }
    }

如果响应不成功,只是打印日志,如果出现异常,则直接抛出,在最外层的 for 循环中,因为单向模式默认的 totalTimes 也是1,因此异常了也就异常了,也不会继续重投。

四、批量消息

由于批量消息的一些处理有些不同,顺带着也了解下批量消息的一些特殊性,因此单拉一节分析。

以下是example 模块的批量消息示例:

public class SimpleBatchProducer {

    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("BatchProducerGroupName");
        producer.start();

        //If you just send messages of no more than 1MiB at a time, it is easy to use batch
        //Messages of the same batch should have: same topic, same waitStoreMsgOK and no schedule support
        String topic = "BatchTest";
        List<Message> messages = new ArrayList<>();
        messages.add(new Message(topic, "Tag", "OrderID001", "Hello world 0".getBytes()));
        messages.add(new Message(topic, "Tag", "OrderID002", "Hello world 1".getBytes()));
        messages.add(new Message(topic, "Tag", "OrderID003", "Hello world 2".getBytes()));

        producer.send(messages);
    }
}

从注释可以了解到,批量消息有以下限制:

  1. 一批消息不可超过1MB【实际是4MB】,当然可以设置默认的批次大小限制
  2. 同一批消息必须在同一个 topic相同的 waitStoreMsgOK
  3. 不能为延迟消息
  4. 不能为事务消息
  5. 不支持消息重试【消费端】

这种方式虽然可以最大程度减少 IO,但是如果一批消息太大是无法正常发送的,我们可以通过producer.setMaxMessageSize()设置 producer 发送消息大小,还需要在 broker 设置maxMessageSize参数,指定 broker 可接收的最大消息大小;

但是也不能太大,如果太大的话,肯定会影响其他线程发送消息的响应时间。

批量消息发送的方式其实和同步模式基本一样,如下:

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

public SendResult send(Message msg,
        long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    //最终发送模式也是使用SYNC同步
        return this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout);
    }

只不过在入口处将消息集合转为了批量消息MessageBatch进行适配:batch(msgs)

MessageBatch继承了Message对象,因此可以使用this.defaultMQProducerImpl.send()进行消息发送

public class MessageBatch extends Message implements Iterable<Message> {

    private final List<Message> messages;
}

Message对象存储消息体就使用了一个字节数组:private byte[] body;

因此我们主要关注如何转为MessageBatch,并且如何对批量消息进行编码存储到byte[] body中的。

4.1 批量消息打包

private MessageBatch batch(Collection<Message> msgs) throws MQClientException {
        MessageBatch msgBatch;
        try {
            //1.将Message集合转为MessageBatch对象,同时进行校验
            msgBatch = MessageBatch.generateFromList(msgs);
            for (Message message : msgBatch) {
                //校验每一个消息,这里和普通的消息发送校验一样,如3.5.1
                Validators.checkMessage(message, this);
                //生成唯一id,批量消息在这里就统一生产了,因此才有了前面同步发送时的一段逻辑,如3.5.2节
                MessageClientIDSetter.setUniqID(message);
                message.setTopic(withNamespace(message.getTopic()));
            }
            //2.对批量消息进行编码,放到byte[]body中
            msgBatch.setBody(msgBatch.encode());
        } catch (Exception e) {
            throw new MQClientException("Failed to initiate the MessageBatch", e);
        }
        msgBatch.setTopic(withNamespace(msgBatch.getTopic()));
        return msgBatch;
    }

4.1.1 校验批量消息

 public static MessageBatch generateFromList(Collection<Message> messages) {
        assert messages != null;
        assert messages.size() > 0;//必须大于0
        List<Message> messageList = new ArrayList<Message>(messages.size());
        Message first = null;
        for (Message message : messages) {
            //不能设置延迟等级,即不能是延迟消息
            if (message.getDelayTimeLevel() > 0) {
                throw new UnsupportedOperationException("TimeDelayLevel is not supported for batching");
            }
            //不能是重试topic,即不支持重试
            if (message.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                throw new UnsupportedOperationException("Retry Group is not supported for batching");
            }
            if (first == null) {
                first = message;
            } else {
                //后面每个消息的topic都必须与第一个保持一致,即批量消息每条消息的topic都必须一致
                if (!first.getTopic().equals(message.getTopic())) {
                    throw new UnsupportedOperationException("The topic of the messages in one batch should be the same");
                }
                //批量消息每条消息的WAIT_STORE_MSG_OK属性都必须保持一致
                if (first.isWaitStoreMsgOK() != message.isWaitStoreMsgOK()) {
                    throw new UnsupportedOperationException("The waitStoreMsgOK of the messages in one batch should the same");
                }
            }
            messageList.add(message);
        }
        MessageBatch messageBatch = new MessageBatch(messageList);

        messageBatch.setTopic(first.getTopic());
        messageBatch.setWaitStoreMsgOK(first.isWaitStoreMsgOK());
        return messageBatch;
    }

这里可以看到前面说的几个限制条件,由于采用和SYNC相同的发送方法,因此,还会走到3.1节消息校验的流程,这里会对编码后的批量消息进行判断,如果超过4M则不允许

4.1.2 批量消息编码

//MessageBatch#encode 
public byte[] encode() {
        return MessageDecoder.encodeMessages(messages);
    }
 public static byte[] encodeMessages(List<Message> messages) {
        //TO DO refactor, accumulate in one buffer, avoid copies
     //保存编码后的每条消息
        List<byte[]> encodedMessages = new ArrayList<byte[]>(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;
     //将编码后的都拷贝到最终的allBytes中
        for (byte[] bytes : encodedMessages) {
            System.arraycopy(bytes, 0, allBytes, pos, bytes.length);
            pos += bytes.length;
        }
        return allBytes;
    }
  • 对每条消息进行编码保存到字节数组集合中
  • 根据所有编码后的消息的总长度创建最终的字节数组
  • 将编码后的消息字节拷贝到最终的字节数组中返回,这就是最后保存到Message的body中的消息体编码数据

最后重点看下如何对每条Message进行编码的:

public static byte[] encodeMessage(Message message) {
        //only need flag, body, properties 只对消息体、flag和属性进行编码
    //消息体
        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;
    //flag
        int sysFlag = message.getFlag();
    	//计算总大小
    	//总长度(int 4字节)+
        int storeSize = 4 // 1 TOTALSIZE 总长度 int
            + 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();
    }

从以上代码可以分析,每条消息存储的结构为:

  • 总长度,4字节,int,()
  • MAGICCODE和BODYCRC各占4字节,总8字节
  • 消息flag,4字节int
  • 消息体长度标识,4字节int
  • 消息数据实际长度,message.getBody().length,存储消息数据
  • 属性长度标识,2字节short
  • 属性实际长度,propertiesBytes.length,存储拼接好的消息属性内容

后面解析的时候也会按照此结构解析,具体位置在存储消息的时候进行解析:

org.apache.rocketmq.store.CommitLog.MessageExtBatchEncoder#encode(final MessageExtBatch messageExtBatch)

encode()中做的主要是对请求中的消息进行解码,然后加上其他消息信息重新编码储存到MessageExtBatch的ByteBuffer encodedBuff;中,后面同步到磁盘的 commitlog 文件。可以自行了解下,这里不细讲,其中MessageExtBatch是在 Broker 收到请求后将请求信息转成MessageExtBatch对象的,继承了MessageExt,MessageExt继承了 Message。

五、顺序消息

顺序消息的实现其实是要 producer 与 consumer 搭配使用的,这里我们只关注 producer 端如何处理。

在1.1.5中,在发送消息的时候添加了一个MessageQueueSelector的参数,主要用于自定义选择发送队列,如下:

for (int i = 0; i < 3; i++) {
                int orderId = i;

                for(int j = 0 ; j <3 ; j ++){
                    Message msg =
                            new Message("OrderTopicTest", "order_"+orderId, "KEY" + orderId,
                                    ("order_"+orderId+" step " + j).getBytes(RemotingHelper.DEFAULT_CHARSET));
                    SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                        @Override
                        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                            Integer id = (Integer) arg;
                            int index = id % mqs.size();
                            return mqs.get(index);
                        }
                    }, orderId);

                    System.out.printf("%s%n", sendResult);
                }
            }

因此,与同步消息不同的,其实也就只要在选择 MessageQueue 的时候不同,

 private SendResult sendSelectImpl(
        Message msg,
        MessageQueueSelector selector,
        Object arg,
        final CommunicationMode communicationMode,
        final SendCallback sendCallback, final long timeout
    ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
        long beginStartTime = System.currentTimeMillis();
        this.makeSureStateOK();
        //检查消息
        Validators.checkMessage(msg, this.defaultMQProducer);

        TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
        if (topicPublishInfo != null && topicPublishInfo.ok()) {
            MessageQueue mq = null;
            try {
               //拿到 topic 下的队列列表
                List<MessageQueue> messageQueueList =
                    mQClientFactory.getMQAdminImpl().parsePublishMessageQueues(topicPublishInfo.getMessageQueueList());
                Message userMessage = MessageAccessor.cloneMessage(msg);
                String userTopic = NamespaceUtil.withoutNamespace(userMessage.getTopic(), mQClientFactory.getClientConfig().getNamespace());
                userMessage.setTopic(userTopic);
					//主要是这里,调用selector.select,按照我们自定义的方式选择一个队列
                mq = mQClientFactory.getClientConfig().queueWithNamespace(selector.select(messageQueueList, userMessage, arg));
            } catch (Throwable e) {
                throw new MQClientException("select message queue threw exception.", e);
            }

            long costTime = System.currentTimeMillis() - beginStartTime;
            if (timeout < costTime) {
                throw new RemotingTooMuchRequestException("sendSelectImpl call timeout");
            }
            if (mq != null) {
               //核心发送代码和前面一样
                return this.sendKernelImpl(msg, mq, communicationMode, sendCallback, null, timeout - costTime);
            } else {
                throw new MQClientException("select message queue return null.", null);
            }
        }

        ...
        throw new MQClientException("No route info for this topic, " + msg.getTopic(), null);
    }

相比其他发送方式,带自定义选择队列策略的发送方式有以下不同:

  • 支持自定义选择 MessageQueue
  • 没有重试,当然也就没有了 broker 故障规避一些东西,但是超时还是有的
  • 因此发送出现异常是直接抛出

六、Broker 的故障延迟机制

接3.3 选择 MessageQueue 部分

当开启了故障延迟,sendLatencyFaultEnable=true 时,走故障延迟逻辑。

在这之前,先了解下 Broker 故障延迟的设计架构:

接口设计

LatencyFaultTolerance是故障延迟实现的核心接口:

public interface LatencyFaultTolerance<T> {
   //更新失败的数据,这里 name 就是 brokerName
    void updateFaultItem(final T name, final long currentLatency, final long notAvailableDuration);
	//当前 broker 是否可用
    boolean isAvailable(final T name);
	//从不可用表中移除某个元素(broker)
    void remove(final T name);
	//选择至少一个可用的 broker
    T pickOneAtLeast();
}

实现类为:LatencyFaultToleranceImpl

public class LatencyFaultToleranceImpl implements LatencyFaultTolerance<String> {
    private final ConcurrentHashMap<String, FaultItem> faultItemTable = new ConcurrentHashMap<String, FaultItem>(16);

    private final ThreadLocalIndex whichItemWorst = new ThreadLocalIndex();
   //更新失败的条目
   @Override
    public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration) {
       //获取旧的失败条目
        FaultItem old = this.faultItemTable.get(name);
        if (null == old) {
           //不存在就创建
            final FaultItem faultItem = new FaultItem(name);
           //设置故障延迟时间和
            faultItem.setCurrentLatency(currentLatency);
            faultItem.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
				//更新
            old = this.faultItemTable.putIfAbsent(name, faultItem);
            if (old != null) {
                old.setCurrentLatency(currentLatency);
                old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
            }
        } else {//旧的存在,直接更新
            old.setCurrentLatency(currentLatency);
            old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
        }
    }
}

faultItemTable维护了失败的 broker 信息,封装到了FaultItem中【见下】。

pickOneAtLeast:

该方法会尝试从故障的 broker 列表中选择一个可用的 broker,主要通过对FaultItem列表进行打乱顺序后重新排序,排序后,如果存在可用的 broker,则会被排在靠前的位置(见下文FaultItem的 compare 方法),然后从中取一个(当然也可能取到的是不可用的)

 public String pickOneAtLeast() {
        final Enumeration<FaultItem> elements = this.faultItemTable.elements();
        List<FaultItem> tmpList = new LinkedList<FaultItem>();
        while (elements.hasMoreElements()) {
            final FaultItem faultItem = elements.nextElement();
            tmpList.add(faultItem);
        }

        if (!tmpList.isEmpty()) {
           //打乱顺序
            Collections.shuffle(tmpList);
				//重新排序
            Collections.sort(tmpList);

            final int half = tmpList.size() / 2;
            if (half <= 0) {
                return tmpList.get(0).getName();
            } else {
                final int i = this.whichItemWorst.getAndIncrement() % half;
                return tmpList.get(i).getName();
            }
        }

        return null;
    }

FaultItem

FaultItem封装了失败的 Broker 信息:

有三个属性:

  • name: brokerName,即失败的 broker 名称

  • currentLatency:此次消息发送的延迟时间

  • startTimestamp:可以理解为故障结束的时间,即当前 broker 可用的时间。

    当 broker 故障时,会根据发送消息耗时计算broker 故障延迟的时间,保存到currentLatencystartTimestamp=故障发生时间+currentLatency,即在startTimestamp之前,该 broker 都被认为不可用,producer 不会选择该 broker 进行消息发送。

是否可用判断:

  • isAvailable()

    当前时间大于startTimestamp时,故障恢复,当前 broker 可以继续使用(不代表真的恢复了,只是可以继续参与MessageQueue 的选择)

FaultItem继承了Comparable接口,在 compare 方法中,会将可用的靠前,不存在可用的就将故障延迟时间端的靠前,否则就将最终可用时间最近的靠前。

class FaultItem implements Comparable<FaultItem> {
        private final String name;
        private volatile long currentLatency;
        private volatile long startTimestamp;

        public FaultItem(final String name) {
            this.name = name;
        }

        public boolean isAvailable() {
            return (System.currentTimeMillis() - startTimestamp) >= 0;
        }
 @Override
        public int compareTo(final FaultItem other) {
           //如果有可用的 broker,则可用的 broker 靠前
            if (this.isAvailable() != other.isAvailable()) {
                if (this.isAvailable())
                    return -1;

                if (other.isAvailable())
                    return 1;
            }
				//延迟时间小的靠前
            if (this.currentLatency < other.currentLatency)
                return -1;
            else if (this.currentLatency > other.currentLatency) {
                return 1;
            }
				//可用时间小的靠前
            if (this.startTimestamp < other.startTimestamp)
                return -1;
            else if (this.startTimestamp > other.startTimestamp) {
                return 1;
            }
		
            return 0;
        }
		.....
}

故障处理时机

通过接口,我们知道主要是通过updateFaultItem方法进行更新失败条目的,而更新时机,就是在发送消息出现异常时进行调用,以及发送成功后也会调用。

其中,同步发送消息成功是在sendKernelImpl之后,如下,而异步发送消息成功或失败调用updateFaultItem是在那个异步请求回调中进行,见3.5.4.2异步发送一节。

  private SendResult sendDefaultImpl(
        Message msg,
        final CommunicationMode communicationMode,
        final SendCallback sendCallback,
        final long timeout
    ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
      ....
            for (; times < timesTotal; times++) {
                String lastBrokerName = null == mq ? null : mq.getBrokerName();
                //选择队列
                MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
                if (mqSelected != null) {
                    mq = mqSelected;
                   
                    try {
                        beginTimestampPrev = System.currentTimeMillis();
                      ......
                        //实际发送消息的方法
                        sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
                        endTimestamp = System.currentTimeMillis();
                       //没有异常时调用,最后一个参数为 false
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
                    }catch (RemotingException e) {
                        endTimestamp = System.currentTimeMillis();
                       //异常时调用,最后一个参数为 true
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                       ....
                        continue;
                    }
                   ........各种 catch 中也会调用updateFaultItem............
                }
            }
        }
  }

调用方法如下,为DefaultMQProducerImplupdateFaultItem方法,最终会调用 LatencyFaultTolerance接口 的updateFaultItem方法:

//这里的currentLatency是本次发送消息的耗时:发送结束时间/异常捕获到的时间 - 消息开始发送时间  
public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
        this.mqFaultStrategy.updateFaultItem(brokerName, currentLatency, isolation);
    }
//MQFaultStrategy#updateFaultItem
 public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
        if (this.sendLatencyFaultEnable) {
            long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);
           //调用latencyFaultTolerance接口的updateFaultItem
            this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration);
        }
    }

其中,有个方法:computeNotAvailableDuration,用于计算不可能的时间保存到FaultItemcurrentLatency中,看下如何计算:

public class MQFaultStrategy{
   private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
    private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};
   
private long computeNotAvailableDuration(final long currentLatency) {
        for (int i = latencyMax.length - 1; i >= 0; i--) {//
            if (currentLatency >= latencyMax[i])
                return this.notAvailableDuration[i];
        }

        return 0;
    }
}

从latencyMax尾部开始,找到第一个比当前耗时currentLatency小的索引 i,然后从不可用周期数组中选择故障延迟的时间

其中currentLatency取值由isolation决定:

  • isolation=true

    发送失败(出现异常)时传 true。currentLatency=3000L,耗时默认为30s,根据30s 获取不可用时长,对应180000L,该 broker 在180000L内就不可用

  • isolation=false

    发送过程没有异常(可能成功,也可能时broker 返回刷盘异常相关错误)传 false。根据消息发送耗时进行计算不可用时长,即如果失败,发送耗时越久,接下来不可用时间越长

那么你可能疑惑了,为什么发送没有出现异常也要设为故障 broker?别忘了,默认的超时时间为3s,如果超时必然抛超时异常,异常时更新失败条目isolation为 true,而如果没有超时,哪怕正好用了3s,latencyMax从尾部开始第一个小于3s 的 index=1,对应notAvailableDuration的值为0,因此计算出来的故障时长duration为0,在是否存活判断中,就必定认为存活。

因此你可以理解为,无论发送是否出错,都会将 broker 加入到失败条目中,只不过没有出错时,其故障时间为0,就相当于该 broker 是可用的,下次发送依然会加入队列选择。

最后回过头看看开启故障机制时,是如何选择 MessageQueue 的:

选择 MessageQueue

 if (this.sendLatencyFaultEnable) {
            try {
                //1.这里选择MessageQueue的方法和没有开启故障机制一样的,就是自增,然后取模
                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);
                    //1.1判断当前 broker 是否可用
                    if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
                       //1.1.1如果第一次发送,则lastBrokerName=null,直接返回该队列;否则,如果当前 broker 等于上次失败的 broker,说明该 broker 已经恢复故障,也可直接返回该队列
                        if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
                            return mq;
                    }
                }
					//2.如果没有可用的broker,则尝试从故障 broker 列表中选择一个很快可用的 broker 返回
                final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
               //根据 broker 拿到对应的写队列数量,相当于查询该 broker 是否还存在
                int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
                if (writeQueueNums > 0) {//>0说明找到了在该 broker 中的 topic
                    //然后又轮序取一个队列?这块没太明白?。,
                    final MessageQueue mq = tpInfo.selectOneMessageQueue();
                    if (notBestBroker != null) {
                       //重置它的 broker 信息为该 broker?
                        mq.setBrokerName(notBestBroker);
                        mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
                    }
                   //返回
                    return mq;
                } else {
                   //该 broker 不存在,直接移除?这里是因为如果TopicPublishInfo#TopicRouteData没有该 broker,说明其已经下线了,所以故障延迟列表中要直接移除吗
                    latencyFaultTolerance.remove(notBestBroker);
                }
            } catch (Exception e) {
                log.error("Error occurred when selecting message queue", e);
            }
 }
  • 如果开启了故障延迟机制,其做法也是轮询的从 MessageQueue集合中获取一个队列,判断所在 broker 是否可用;

    如果可用,这里又判断是否为上次失败的broker,如果是第一次或为上次失败的 broker,且可用,才返回该队列。

    第一步相当于,在重试的情况下,即使非上次失败的 broker可用也不会直接返回,仅仅为了判断上次失败的 broker 是否可用,不可用的话就进入到第2步;

    这里其实是有些不合理的,既然已经可用了,为什么不直接返回呢,还要再走第二步?我这里分析的是4.7.1版本,抱着疑惑的态度我下载了较新的4.9.2版本,发现已经修改了这部分逻辑,如下:

     			int index = tpInfo.getSendWhichQueue().incrementAndGet();
                    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()))
                           //4.9.0版本后,轮询选择队列,如果broker 可用,直接返回
                            return mq;
                    }
    
                   ....
    
  • 当没有可用的 broker 时,走到了第二步,说实话这一块我是不太理解的,先放着

总之 broker 故障延迟机制相对默认机制的区别为,故障延迟机制下,当发送消息出现了异常,会根据此次发送耗时决定该 broker接下来不可用的时长,在这段时间内,无论发多少次消息都会规避该 broker,而默认机制下,仅仅会在下一次规避,下下次就会再次尝试参与轮询选择

总结

Producer发送消息的方式有多种:

  • 同步
  • 异步
  • 批量
  • 单向
  • 顺序
  • 事务
  • 广播
  • 过滤消息
  • 消息轨迹

等等,由于本文主要介绍消息发送的核心原理,因此只对同步、异步、批量、单向、顺序几个基本的发送方式做了介绍,其他的如事务消息比较复杂,涉及东西较多会在之后有时间再做分析。

Producer 端处理发送有两大步骤,1.启动 producer,2.执行发送

producer 启动

Producer 启动,主要关注几个核心类:非事务的生产者对象DefaultMQProducer、producer实现类DefaultMQProducerImpl以及MQClientInstance

DefaultMQProducer其实就是定义了 producer 的一些核心参数,如发送超时时间、生产组、默认 topic 队列数等,详见1.2节

DefaultMQProducerImpl

该类主要提供了和 producer 有关的方法入口,如消息发送、创建 topic、选择 MessageQueue等,另外缓存了该 producer 有关的 topic 路由信息。但所有这些方法的核心实现,其实都是由MQClientInstance来实现

MQClientInstance

客户端实例,这才是核中核,producer 与 consumer 启动时,最终都是生成了一个MQClientInstance实例,因此它就可以看作一个 producer 实例,produce 核心的启动也是在该类中实现的。

其内部维护的核心数据大概有以下这些:

  • 客户端 id。ip@进程号

  • producer 缓存表,key为生产组,value 为DefaultMQProducerImpl

  • consumer缓存表,key 为消费组,value 为消费者的拉和推两个实现类

  • netty 客户端配置

  • 客户端 API 实现类,MQClientAPIImpl,用于通过 Netty 与外部通信调用,如发送消息给 broker,拉取消息从 NameServer、发送心跳给 broker 等等一大堆通信实现

  • 路由缓存表,key:topic 名,value:TopicRouteData,这些都是从 NameServer 拉取后缓存起来的数据

  • broker 的一些缓存表,TopicRouteData中带的 broker 信息缓存起来的

  • 网络请求处理器ClientRemotingProcessor

    用于处理从 broker 或其他端发送来的网络请求,主要的如:事务的回查请求

  • 拉消息服务PullMessageService和rebalance 服务RebalanceService等,这些和消费有关的,本节不关注;

其他的,还有一些比较重要的定时任务,如定时从 NameServer 拉取 topic 信息等,详见2.3节

消息发送

消息发送这块,其实我们可以拆分为几个问题来进行总结:

  1. 路由信息的拉取机制;

  2. 拿到 topic 队列后如何进行队列选择?

  3. 消息发送如何保证高可用性?重试?故障规避?

  4. 同步、异步在实现上有何差异性?【3.5.4.1.4和3.5.4.1.5节】

  5. 普通消息与批量消息有合差异性?为什么可以共用相当于的发送逻辑?

    因为批量消息MessageBatch继承了Message类,并通过自定义规则对所有消息进行统一编码合并为一个字节数组,这样就可以保存到 Message 的消息体字段byte[] body中进行传输了,然后在 Broker 端对批量消息进行特殊处理,按编码规格进行统一解码即可

  6. Broker 端收到消息后如何进行处理?

我们一个个捋

路由消息拉取机制

  • 首先,路由信息持久化在 broker 端,broker 每30s 像 NameServer 发送一次心跳,同时携带 topic 路由信息,缓存到 NameServer 端,如果topic变化就会更新 NameServer 端路由信息;

  • 接着,producer 启动时,MQClientInstance中有个定时任务每30s 从 NameServer 拉取一次消息,更新到本地缓存;

  • 最后,producer 会先从本地查询 topic 信息,如果查询不到,则从 NameServer 获取【方式同定时任务拉取】;

  • 如果没有查询到原topic 信息,则根据默认topic 从 NameServer 拉取

  • 如果未开启自动创建主题,或查询不到 topic,则抛异常,如果查询到默认 topic,则将默认 topic 的队列数量等信息设为默认主题队列数,最后将得到的默认topic对象TopicRouteData克隆后作为原 topic作为 key更新到本地缓存。

topic 整体流转结构如下图:

在这里插入图片描述

队列选择与消息发送的高可用

队列选择分三类:

  • 使用自定义选择策略
  • 未使用自定义策略,其未开启 broker 故障延迟机制,按默认机制选择;
  • 未使用自定义策略,但开启 broker 故障延迟机制
  • 默认情况下都是通过轮询机制选择,如果被规避掉,则继续轮询下一个。

因为队列选择与producer 端高可用相关,因此放一起总结

高可用:

  • 使用自定义选择策略时,没有重试机制

  • 其他默认情况下,producer 发送失败基本都会进行重试;

    • 同步方式重试次数由retryTimesWhenSendFailed参数决定,默认2次;
    • 异步方式重试次数由retryTimesWhenSendAsyncFailed参数决定,默认2次;
    • 同步的重试是在消息发送的外层通过 for 循环实现;
    • 异步的重试是在异步回调接口InvokeCallback#operationComplete中处理,没超过重试次数就再执行一次发送
  • 故障规避

    • 默认规避机制

      每次发送失败会记录上次失败的 broker,在第一次重试时规避掉该 broker,选择下一个;

      但是在第二次重试时,仅会规避第一次重试的 broker,而初始发送失败的 broker 会正常加入轮询选择;

      即每次失败的 broker 仅会在下一次重试规避,后面恢复正常【不代表 broker 可用,仅正常加入轮询选择】

    • 开启broker 故障规避机制

      每次发送异常都会将不可用的 broker 封装成 FaultItem 维护起来,并计算其不可用时长,维护一个恢复可用的时间=当前时间+不可用时长,在选择队列的时候,会根据当前时间是否大于对应 broker 的恢复可用时长来判断该 broker 在本次是否可用;

      而发送成功时也会保存到失败条目列表,只不过不可用时长为0 ,因此就会被判定为可用的 broker 不用担心。

      与默认机制不同的就是会在接下来的一端时间内都被规避掉认为不可用

  • 单向发送没有重试机制,不保证可用性

Broker 端收到消息后的处理

  • 再次对消息 topic 等进行一次校验,校验方式同 producer 发送前校验一样

  • 创建 topic 配置, broker 端有一个路由管理器,维护了从本地store/config/topics.json 加载的 topic 路由配置信息

    如果没有找到待发送的 topic,则获取默认主题配置按照默认主题信息创建原主题配置,前提是开启了自动创建主题,否则直接返回错误;

    创建好后会将配置信息更新到 broker端路由管理器,再刷新到磁盘中,再通过 broker 心跳同步到 NameServer

  • DLQ 死信 topic处理

    消费端消费失败会进行重试,发送消息到 RETRY+消费组名的 topic 中,如果是重试消息且超过消费的重试次数则创建名为DLQ+消费组的死信 topic,将当前信息发送到该 topic,由人工手动检查处理

  • 最后就是存储消息,我们下节讲

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值