RocketMQ学习之四----源码阅读万字总结


搭建源码环境

下载源码4.7.1版本

配置nameServer启动配置
在这里插入图片描述

环境变量

ROCKETMQ_HOME=D:\MQ\day01\rocketmq-all-4.7.1-source-release

broker启动配置

在这里插入图片描述
指定broker.conf配置文件和环境变量

nameServer启动过程

public static void main(String[] args) {
        main0(args);
    }

    public static NamesrvController main0(String[] args) {
        //K2 NameServer启动的核心组件,NamesrvController
        //类似于Web应用里的Controller,这个组件就是用来接收网络请求的。那NameServer到底要接收哪些网络请求?
        try {
            NamesrvController controller = createNamesrvController(args);
            start(controller);
            String tip = "The Name Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer();
            log.info(tip);
            System.out.printf("%s%n", tip);
            return controller;
        } catch (Throwable e) {
            e.printStackTrace();
            System.exit(-1);
        }

        return null;
    }

可以看到核心就是俩个步骤,创建一个NamesrvController,然后启动它。
controller我们知道是用来处理网络请求的。
整个NameServer就是一个Controller

 final NamesrvConfig namesrvConfig = new NamesrvConfig();
 final NettyServerConfig nettyServerConfig = new NettyServerConfig();
        
 nettyServerConfig.setListenPort(9876);

 final NamesrvController controller = 
         new NamesrvController(namesrvConfig, nettyServerConfig);
        
 controller.getConfiguration().registerConfig(properties);

 return controller;      
         

也就是下图中的这俩个

在这里插入图片描述
那么接下来就来看下启动服务

public static NamesrvController start(final NamesrvController controller) throws Exception {

        if (null == controller) {
            throw new IllegalArgumentException("NamesrvController is null");
        }
        //初始化,主要是初始化几个定时任务
        boolean initResult = controller.initialize();
        if (!initResult) {
            controller.shutdown();
            System.exit(-3);
        }

        Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                controller.shutdown();
                return null;
            }
        }));

        controller.start();

        return controller;
    }

启动也就是2步

public static NamesrvController start(final NamesrvController controller) throws Exception {

        if (null == controller) {
            throw new IllegalArgumentException("NamesrvController is null");
        }
        //初始化,主要是初始化几个定时任务
        boolean initResult = controller.initialize();
        if (!initResult) {
            controller.shutdown();
            System.exit(-3);
        }

        Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                controller.shutdown();
                return null;
            }
        }));

        controller.start();

        return controller;
    }

初始化,启动

//创建NettyServer网络处理对象
this.remotingServer = 
     new NettyRemotingServer(
        this.nettyServerConfig, this.brokerHousekeepingService);

//Netty服务器的工作线程池
this.remotingExecutor =
       Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
//注册Processor,把remotingExecutor注入到remotingServer中
this.registerProcessor();

解释一下remotingServer ,处理nettyServer的网络请求,包括

  • broker要来注册信息,
  • producer和consumer要来拿服务列表
//开启定时任务:每隔10s扫描一次Broker,移除不活跃的Broker
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override
     public void run() {
        NamesrvController.this.routeInfoManager.scanNotActiveBroker();
       }
}, 5, 10, TimeUnit.SECONDS);

这样再来看这个图
在这里插入图片描述
就懂了

broker启动过程

public static void main(String[] args) {
        start(createBrokerController(args));
}

public static BrokerController start(BrokerController controller) {
        try {
            //K1 Controller启动
            controller.start();

            //省略

            return controller;
        } catch (Throwable e) {
            e.printStackTrace();
            System.exit(-1);
        }

        return null;
    }

可以看到也是创建BrokerController ,并启动。

先来看创建
public static BrokerController createBrokerController(String[] args) {
        //省略

 try {
     //省略
     //K1 Broker的核心配置信息

     final BrokerConfig brokerConfig = new BrokerConfig();
     final NettyServerConfig nettyServerConfig = new NettyServerConfig();
     final NettyClientConfig nettyClientConfig = new NettyClientConfig();
     
     //Netty服务端的监听端口10911
     nettyServerConfig.setListenPort(10911);
     //K2 这个明显是Broker用来存储消息的一些配置信息。
     final MessageStoreConfig messageStoreConfig = new MessageStoreConfig();
     //省略
     
     //判断集群角色。通过BrokerId判断主从
     switch (messageStoreConfig.getBrokerRole()) {
         case ASYNC_MASTER:
         case SYNC_MASTER:
             brokerConfig.setBrokerId(MixAll.MASTER_ID);
             break;
         case SLAVE:
             if (brokerConfig.getBrokerId() <= 0) {
                 System.out.printf("Slave's brokerId must be > 0");
                 System.exit(-3);
             }

             break;
         default:
             break;
     }
     //K2 这里可以看到,判断是否基于Dledger技术来管理主从同步和CommitLog的条件就是brokerId设置为-1
     if (messageStoreConfig.isEnableDLegerCommitLog()) {
         brokerConfig.setBrokerId(-1);
     }
     //省略
    //创建Controllerg
    final BrokerController controller = new BrokerController(
                brokerConfig,
                nettyServerConfig,
                nettyClientConfig,
                messageStoreConfig);
     // remember all configs to prevent discard
     controller.getConfiguration().registerConfig(properties);
     //初始化 注意从中理清楚Broker的组件结构
     boolean initResult = controller.initialize();
     //省略
     return controller;
 } catch (Throwable e) {
     e.printStackTrace();
     System.exit(-1);
 }

 return null;
}

可以看见这里和nameServer的区别就是:
nameServer只是服务端,broker既要做服务端也要做客户端。
broker在事物消息的场景下就要做客户端。
下边的几行是解释了集群模式下的brokerid规则。

controller.initialize();

消息存储

//消息存储管理组件,管理磁盘上的消息的。
this.messageStore = new DefaultMessageStore(this.messageStoreConfig, 	
     this.brokerStatsManager, 
     this.messageArrivingListener,
     this.brokerConfig);

对consumer的filter过滤器进行持久化的任务

//这里可以看到,消费者的filter是被下推到了Broker来执行的。
 this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
     @Override
     public void run() {
         try {
             BrokerController.this.consumerFilterManager.persist();
         } catch (Throwable e) {
             log.error("schedule persist consumer filter error.", e);
         }
     }
 }, 1000 * 10, 1000 * 10, TimeUnit.MILLISECONDS);

brokerOuterAPI定时获取nameServer地址

//设置NameServer的地址列表。可以配置加载,也可以发远程请求加载。
if (this.brokerConfig.getNamesrvAddr() != null) {
    this.brokerOuterAPI.updateNameServerAddressList(this.brokerConfig.getNamesrvAddr());
    log.info("Set user specified name server address: {}", this.brokerConfig.getNamesrvAddr());
} else if (this.brokerConfig.isFetchNamesrvAddrByAddressServer()) {
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

        @Override
        public void run() {
            try {
                BrokerController.this.brokerOuterAPI.fetchNameServerAddr();
            } catch (Throwable e) {
                log.error("ScheduledTask fetchNameServerAddr exception", e);
            }
        }
    }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
}

接下来看start方法

this.messageStore.start();

Broker中启动了两个Netty服务,这样就可以接收请求了。

 this.remotingServer.start();

Broker核心的心跳注册任务。注册broker到nameserver去

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

    @Override
    public void run() {
        try {
            BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
        } catch (Throwable e) {
            log.error("registerBrokerAll Exception", e);
        }
    }
    //这个任务间隔时间默认是30秒
}, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);
        

在这里插入图片描述

接下来看broker服务信息注册

BrokerController.this.registerBrokerAll方法会发起向NameServer注册心跳。启动时会立即注册,同时也会启动一个线程池,以10秒延迟,默认30秒的间隔 持续向NameServer发送心跳。
在这里插入图片描述

public synchronized void registerBrokerAll(final boolean checkOrderConfig, boolean oneway, boolean forceRegister) {
       
     //这里才是比较关键的地方。先判断是否需要注册,然后调用doRegisterBrokerAll方法真正去注册。
     if (forceRegister || needRegister(this.brokerConfig.getBrokerClusterName(),
         this.getBrokerAddr(),
         this.brokerConfig.getBrokerName(),
         this.brokerConfig.getBrokerId(),
         this.brokerConfig.getRegisterBrokerTimeoutMills())) {
         doRegisterBrokerAll(checkOrderConfig, oneway, topicConfigWrapper);
     }
 }

broker要不要往nameServer注册,是由nameServer告诉,然后看注册doRegisterBrokerAll

private void doRegisterBrokerAll(boolean checkOrderConfig, boolean oneway,
     TopicConfigSerializeWrapper topicConfigWrapper) {
     //为什么返回的是个List?这就是因为Broker是向所有的NameServer进行注册。
     List<RegisterBrokerResult> registerBrokerResultList = this.brokerOuterAPI.registerBrokerAll(
         this.brokerConfig.getBrokerClusterName(),
         this.getBrokerAddr(),
         this.brokerConfig.getBrokerName(),
         this.brokerConfig.getBrokerId(),
         this.getHAServerAddr(),
         topicConfigWrapper,
         this.filterServerManager.buildNewFilterServerList(),
         oneway,
         this.brokerConfig.getRegisterBrokerTimeoutMills(),
         this.brokerConfig.isCompressedRegister());
     //如果注册结果的数量大于0,那么就对结果进行处理
     if (registerBrokerResultList.size() > 0) {
         RegisterBrokerResult registerBrokerResult = registerBrokerResultList.get(0);
         if (registerBrokerResult != null) {
             //主节点地址
             if (this.updateMasterHAServerAddrPeriodically && registerBrokerResult.getHaServerAddr() != null) {
                 this.messageStore.updateHaMasterAddress(registerBrokerResult.getHaServerAddr());
             }
             //从节点地址
             this.slaveSynchronize.setMasterAddr(registerBrokerResult.getMasterAddr());

             if (checkOrderConfig) {
                 this.getTopicConfigManager().updateOrderTopicConfig(registerBrokerResult.getKvTable());
             }
         }
     }
 }

Broker发起网络请求,向所有的NameServer进行注册。结果是list。list大于0,只取到1个去做判断,可以看到nameServer是无状态的。每个人结果是一样的。

怎么注册?

以netty的方式构建1个请求,然后发送。

在这里插入图片描述

producer

  • 普通发送者:DefaultMQProducer。这个只需要构建一个Netty客户端,往Broker发送消息就行了。注意,异步回调只是在Producer接收到Broker的响应后自行调整流程,不需要提供Netty服务。
  • 另一种是事务消息发送者:
    TransactionMQProducer。这个需要构建一个Netty客户端,往Broker发送消息。同时也要构建Netty服务端,供Broker回查本地事务状态。
    这里只关注DefaultMQProducer的整个过程

首先要知道生产者的一些流转状态
服务流转状态

public enum ServiceState {
    /**
     * Service just created,not start
     */
    CREATE_JUST,//刚刚初始化
    /**
     * Service Running
     */
    RUNNING,//运行
    /**
     * Service shutdown
     */
    SHUTDOWN_ALREADY,//已经停止
    /**
     * Service Start failure
     */
    START_FAILED;//失败
}

DefaultMQProducer:重点就是

public void start() throws MQClientException {
        
   this.defaultMQProducerImpl.start();

}
public void start(final boolean startFactory) {
    this.serviceState = ServiceState.START_FAILED;
    //检查生产者组是否符合要求
    this.checkConfig();
    //修改当前的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);
    }

    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;

看下实例启动里的

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();
                }
                // 具体的业务
                this.mQClientAPIImpl.start();
            
                this.startScheduledTask();
                
                this.pullMessageService.start();
                
                //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;
        }
    }
}

重点看下producer的负载均衡,producer是在发送的时候做负载均衡的选择。

Producer选择MessageQueue的方法就是自增,然后取模。并且只有这一种方法。

我们进入send方法,看到其中有一行
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);

public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {

//这里可以看到,Producer选择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);
    //Broker轮询。尽量将请求平均分配给不同的Broker
    if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
        if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
            return mq;
    }
}
}

在这里插入图片描述
容错机制:producer如果发现往一个broker发消息失败了,以后尽量不往这个broker发

然后 在发送Netty请求时,实际上是指定的MessageQueue,而不是Topic。Topic只是用来找MessageQueue。

然后根据MessageQueue再找所在的Broker,往Broker发送请求

消息存储

1、功能回顾

我们接着上面的流程,Producer把消息发到了Broker,接下来就关注下Broker接收到消息后是如何把消息进行存储的。最终存储的文件有哪些?

  • commitLog:消息存储目录,不管是哪个Topic
  • config:运行期间一些配置信息,以JSON格式
  • consumerqueue:消息消费队列存储目录,快速的找到在commitLog的位置。
  • index:消息索引文件存储目录
  • abort:broker正常启动文件大小为0
  • checkpoint:文件检查点,存储CommitLog文件最后一次刷盘时间戳、consumerquueue最后一次刷盘时间,index索引文件最后一次刷盘时间戳。

还记得我们之前看到的Broker的核心组件吗?其中messageStore就是负责消息存储的核心组件。

2、源码重点:

消息存储的入口在:DefaultMessageStore.putMessage

PutMessageResult result = this.commitLog.putMessage(msg);

然后利用零拷贝

MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();

零拷贝
我们的文件在操作系统中分为内核态和用户态,文件存盘都是存在了内核态。
如果需要对一个文件进行读,需要把它从内核态读到用户态,再从用户态读到我们的APP内存。
写的话,需要先写入到用户态,再写入到内核态。
这个过程中需要对文件进行多次拷贝。
这就是IO中File类的模式。

MappedFile读的就不是文件内容了,读的是映射,比如磁盘地址,长度这样一些属性,读到APP,操作的话操作的是映射,不是内容。
写的时候是直接在用户态就完成了,减少了拷贝次数

  关于零拷贝还有另一种技术DMA,直接在内核态操作

RocketMQ做了优化,文件固定大小1G

顺序写

result = mappedFile.appendMessage(msg, this.appendMessageCallback);

文件写满了,就创建一个新文件,重写消息

mappedFile = this.mappedFileQueue.getLastMappedFile(0);

1-commitLog写入

CommitLog的doAppend方法就是Broker写入消息的实际入口。这个方法最终会把消息追加到MappedFile映射的一块内存里,并没有直接写入磁盘。写入消息的过程是串行的,一次只会允许一个线程写入。

写道映射,怎么存到硬盘?

   //文件刷盘
   handleDiskFlush(result, putMessageResult, msg);
   //主从同步
   handleHA(result, putMessageResult, msg);

2-文件刷盘
分为同步刷盘和异步刷盘,flushDiskType为broker.conf配置的属性
同步刷盘:消息写入到内存后,立刻就构建1个请求去刷盘
在这里插入图片描述

异步刷盘是把消息映射到MappedFile后,单独唤醒一个服务来进行刷盘
每隔500毫秒负责将文件映射写入到硬盘,所以会存在丢消息的可能

是否开启了对外内存。TransientStorePoolEnable。如果开启了堆外内存,会在启动时申请一个跟CommitLog文件大小一致的堆外内存,这部分内存就可以确保不会被交换到虚拟内存中

3-分发ConsumeQueue和IndexFile

BrokerController的start方法中

this.reputMessageService.start();

当CommitLog写入一条消息后,在DefaultMessageStore的start方法中,
会启动一个后台线程reputMessageService,每隔1毫秒就会去拉取CommitLog中最新更新的一批消息,然后分别转发到ComsumeQueue和IndexFile里去,这就是他底层的实现逻辑。

​ 并且,如果服务异常宕机,会造成CommitLog和ConsumeQueue、IndexFile文件不一致,有消息写入CommitLog后,没有分发到索引文件,这样消息就丢失了。DefaultMappedStore的load方法提供了恢复索引文件的方法,入口在load方法。

4、过期文件删除

​ 入口: DefaultMessageStore.addScheduleTask -> DefaultMessageStore.this.cleanFilesPeriodically()

默认情况下, Broker会启动后台线程,每60秒,检查CommitLog、ConsumeQueue文件。
然后对超过72小时的数据进行删除。也就是说,默认情况下, RocketMQ只会保存3天内的数据。
这个时间可以通过fileReservedTime来配置。
注意他删除时,并不会检查消息是否被消费了。

3、汇总

在这里插入图片描述

文件存储部分的总结

RocketMQ的存储文件包括消息文件(Commitlog)、消息消费队列文件(ConsumerQueue)、Hash索引文件(IndexFile)、监测点文件(checkPoint)、abort(关闭异常文件)。

单个消息存储文件、消息消费队列文件、Hash索引文件长度固定以便使用内存映射机制进行文件的读写操作。

RocketMQ组织文件以文件的起始偏移量来命令文件,这样根据偏移量能快速定位到真实的物理文件。RocketMQ基于内存映射文件机制提供了同步刷盘和异步刷盘两种机制,异步刷盘是指在消息存储时先追加到内存映射文件,然后启动专门的刷盘线程定时将内存中的文件数据刷写到磁盘。

CommitLog,消息存储文件,RocketMQ为了保证消息发送的高吞吐量,采用单一文件存储所有主题消息,保证消息存储是完全的顺序写,但这样给文件读取带来了不便,为此RocketMQ为了方便消息消费构建了消息消费队列文件,基于主题与队列进行组织,同时RocketMQ为消息实现了Hash索引,可以为消息设置索引键,根据所以能够快速从CommitLog文件中检索消息。

当消息达到CommitLog后,会通过ReputMessageService线程接近实时地将消息转发给消息消费队列文件与索引文件。为了安全起见,RocketMQ引入abort文件,记录Broker的停机是否是正常关闭还是异常关闭,在重启Broker时为了保证CommitLog文件,消息消费队列文件与Hash索引文件的正确性,分别采用不同策略来恢复文件。

RocketMQ不会永久存储消息文件、消息消费队列文件,而是启动文件过期机制并在磁盘空间不足或者默认凌晨4点删除过期文件,文件保存72小时并且在删除文件时并不会判断该消息文件上的消息是否被消费。

消费者

1、功能回顾

结合我们之前的示例,回顾下消费者这一块的几个重点:

  • 消费者也是有两种,推模式消费者和拉模式消费者。消费者的使用过程也跟生产者差不多,都是先start()然后再开始消费。

  • 消费者以消费者组的模式开展。消费者组之间有集群模式和广播模式两种消费模式。我们就要了解下这两种集群模式是如何做的逻辑封装。

  • 然后我们关注下消费者端的负载均衡的原理。即消费者是如何绑定消费队列的。

  • 最后我们来关注下在推模式的消费者中,MessageListenerConcurrently 和MessageListenerOrderly这两种消息监听器的处理逻辑到底有什么不同,为什么后者能保持消息顺序。

我们接下来就通过这几个问题来把RocketMQ的消费者部分源码串起来。

2、源码重点:

1、启动

​ DefaultMQPushConsumer.start方法

​ 启动过程不用太过关注,有个概念就行,然后客户端启动的核心是mQClientFactory 主要是启动了一大堆的服务。

​ 这些服务可以结合具体场景再进行深入。例如pullMessageService主要处理拉取消息服务,rebalanceService主要处理客户端的负载均衡。

2、消息拉取:

​ 拉模式: PullMessageService

//拉取消息的请求队列
 PullRequest pullRequest = this.pullRequestQueue.take();
 //处理请求
 this.pullMessage(pullRequest);
private void pullMessage(final PullRequest pullRequest) {
   final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
   if (consumer != null) {
       DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
       //推模式的消费者最终还是会使用拉消息的方式
       impl.pullMessage(pullRequest);
   } else {
       log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
   }
}

PullRequest里有messageQueue和processQueue,其中messageQueue负责拉取消息,拉取到后,将消息存入processQueue,进行处理。 存入后就可以清空messageQueue,继续拉取了。

3、 客户端负载均衡策略
广播模式不存在负载均衡。

在消费者示例的start方法中,启动RebalanceService,这个是客户端进行负载均衡策略的启动服务。他只负责根据负载均衡策略获取当前客户端分配到的MessageQueue示例。

五种负载策略,可以由Consumer的allocateMessageQueueStrategy属性来选择。

最常用的是AllocateMessageQueueAveragely平均分配和AllocateMessageQueueAveragelyByCircle平均轮询分配。

平均分配是把MessageQueue按组内的消费者个数平均分配。

而平均轮询分配就是把MessageQueue按组内的消费者一个一个轮询分配。

例如,六个队列q1,q2,q3,q4,q5,q6,分配给三个消费者c1,c2,c3

平均分配的结果就是: c1:{q1,q2},c2:{q3,q4},c3{q5,q6}

平均轮询分配的结果就是: c1:{q1,q4},c2:{q2,q5},c3:{q3,q6}

4 、并发消费与顺序消费的过程

消费的过程依然是在DefaultMQPushConsumerImpl的 consumeMessageService中。他有两个子类ConsumeMessageConcurrentlyService和ConsumeMessageOrderlyService。其中最主要的差别是ConsumeMessageOrderlyService会在消费前把队列锁起来,优先保证拉取同一个队列里的消息。
在这里插入图片描述

消费过程的入口在DefaultMQPushConsumerImpl的pullMessage中定义的PullCallback中。

延迟消息

1、功能回顾

我们这里,就用一个典型的延迟消息的流程,来把上面看到的各个组件,结合一下。

延迟消息的核心使用方法就是在Message中设定一个MessageDelayLevel参数,对应18个延迟级别。然后Broker中会创建一个默认的Schedule_Topic主题,这个主题下有18个队列,对应18个延迟级别。消息发过来之后,会先把消息存入Schedule_Topic主题中对应的队列。然后等延迟时间到了,再转发到目标队列,推送给消费者进行消费。

整个延迟消息的实现方式是这样的:

在这里插入图片描述

2、源码重点

延迟消息的处理入口在scheduleMessageService这个组件中。 他会在broker启动时也一起加载。

if (msg.getDelayTimeLevel() > 0) {
       if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
           msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
       }

       topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC;
       queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());

       // Backup real topic, queueId
       MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
       MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
       msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));

       msg.setTopic(topic);
       msg.setQueueId(queueId);
   }

偷偷的把topic和队列给改了,在putMessage的时候。

1、消息写入:

代码见CommitLog.putMessage方法。

在CommitLog写入消息时,会判断消息的延迟级别,然后修改Message的Topic和Queue,达到转储Message的目的。

2、消息转储到目标Topic

这个转储的核心服务是scheduleMessageService,他也是Broker启动过程中的一个功能组件、

然后ScheduleMessageService会每隔1秒钟执行一个executeOnTimeup任务,将消息从延迟队列中写入正常Topic中。 代码见ScheduleMessageService中的DeliverDelayedMessageTimerTask.executeOnTimeup方法。

这个其中有个需要注意的点就是在ScheduleMessageService的start方法中。有一个很关键的CAS操作:

if (started.compareAndSet(false, true)) {

这个CAS操作保证了同一时间只会有一个DeliverDelayedMessageTimerTask执行。保证了消息安全的同时也限制了消息进行回传的效率。所以,这也是很多互联网公司在使用RocketMQ时,对源码进行定制的一个重点。

消费者部分小结:

RocketMQ消息消费方式分别为集群模式、广播模式。

消息队列负载由RebalanceService线程默认每隔20s进行一次消息队列负载,根据当前消费者组内消费者个数与主题队列数量按照某一种负载算法进行队列分配,分配原则为同一个消费者可以分配多个消息消费队列,同一个消息消费队列同一个时间只会分配给一个消费者。

消息拉取由PullMessageService线程根据RebalanceService线程创建的拉取任务进行拉取,默认每次拉取32条消息,提交给消费者消费线程后继续下一次消息拉取。如果消息消费过慢产生消息堆积会触发消息消费拉取流控。

并发消息消费指消费线程池中的线程可以并发对同一个消息队列的消息进行消费,消费成功后,取出消息队列中最小的消息偏移量作为消息消费进度偏移量存储在于消息消费进度存储文件中,集群模式消息消费进度存储在Broker(消息服务器),广播模式消息消费进度存储在消费者端。

RocketMQ不支持任意精度的定时调度消息,只支持自定义的消息延迟级别,例如1s、2s、5s等,可通过在broker配置文件中设置messageDelayLevel。

顺序消息一般使用集群模式,是指对消息消费者内的线程池中的线程对消息消费队列只能串行消费。与并发消息消费最本质的区别是消息消费时必须成功锁定消息消费队列,在Broker端会存储消息消费队列的锁占用情况。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值