RocketMQ生产者是如何启动的?

357 阅读10分钟

引言:

从源码角度分析下,RocketMQ生产者是如何启动的?为发送消息做了哪些准备?

  1. Rocket MQ消息概况

  1. 相关概念回顾

Producer group生产者组:

同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。如果发送的是事物消息且原始生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。

一个Producer Group下包含多个Producer实例,可以是多台机器,也可以是一台机器的多个进程,或者一个进程的多个Producer对象。一个Producer Group可以发送多个Topic消息, Producer Group作用如下:标识一类Producer 可以通过运维工具查询这个发送消息应用下有多个Producer实例,发送分布式事务消息时,如果Producer中途意外宕机,Broker会主动回调Producer Group内的任意一台机器来确认 事务状态

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位置

  1. 三种消息发送

RocketMQ支持三种消息发送方式:同步/异步/one way

先来看看如何发送?

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

    DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);

    // Uncomment the following line while debugging, namesrvAddr should be set to your local address
    //producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);

    producer.start();
    for (int i = 0; i < 128; i++) {
        try {
            Message msg = new Message(TOPIC, TAG, "OrderID188", "Hello world".getBytes(StandardCharsets.UTF_8));
            // 1.同步发送
            SendResult sendResult = producer.send(msg);
            // 2. 异步发送
            producer.send(msg, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    countDownLatch.countDown();
                    System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
                }
            
                @Override
                public void onException(Throwable e) {
                    countDownLatch.countDown();
                    System.out.printf("%-10d Exception %s %n", index, e);
                    e.printStackTrace();
                }
            });
            // 3. one way
            producer.sendOneway(msg);
            System.out.printf("%s%n", sendResult);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    countDownLatch.await(5, TimeUnit.SECONDS);
    producer.shutdown();
}

2.Message的结构

基础属性包括,消息所属主题 topic ,消息 Flag(RocketMQ 不做处理)、扩展属性、消息体 、事务ID。

Flag的种类包括:

其中总共8位,每一位都有其代表的意义,可以用位运算快速得出属性

if ((sysFlag & MessageSysFlag.COMPRESSED_FLAG) == MessageSysFlag.COMPRESSED_FLAG) {
    ...
}

Message的全参数构造器如下:

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

    if (tags != null && tags.length() > 0) {
        this.setTags(tags);
    }

    if (keys != null && keys.length() > 0) {
        this.setKeys(keys);
    }

    this.setWaitStoreMsgOK(waitStoreMsgOK);
}
//Eg
Message msg = new Message("TopicTest" /* Topic */,
        "TagA" /* Tag */, "xxx", /* keys*/
        MessageSysFlag.MULTI_TAGS_FLAG, /* flag */
        ("Hello RocketMQ " +
                i).getBytes(StandardCharsets.UTF_8) /* Message body */
, true);

Message 扩展属性主要包含下面几个 。

tag :消息 TAG ,用于消息过滤 。

keys: Message 索引键, 多个用空格隔开, RocketMQ 可以根据这些 key 快速检索到消息 。

waitStoreMsgOK :消息发送时是否等消息存储完成后再返回 。

delayTimeLevel : 消息延迟级别,用于定时消息或消息重试 。

这些扩展属性存储在 Message 的 properties 中 。

那么此时一条消息,大致如图所示

  1. 生产者启动流程

RocketMQ的组件大多都遵循下图所示的流程

生产者启动时序图:

  1. 生产者实例化

从生产者构造器开始看。

DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);

public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) {
    this.defaultMQProducer = defaultMQProducer;
    this.rpcHook = rpcHook;

    this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue<>(50000);
    this.defaultAsyncSenderExecutor = new ThreadPoolExecutor(
        Runtime.getRuntime().availableProcessors(),
        Runtime.getRuntime().availableProcessors(),
        1000 * 60,
        TimeUnit.MILLISECONDS,
        this.asyncSenderThreadPoolQueue,
        new ThreadFactoryImpl("AsyncSenderExecutor_"));
    if (defaultMQProducer.getBackPressureForAsyncSendNum() > 10) {
        semaphoreAsyncSendNum = new Semaphore(Math.max(defaultMQProducer.getBackPressureForAsyncSendNum(), 10), true);
    } else {
        semaphoreAsyncSendNum = new Semaphore(10, true);
        log.info("semaphoreAsyncSendNum can not be smaller than 10.");
    }

    if (defaultMQProducer.getBackPressureForAsyncSendNum() > 1024 * 1024) {
        semaphoreAsyncSendSize = new Semaphore(Math.max(defaultMQProducer.getBackPressureForAsyncSendNum(), 1024 * 1024), true);
    } else {
        semaphoreAsyncSendSize = new Semaphore(1024 * 1024, true);
        log.info("semaphoreAsyncSendSize can not be smaller than 1M.");
    }
}

2.2生产者启动

DefaultMQProducerlmpl的 start 方法来跟踪

分别是:CREATE_JUSTRUNNINGSTART_FAILEDSHUTDOWN_ALREADY

对于producer启动来说,需要关心的状态就只有 CREATE_JUST,这也是 Producer 实例化之后默认的状态,在初始化时就会设置一个默认值,如下所示:

// 源码位置:
// 子项目: client
// 包名: org.apache.rocketmq.client.impl.producer;
// 文件: DefaultMQProducerImpl
// 行数: 111
private ServiceState serviceState = ServiceState.CREATE_JUST;

当其调用了 start() 成功之后,Producer 就会将状态修改为 RUNNING,失败了就会变成 START_FAILED

Step1: 参数校验

与快递员发快递类似,首先得检查一下地址是不是合法的,送个火星,这可办不到啊,检查 productGroup 是否符合要求;并改变生产者 的 instanceName 为进程 ID 。

检查参数是否合法

改变生产者 的 instanceName 为进程 ID

public void changeInstanceNameToPID() {
    if (this.instanceName.equals("DEFAULT")) {
        this.instanceName = UtilAll.getPid() + "#" + System.nanoTime();
    }
}

进程id+ 纳秒时间戳,这里有 nanoTime 纳秒时间戳的加持,想要得到相同 instanceName 的概率非常非常小,给大家举个具体的例子:41794#17228372610333

Step2: 创建 MQClientInstance 实例

MQClinetInstance是什么?

MQClinetInsatance是RMQ的客户端,无论是生产者还是消息者底层都会与broker进行打交道,从源码层面上,这部分的功能被抽象成一个单独的类,负责和broker打交道。

类定义在client包中,可以理解为是一个工厂,是对生产者、消费者以及控制台三者的合集,内部封装了netty客户端,消息的生产,消费和负载均衡的实现类等。另外MQClientInstance的实例化并不是直接new后使用,而是通过MQClientManager这个单例类,使用饿汉模式设计保证线程安全。

每一个clientid与 MQClientInstance一一对应。MQClientInstance封装了 RocketMQ 网络处理 API ,是消息生产者( Producer )、消息消费者( Consumer )与 NameServer、 Broker 打交道的网络通道 。

整个JVM 实例中只存在一个 MQ ClientManager 实例,维护一个 MQClientlnstance 缓存表

private ConcurrentMap<String/* clientId */, MQClientInstance> factoryTable =
    new ConcurrentHashMap<>();

如上图所示,MQClientManager采用了简单的饿汉单例模式(类加载时就创建了单例对象)。

而MQClientInstance则是通过getOrCreateMQClientInstance方法,从命名得知获取或者创建,应该是利用了factoryTable这个缓存表。

public MQClientInstance getOrCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) {
  // 生成 clientId
  String clientId = clientConfig.buildMQClientId();
  // 从这个 table 里先获取一次
  MQClientInstance instance = this.factoryTable.get(clientId);
  // 第一次进来, table 肯定没有数据, 所以它一定是 null
  if (null == instance) {
    // 所以肯定会进到这里来, 调用构造函数将其实例化出来
    instance =
      new MQClientInstance(clientConfig.cloneClientConfig(),
                           this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook);
    // 生成好之后就会写入 factoryTable 中, 所以后续再次调用这个方法就能够获取到了
    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;
}

整体流程如下图所示:

Q:为什么使用一个缓存表?

A: 避免多次调用重复生成MQClientInstance

Q: 如何保证一定返回的是对象是单例?

A:

instance =
    new MQClientInstance(clientConfig.cloneClientConfig(),
        this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook);
MQClientInstance prev = this.factoryTable.putIfAbsent(clientId, instance);

虽然concurrenthashMap#putIfAbsent可以保证,不存在就添加的原子性,但是由于上面这两句,即实例化和putIfAbsent语句并不能保证原子性。因此putIfAbsent的结果可能会有两个实例,因此使用prev变量来接受返回值。

在方法返回之前判断一下instance和prev的关系。

if (prev != null) {
    instance = prev;
    log.warn("Returned Previous MQClientInstance for clientId:[{}]", clientId);
} else {
    log.info("Created new MQClientInstance for clientId:[{}]", clientId);
}

clientld 为客户端 IP+ instance+ (unitname 可选),如果在同一台物理服务器部署两个应用程序,岂不是 clientld 相同, 会造成混乱?

为了避免这个问题 , 如果 instance 为默认值 DEFAULT 的话, RocketMQ 会自动将instance 设置为进程 ID ,这样避免了不同进程的相互影响,但同 一 个 NM 中 的不同消费者和不同生产者在启动时获取到的 MQC!ientlnstane 实例都是同 一 个 。

IP地址@{IP 地址}@{InstanceName}

public String buildMQClientId() {
    StringBuilder sb = new StringBuilder();
    sb.append(this.getClientIP());

    sb.append("@");
    sb.append(this.getInstanceName());
    if (!UtilAll.isBlank(this.unitName)) {
        sb.append("@");
        sb.append(this.unitName);
    }

    if (enableStreamRequestType) {
        sb.append("@");
        sb.append(RequestType.STREAM);
    }

    return sb.toString();
}

Step3: 向MQClientlnstance 登记 Producer 信息

Step4: 启动 MQClientInstance

接下来,Producer 会调用 MQClientInstance 的 start() 方法来初始化一些核心逻辑。

投递 Message 的很多核心逻辑都在 MQClientInstance 当中,所以我们有必要来看看这里都做了什么。我们知道 Producer 需要投递 Message 到 Broker,那么必然需要和 Broker 建立连接。Producer 也需要和 NameServer 通信来获取 Broker 的相关元数据。所以这里首先就是启动用于通信的 Channel。 除此之外还需定义如何是pull还是push消息。

case为CREATE_JUST的时候看下面的注解就是我们刚刚罗列的需要的功能点

public void start() throws MQClientException {
    synchronized (this) {
        this.serviceState = ServiceState.START_FAILED;
        // 如果没有namesrvAddr则去查找,fetchNameServerAddr方法下面再详细说
        if (null == this.clientConfig.getNamesrvAddr()) {
            this.mQClientAPIImpl.fetchNameServerAddr();
        }
        // 启动请求相应通道,打开channel
        this.mQClientAPIImpl.start();
        // 启动定时任务
        this.startScheduledTask();
        // 启动拉取消息服务
        this.pullMessageService.start();
        // 启动负载均衡服务
        this.rebalanceService.start();
        // 启动消息推送服务,注意这里又回去调用了DefaultMQProducerImpl的start方法,但是参数是false。
        this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
        this.serviceState = ServiceState.RUNNING;
    }
}
  1. 设置nameserver的地址

可以看到就是没有在config中指定的时候,就每隔30s去请求一次,调用http接口去寻址,前提需配置hosts信息,客户端默认每隔两分钟去访问一次这个http地址,并更新本地namesrvAddr地址。

org.apache.rocketmq.client.impl.MQClientAPIImpl#fetchNameServerAddr
public String fetchNameServerAddr() {
    try {
        String addrs = this.topAddressing.fetchNSAddr();
        if (!UtilAll.isBlank(addrs)) {
            if (!addrs.equals(this.nameSrvAddr)) {
                log.info("name server address changed, old=" + this.nameSrvAddr + ", new=" + addrs);
                this.updateNameServerAddressList(addrs);
                this.nameSrvAddr = addrs;
                return nameSrvAddr;
            }
        }
    } catch (Exception e) {
        log.error("fetchNameServerAddr Exception", e);
    }
    return nameSrvAddr;
}

org.apache.rocketmq.common.namesrv.DefaultTopAddressing#fetchNSAddr()
@Override
public final String fetchNSAddr() {
    if (!topAddressingList.isEmpty()) {
        for (TopAddressing topAddressing : topAddressingList) {
            String nsAddress = topAddressing.fetchNSAddr();
            if (!Strings.isNullOrEmpty(nsAddress)) {
                return nsAddress;
            }
        }
    }
    // Return result of default implementation
    return fetchNSAddr(true, 3000);
}
org.apache.rocketmq.common.namesrv.DefaultTopAddressing#fetchNSAddr(boolean, long)
public final String fetchNSAddr(boolean verbose, long timeoutMills) {
    String url = this.wsAddr;
    try {
        if (null != para && para.size() > 0) {
            if (!UtilAll.isBlank(this.unitName)) {
                url = url + "-" + this.unitName + "?nofix=1&";
            }
            else {
                url = url + "?";
            }
            for (Map.Entry<String, String> entry : this.para.entrySet()) {
                url += entry.getKey() + "=" + entry.getValue() + "&";
            }
            url = url.substring(0, url.length() - 1);
        }
        else {
            if (!UtilAll.isBlank(this.unitName)) {
                url = url + "-" + this.unitName + "?nofix=1";
            }
        }

        HttpTinyClient.HttpResult result = HttpTinyClient.httpGet(url, null, null, "UTF-8", timeoutMills);
        if (200 == result.code) {
            String responseStr = result.content;
            if (responseStr != null) {
                return clearNewLine(responseStr);
            } else {
                LOGGER.error("fetch nameserver address is null");
            }
        } else {
            LOGGER.error("fetch nameserver address failed. statusCode=" + result.code);
        }
    } catch (IOException e) {
        if (verbose) {
            LOGGER.error("fetch name server address exception", e);
        }
    }
    
    if (verbose) {
        String errorMsg =
            "connect to " + url + " failed, maybe the domain name " + MixAll.getWSAddr() + " not bind in /etc/hosts";
        errorMsg += FAQUrl.suggestTodo(FAQUrl.NAME_SERVER_ADDR_NOT_EXIST_URL);

        LOGGER.warn(errorMsg);
    }
    return null;
}
  1. 开启通信的channel

建立底层通信channel,MQClientAPIImpl.start方法最后调用RemotingClient的start方法,而remotingClient调用了netty建立底层通信。


@Override
public void start() {
    if (this.defaultEventExecutorGroup == null) {
        this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(
            nettyClientConfig.getClientWorkerThreads(),
            new ThreadFactoryImpl("NettyClientWorkerThread_"));
    }
    //netty 启动
    Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class)
        .option(ChannelOption.TCP_NODELAY, true)
        .option(ChannelOption.SO_KEEPALIVE, false)
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyClientConfig.getConnectTimeoutMillis())
        .handler(new ChannelInitializer<SocketChannel>() {
            @Override
            public void initChannel(SocketChannel ch) throws Exception {
                ChannelPipeline pipeline = ch.pipeline();
                if (nettyClientConfig.isUseTLS()) {
                    if (null != sslContext) {
                        pipeline.addFirst(defaultEventExecutorGroup, "sslHandler", sslContext.newHandler(ch.alloc()));
                        LOGGER.info("Prepend SSL handler");
                    } else {
                        LOGGER.warn("Connections are insecure as SSLContext is null!");
                    }
                }
                ch.pipeline().addLast(
                    nettyClientConfig.isDisableNettyWorkerGroup() ? null : defaultEventExecutorGroup,
                    new NettyEncoder(),
                    new NettyDecoder(),
                    new IdleStateHandler(0, 0, nettyClientConfig.getClientChannelMaxIdleTimeSeconds()),
                    new NettyConnectManageHandler(),
                    new NettyClientHandler());
            }
        });
    if (nettyClientConfig.getClientSocketSndBufSize() > 0) {
        LOGGER.info("client set SO_SNDBUF to {}", nettyClientConfig.getClientSocketSndBufSize());
        handler.option(ChannelOption.SO_SNDBUF, nettyClientConfig.getClientSocketSndBufSize());
    }
    if (nettyClientConfig.getClientSocketRcvBufSize() > 0) {
        LOGGER.info("client set SO_RCVBUF to {}", nettyClientConfig.getClientSocketRcvBufSize());
        handler.option(ChannelOption.SO_RCVBUF, nettyClientConfig.getClientSocketRcvBufSize());
    }
    if (nettyClientConfig.getWriteBufferLowWaterMark() > 0 && nettyClientConfig.getWriteBufferHighWaterMark() > 0) {
        LOGGER.info("client set netty WRITE_BUFFER_WATER_MARK to {},{}",
            nettyClientConfig.getWriteBufferLowWaterMark(), nettyClientConfig.getWriteBufferHighWaterMark());
        handler.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(
            nettyClientConfig.getWriteBufferLowWaterMark(), nettyClientConfig.getWriteBufferHighWaterMark()));
    }
    if (nettyClientConfig.isClientPooledByteBufAllocatorEnable()) {
        handler.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
    }
    // 定时任务 扫描存活的namesrv
    TimerTask timerTaskScanResponseTable = new TimerTask() {
        @Override
        public void run(Timeout timeout) {
            try {
                NettyRemotingClient.this.scanResponseTable();
            } catch (Throwable e) {
                LOGGER.error("scanResponseTable exception", e);
            } finally {
                timer.newTimeout(this, 1000, TimeUnit.MILLISECONDS);
            }
        }
    };
    this.timer.newTimeout(timerTaskScanResponseTable, 1000 * 3, TimeUnit.MILLISECONDS);

    int connectTimeoutMillis = this.nettyClientConfig.getConnectTimeoutMillis();
    // 定时任务 扫描存活的namesrv
    TimerTask timerTaskScanAvailableNameSrv = new TimerTask() {
        @Override
        public void run(Timeout timeout) {
            try {
                NettyRemotingClient.this.scanAvailableNameSrv();
            } catch (Exception e) {
                LOGGER.error("scanAvailableNameSrv exception", e);
            } finally {
                timer.newTimeout(this, connectTimeoutMillis, TimeUnit.MILLISECONDS);
            }
        }
    };
    this.timer.newTimeout(timerTaskScanAvailableNameSrv, 0, TimeUnit.MILLISECONDS);
}
  1. 启动一些定时任务
    private void startScheduledTask() {
        //如果当前客户端没有指定setNamesrvAddr,启动查找NamesrvAddr地址服务,每两分钟一次
        MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr();

        //从NameServer获取topic信息后,更新客户端topic路由信息
        MQClientInstance.this.updateTopicRouteInfoFromNameServer();

        //定时清理已经不存在的broker服务
        MQClientInstance.this.cleanOfflineBroker();
               
        //定时发送心跳服务
        MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();

        //定时做consumer offset持久化
        MQClientInstance.this.persistAllConsumerOffset();

        //定时调整消费线程池
        MQClientInstance.this.adjustThreadPool();
    } 

分析一下生产者定时任务的作用以及设置定时任务的目的。总体来说分为三点,一是减少消息发送失败的概率,,二是记录当前消息消费的状态(如果客户端是consumer的话),三.(如果客户端是consumer的话)是根据当前需要发送的消息量(即任务量)来调整线程数,以保证消息能及时消费。如果客户端是consumer的话还需要考虑后面两点

要减少消息失败的概率,可以从物理架构图入手。

站在Producer集群出发

  1. NameServer 也就是服务发现(后用sd简写)的地址变更了,或者有sd的实例宕机了,那么producer需要及时感知。否则topic broker等元数据信息就不能及时获取到了。
  2. 从sd中获取到下线的broker及时从本地缓存中清理,避免消息发送到当机宕机的broker中
作用及目的任务备注
作用:减少消息发送失败的概率查找NameServer的地址2min一次
作用:减少消息发送失败的概率扫描可用的nameserverorg.apache.rocketmq.remoting.netty.NettyRemotingClient#scanAvailableNameSrv。频率需配置这个任务是在启动Netty的时候注册的清理两种失败场景1. nameserver的地址和netty客户端缓存中的地址对不上了,进行清理
  1. 如果nameserver的地址并没有及时更新或者因为网络等其他原因建立通信channel失效了,清理netty客户端缓存的地址。 | | 作用:减少消息发送失败的概率 | 更新topic信息 | MQClientInstance是消费者和生产者共用的先看生产者部分这里就根据拿到 Topic 元数据当中的 Broker 相关数据,和本地维护的 Broker 数据进行对比,清理掉在 Topic 元数据中不存在的 Broker。具体实现在:org.apache.rocketmq.client.impl.factory.MQClientInstance#updateTopicRouteInfoFromNameServer(java.lang.String, boolean, org.apache.rocketmq.client.producer.DefaultMQProducer) | | 作用:减少消息发送失败的概率 | 向所有broker发送心跳 | 两层含义:一来告诉 Broker 我还活着,二来定时刷新 Broker 存的客户端数据。发送心跳的可以是 Producer,也可以是 Consumer,具体看谁在使用 MQClientInstance。- 如果是 Producer,那么心跳所包含的数据很少,就只有当前客户端的所有生产者组
  • 如果是 Consumer,那数据就多了,比如都有哪些消费者组的名称、消费的模式是广播还是集群、从哪里开始消费数据、消费者消费的 Topic 的简要数据等。 | | 作用:减少消息发送失败的概率 | 清理下线brokerorg.apache.rocketmq.client.impl.factory.MQClientInstance#cleanOfflineBroker | | | 作用:记录当前消息消费的状态 | 持久化消息offsetorg.apache.rocketmq.client.impl.factory.MQClientInstance#persistAllConsumerOffset | 其实就是如果当前客户端是 Consumer,就会将当前消费到哪儿了持久化起来,不然下次重启就不知道从哪里开始,从头开始?那已经消费过的消息再消费一次不就变成重复消费了吗?所以定时持久化 Offset 是非常必要的一个操作。 | | 作用:调整线程池大小 | | 1. 遍历每一个消费者组
  1. 对每一个实例的线程池调整调整的策略:消费队列数和线程数大小来衡量消费队列数如果大于线程数阈值1,说明任务多了,人少了,需要提高线程,增加干活的人消费队列数如果小于线程数阈值2,说明任务少,人多了,减少线程数,不需要那么多人干活了 |

  2. 启动拉取消息服务

  3. 启动负载均衡服务

  4. 启动推送服务

  5. 修改状态为启动态(RUNNING)

到这里生产者启动的流程就差不多了,最后贴一张类图(核心是这个MQClientInstance客户端)。

下一节将深入看看Rocket MQ是如何将一条消息发送出去的?

参考:

《Rocket MQ技术内幕》