19、RocketMQ核⼼编程模型

 、源码环境搭建

1 、主要功能模块


RocketMQ的官方Git仓库地址:https://github.com/apache/rocketmq 可以用git把项目 clone下来或者直接下载代码包。
也可以到RocketMQ的官方网站上下载指定版本的源码: http://rocketmq.apache.org/dowloading/releases/ 源码下很多的功能模块 ,其中大部分的功能模块都是可以见名知义的:
broker: Broker 模块( broke 启动进程)
client :消息客户端 , 包含消息生产者 、消息消费者相关类
example: RocketMQ 例代码
namesrv:NameServer模块
store:消息存储模块
remoting:远程访问模块


2 、源码启动服务

将源码导入IDEA后 ,需要先对源码进行编译 。
编译指令 clean install -Dmaven.test.skip=true

编译完成后就可以开始调试代码了 。调试时需要按照以下步骤:

2.1 启动nameServer

展开namesrv模块  运⾏NamesrvStartup类即可启动NameServer服务。

 对这个NamesrvStartup类做—个简单的解读都知道 ,可以通过-c参数指定—个properties配置⽂件 ,并通过-p参数打印出nameserver所有⽣效的参数配置。

orderMessageEnable参数默认是false ,通过指定配置⽂件 ,修改成了true 服务端部署时 ,也可以这样调整nameserver的默认参数配置。 

配置完成后 ,去掉-p参数 ,再次执⾏ ,就可以启动nameserver服务了 。启动成功 ,可以在控制台看到这样⽇志 

load config properties file OK, /Users/roykingw/names rv/names rv.properties The Name Server boot success. serializeType=JSON, address 0.0.0.0:9876

2.2 启动Broker

类似的  Broker服务的启动⼊⼝在broker模块的BrokerStatup类。

Broker服务  同样可以通过-c参数指定broker.conf⽂件 ,并通过-p或者-m参数打印出⽣效的配置信息。

broker.conf配置⽂件在distribution模块中。

然后重新启动  即可启动Broker

2.3 调⽤客户端

服务启动好了之后 ,就可以使⽤客户端收发消息了。 客户端代码在example模块中 ,具体使⽤⽅式略过。

3 、读源码的⽅法

整个源码环境调试好后 ,接下来就可以开始详细调试源码了 。但是对于RocketMQ的源码 ,不建议打断点调试  因为线程和定时任务太多 ,打断点很难调试到。 RocketMQ的源码有个特点 ,就是 ⼏乎没有注释 。所以开始读源码之前 ,我会给你分享—些读源码的⽅式  以便后续你能更好的跟上我的思路。

1 、带着问题读源码 。如果没有⾃⼰的思考 ,源码不如不读!!!

2 、⼩步快⾛ 。不要觉得—两遍就能读懂源码。这⾥我会分为三个阶段来带你逐步加深对源码的理解。

3 、分步总结 。带上⾃⼰的理解 ,及时总结。对各种扩展功能  尝试验证。对于RocketMQ ,试着去理解源码中的各种单元测试。

 、源码热身阶段

梳理—些重要的服务端核⼼配置  同时梳理—下NameServerBroker有哪些核⼼组件 ,找到—点点读源码的感觉。

1 NameServer的启动过程

1.1关注的问题

RocketMQ集群中  实际记性消息存储 、推送等核⼼功能点额是Broker 。⽽NameServer的作⽤ ,其实和微服务中的注册中⼼⾮常类似 ,他只是提供了Broker 端的服务注册与发现功能。

第—次看源码 ,不要太过陷⼊具体的细节 ,先搞清楚NameServer的⼤体结构。

1.2源码重点

NameServer的启动⼊⼝类是org.apache.rocketmq.namesrv.NamesrvStartup 。其中的核⼼是构建并启动—个NamesrvController。这个Cotroller对象就跟 MVC中的Controller是很类似的 ,都是响应客户端的请求 。只不过 ,他响应的是基于Netty的客户端请求。

另外还启动了—个ControllerManager服务  这个服务主要是⽤来保证服务⾼可⽤的  这⾥暂不解读。

另外 ,他的实际启动过程 ,其实可以配合NameServer的启动脚本进⾏更深⼊的理解 。我们这最先关注的是他的整体结构:

解读出以下⼏个重点:

  1. 1、这⼏个配置类就可以⽤来指导如何优化Nameserver的配 。⽐如 ,如何调整nameserver的端口? ⾃⼰试试从源码中找找答案。
  2. 2、在之前的4.x版本当中  Nameserver中是没有ControllerManagerNettyRemotingClient  这意味着现在NameServer现在也需要往外发Netty请求了。
  3. 3 、稍微解读下Nameserver中核⼼组件例如RouteInfoManager的结构 ,可以发现RocketMQ的整体源码⻛格其实就是典型的MVC思想 Controller响应⽹络请  ,各种Manager和其中包含的Service处理业务  内存中的各种Table保存消息。

2 Broker服务启动过程

2.1关注重点

Broker是整个RocketMQ的业务核⼼ 。所有消息存储 、转发这些重要的业务都是Broker进⾏处理。 这⾥重点梳理Broker有哪些内部服务。这些内部服务将是整理Broker核⼼业务流程的起点。

2.2 源码重点

Broker启动的⼊⼝在BrokerStartup这个类 ,可以从他的main⽅法开始调试。

启动过程关键点:重点也是围绕—个BrokerController对象 ,先创建 ,然后再启动。

⾸先: 在BrokerStartup.createBrokerController⽅法中可以看到Broker的⼏个核⼼配置:

  • .  BrokerConfig  Broker服务配置
  • .  MessageStoreConfig : 消息存储配置 。 这两个配置参数都可以在broker.conf⽂件中进⾏配置
  • .  NettyServerConfig Netty服务端占⽤了10911端⼝ 。同样也可以在配置⽂件中覆盖。
  • .  NettyClientConfig  Broker既要作为Netty服务端  向客户端提供核⼼业务能⼒  ⼜要作为Netty客户端  NameServer注册⼼跳。
  • .  AuthConfig:权限相关的配置。

这些配置是我们了解如何优化 RocketMQ 使⽤的关键。

然后: 在BrokerController.start⽅法可以看到启动了—⼤堆Broker的核⼼服务 ,我们挑—些重要的


this.messageStore.start();//启动核⼼的消息存储组件

this.timerMessageStore.start(); //时间轮服务 ,主要是处理指定时间点的延迟消息。

this.remotingServer.start(); //Netty服务端

this.fastRemotingServer.start(); //启动另—个Netty服务端。

this.broke rOuterAPI.start();//启动客户端 ,往外发请求

this.topicRouteInfoManager.start(); //管理Topic路由信息

BrokerController.this.registerBrokerAll:  //向所有依次NameServer注册⼼跳。

this.brokerStatsManager.start();//服务状态

我们现在不需要了解这些核⼼组件的具体功能  只要有个⼤概  Broker中有—⼤堆的功能组件负责具体的业务 。后⾯等到分析具体业务时再去深⼊每个服务的 细节。

我们需要抽象出Broker的—个整体结构:

 可以看到Broker启动了两个Netty服务 ,他们的功能基本差不多 。实际上 ,在应⽤中 ,可以通过producer.setSendMessageWithVIPChannel(true) ,让少量⽐ 较重要的producerVIP的通道 。⽽在消费者端 ,也可以通过consumer.setVipChannelEnabled(true) ,让消费者⽀持VIP通道的数据。

三、⼩试⽜⼑阶段

开始理解—些⽐较简单的业务逻辑

3 Netty服务注册框架

3.1关注重点?

RocketMQ实际上是—个复杂的分布式系统  NameServer  Broker Client之间需要有⼤量跨进程的RPC调⽤。这些复杂的RPC请求是怎么管理 ,怎么调⽤的 呢?这是我们去理解RocketMQ底层业务的基础。这—部分的重点就是去梳理RocketMQ的这—整套基于Netty的远程调⽤框架。

需要说明的是  RocketMQ整个服务调⽤框架绝⼤部分是使⽤Netty框架封装的。 所以 ,要看懂这部分代码 ,需要你对Netty架有⾜够的了解。

3.2源码重点

Netty的所有远程通信功能都由remoting模块实现 remoting模块中有两个对象最为重要 。 就是RPC的服务端RemotingServer以及客户端RemotingClient。在 RocketMQ ,涉及到的远程服务⾮常多  同—个服务 ,可能既是RPC的服务端也可以是RPC的客户端 。例如Broker服务 ,对于Client来说 ,他需要作为服务端  响应他们发送消息以及拉取消息等请求 ,所以Broker是需要RemotingServer的。 ⽽另—⽅⾯  Broker需要主动向NameServer发送⼼跳请求  这时  Broker   需要RemotingClient 。因此  Broker既是RPC的服务端⼜是RPC的客户端。

对于这部分的源码 ,就可以从remoting模块中RemotingServerRemotingClient的初始化过程⼊⼿。有以下⼏个重点是需要梳理清楚的:

1 RemotingServerRemotingClient之间是通过什么协议通讯的?

RocketMQ  RemotingServer是—个接⼝ ,在这个接⼝下 ,提供了两个具体的实现类  NettyRemotingServerMultiProtocolRemotingServer 。他们都是基 Netty框架封装的  只不过处理数据的协议不—样 。也就是说  RocketMQ可以基于不同协议实现RPC访问 。其实这也就为RocketMQ提供多种不同语⾔的客   户端打下了基础。

2 、哪些组件需要Netty服务端?哪些组件需要Netty客户端?

之间简单梳理过  NameServerBroker的服务内部都是既有RemotingServerRemotingClient的。 那么作为客户端的ProducerConsumer ,是不是就只需 RemotingClient呢?其实也不是 ,事务消息的Producer也需要响应Broker的事务状态回查 ,他也是需要NettyServer的。

这⾥需要注意的是  Netty框架是基于Channel⻓连接发起的RPC通信 。只要⻓连接建⽴了 ,那么数据发送是双向的。 也就是说 Channel⻓连接建⽴完成后, NettyServer服务端也可以向NettyClient客户端发送请求 ,所以服务端和客户端都需要对业务进⾏处理。

3 Netty框架最核⼼的部分是如何构架处理链  RocketMQ是如何构建的呢?

服务端构建处理链的核⼼代码:

// org.apache.rocketmq.remoting.netty.NettyRemotingServer
protected ChannelPipeline configChannel(SocketChannel ch) {
    return ch.pipeline()
    .addLast(defaultEventExecutorGroup, HANDSHAKE_HANDLER_NAME, new HandshakeHandler())
    .addLast(defaultEventExecutorGroup, encoder, //请求编码器
    new NettyDecoder(), //请求解码器   distributionHandler, //请求计数器 new                 IdleStateHandler(0, 0,
    nettyServerConfig.getServerChannelMaxIdleTimeSeconds()), //⼼跳管理器
    connectionManageHandler, //连接管理器
    serverHandler //核⼼的业务处理器
    );
}

我们这⾥主要分析业务请求如何管理 。分两个部分来看:

1 、请求参数:

从请求的编解码器可以看出  RocketMQ的所有RPC请求数据都封装成RemotingCommand对象 RemotingCommand对象中有⼏个重要的属性:

private int code; //响应码 ,表示请求处理成功还是失败
private int opaque = requestId.getAndIncrement(); //服务端内部会构建唯—的请求ID。
private transient CommandCustomHeader customHeader; //⾃定义的请求头 。⽤来区分不同的业务请求 private transient byte [] body; //请求参数体
private int flag = 0; //参数类型 ,   默认0表示请求 , 1表示响应

2 、处理逻辑

所有核⼼的业务请求都是通过—个NettyServerHandler进⾏统—处理 。他处理时的核⼼代码如下:

@ChannelHandler.Sharable
public class NettyServerHandler extends SimpleChannelInboundHandler<RemotingCommand> { //统—处理所有业务请求
@Override
protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) {
int localPort = RemotingHelper.parseSocketAddressPort(ctx.channel().localAddress());
NettyRemotingAbstract remotingAbstract = NettyRemotingServer.this.remotingServerTable.get(localPort);
if (localPort != -1 && remotingAbstract != null) {
remotingAbstract.processMessageReceived(ctx, msg); //核⼼处理请求的⽅法
return;
}
// The related remoting server has been shutdown, so close the connected channel
RemotingHelper.closeChannel(ctx.channel());
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
//调整channel的读写属性
}
}
*2.1、在最核⼼的处理请求的processMessageReceived⽅法中 ,会将请求类型分为 REQUEST__COMMAND 和 RESPONSE_COMMAND来处理 。**为什么会 有两种不同类型的请求呢?

这是因为客户端的业务请求会有两种类型:—种是客户端发过来的业务请求 ,另—种是客户上次发过来的业务请求 ,可能并没有同步给出相应。这时就需要客 户端再发—个response类型的请求 ,获取上—次请求的响应。这也就能⽀持异步的RPC调⽤ 

2.2 、如何处理request类型的请求?

服务端和客户端都会维护—个processorTable。这是个HashMap,key是服务码 ,也就对应RemotingCommandcode value是对应的运⾏单元

Pair<NettyRequestProcessor, ExecutorService> 。包含了执⾏线程的线程池和具体处理业务的Processor 。 ⽽这些Processor ,是由业务系统⾃⾏注册的。 也就是说 ,想要看每个服务具体有哪些业务能⼒ ,就只要看他们注册了哪些Processor就知道了。

Broker服务注册 ,详⻅ BrokerController.registerProcssor()⽅法。

NameServer的服务注册⽅法 ,重点如下:

private void registerProcessor() {
if (names rvConfig.isClusterTest()) { //是否测试集群模式 ,默认是false。也就是说现在阶段不推荐。
this.remotingServer.registerDefaultProcessor(new ClusterTestRequestProcessor(this,
names rvConfig.getProductEnvName()), this.defaultExecutor);
} else {
// Support get route info only temporarily
ClientRequestProcessor clientRequestProcessor = new ClientRequestProcessor(this);
this.remotingServer.registerProcessor(RequestCode.GET_ROUTEINFO_BY_TOPIC,
clientRequestProcessor, this.clientRequestExecutor);
this.remotingServer.registerDefaultProcessor(new DefaultRequestProcessor(this), this.defaultExecutor);
}
}

另外  NettyClient也会注册—个⼤的ClientRemotingProcessor ,统—处理所有请求 。注册⽅法⻅ org.apache.rocketmq.client.impl.MQClientAPIImpl类的构 造⽅法 。也就是说  只要⻓连接建⽴完成了  NettyClient⽐如Producer ,也可以处理NettyServer发过来的请求。

2.3 、如何处理response类型的请求?

NettyServer处理完request请求后 ,会先缓存到responseTable ,等NettyClient下次发送response类型的请求 ,再来获取。这样就不⽤阻塞Channel ,提升 请求的吞吐量 。优雅的⽀持了异步请求。

** 2.4 、关于RocketMQ的同步结果推送与异步结果推送**

RocketMQRemotingServer服务端 ,会维护—个responseTable  这是—个线程同步的Map结构 key为请求的ID value是异步的消息结果。 ConcurrentMap<Integer /* opaque */, ResponseFuture>

处理同步请求(NettyRemotingAbstract#invokeSyncImpl) ,处理的结果会存⼊responseTable ,通过ResponseFuture提供—定的服务端异步处理⽀持 ,提升 服务端的吞吐量 。 请求返回后 ,⽴即从responseTable中移除请求记录。

实际上  同步也是通过异步实现的。

//org.apache.rocketmq.remoting.netty.ResponseFuture
//发送消息后 ,通过countDownLatch阻塞当前线程 ,造成同步等待的效果。
public RemotingCommand waitResponse(final long timeoutMillis) throws InterruptedException {
this.countDownLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
return this.responseCommand;
}
//等待异步获取到消息后 ,再通过countDownLatch释放当前线程。
public void putResponse(final RemotingCommand responseCommand) {
this.responseCommand = responseCommand;
this.countDownLatch.countDown();
}

处理异步请求(NettyRemotingAbstract#invokeAsyncImpl) ,处理的结果依然会存⼊responsTable ,等待客户端后续再来请求结果 。但是他保存的依然是— ResponseFuture ,也就是在客户端请求结果时再去获取真正的结果。

另外 ,在RemotingServer启动时 ,会启动—个定时的线程任务 ,不断扫描responseTable ,将其中过期的response清除掉。

//org.apache.rocketmq.remoting.netty.NettyRemotingServer
TimerTask timerScanResponseTable = new TimerTask() {
@Override
public void run(Timeout timeout) {
try {
NettyRemotingServer.this.scanResponseTable();
} catch (Throwable e) {
log.error("scanResponseTable exception", e);
} finally {
timer.newTimeout(this, 1000, TimeUnit.MILLISECONDS);
}
}
};
this.timer.newTimeout(timerScanResponseTable, 1000 * 3, TimeUnit.MILLISECONDS);

整体RPC框架流程如下图:

可以看到  RocketMQ基于Netty框架实现的这—套基于服务码的服务注册机制  即可以让各种不同的组件都按照⾃⼰的需求注册⾃⼰的服务⽅法  ⼜可以以—  种统—的⽅式同时⽀持同步请求和异步请求 。所以这—套框架 ,其实是⾮常简洁易⽤的。在使Netty框架进⾏相关应⽤开发时 ,都可以借鉴他的这—套服务注 册机制。 例如开发—个⼤型的IM项⽬ ,要添加好友、发送⽂本、发送图⽚、发送附件 、甚⾄还有表情 、红包等等各种各样的请求。这些请求如何封装 ,就可以  参考这—套服务注册框架。 

4 Broker⼼跳注册管理

4.1关注重点

RocketMQ的服务调⽤框架整理清楚之后 ,接下来就可以从—些具体的业务线来进⾏详细梳理了。

之前介绍过  Broker会在启动时向所有NameServer注册⾃⼰的服务信息 ,并且会定时往NameServer发送⼼跳信息 NameServer会维护Broker的路由列  ,并对路由表进⾏实时更新。这—轮就重点梳理这个过程。

4.2源码重点

Broker启动后会⽴即发起向NameServer注册⼼跳 。⽅法⼊⼝ :BrokerController.this.registerBrokerAll 。 然后启动—个定时任务  10秒延迟 ,默认30秒的 间隔持续向NameServer发送⼼跳。

//K4 Broker向NameServer进⾏⼼跳注册
if ( !isIsolated && !this.messageStoreConfig.isEnableDLegerCommitLog() && !this.messageStoreConfig.isDuplicationEnable()) {
changeSpecialServiceStatus(this.brokerConfig.getBroke rId() == MixAll.MASTER_ID);
this.registerBrokerAll(true, false, true);
}
//启动后定时注册
scheduledFutures.add(this.scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(this.getBrokerIdentity())
{
@Override
public void run0() {
try {
if (System.currentTimeMillis() < shouldStartTime) {
BrokerController.LOG.info("Register to names rv after {}", shouldStartTime);
return;
}
if (isIsolated) {
BrokerController.LOG.info("Skip register for broker is isolated");
return;
}
BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
} catch (Throwable e) {
BrokerController.LOG.error("registerBrokerAll Exception", e);
}
}
}, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS));

NameServer内部会通过RouteInfoManager组件及时维护Broker信息 。具体参⻅org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager registerBroker⽅法

同时在NameServer启动时 ,会启动定时任务 ,扫描不活动的Broker 。⽅法⼊⼝ :NamesrvController.initialize⽅法 ,往下跟踪到startScheduleService 法。

 

4.3极简化的服务注册发现流程

为什么RocketMQ要⾃⼰实现—个NameServer ,⽽不⽤Zookeeper Nacos这样现成的注册中⼼?

⾸先 ,依赖外部组件会对产品的独⽴性形成侵⼊ ,不利于⾃⼰的版本演进 Kafka要抛弃Zookeeper就是—个先例。

另外 ,其实更重要的还是对业务的合理设计 NameServer之间不进⾏信息同步 ,⽽是依赖Broker端向所有NameServer同时发起注册。这让NameServer的服 务可以⾮常轻量 。如果可能 ,你可以与NacosZookeeper的核⼼流程做下对⽐ NameServer集群只要有—个节点存活 。整个集群就能正常提供服务 ,⽽

Zookeeper  Nacos等都是基于多数派同意的机制 ,需要集群中超过半数的节点存活才能正常提供服务。

但是 ,要知道  这种极简的设计 ,其实是以牺牲数据—致性为代价的。 Broker往多个NameServer同时发起注册 ,有可能部分NameServer注册成功 ,⽽部分   NameServer注册失败了。这样  多个NameServer之间的数据是不—致的。 作为通⽤的注册中⼼  这是不可接受的。 但是对于RocketMQ  这⼜变得可以接受  。因为客户端从NameServer上获得Broker列表后  只要有—个正常运⾏的Broker就可以了 ,并不需要完整的Broker列表。

5 Producer发送消息过程

5.1关注重点

⾸先: 回顾下我们之前的Producer使⽤案例。

Producer有两种:

  1. .  —种是普通发送者: DefaultMQProducer 。只负责发送消息 ,发送完消息 ,就可以停⽌了。
  2. .  另—种是事务消息发送者: TransactionMQProducer 。⽀持事务消息机制。 需要在事务消息过程中提供事务状态确认的服务  这就要求事务消息发送者虽 然是—个客户端 ,但是也要完成整个事务消息的确认机制后才能退出。

事务消息机制后⾯将结合Broker进⾏整理分析。这—步暂不关注 。我们只关注DefaultMQProducer的消息发送过程。

然后:整个Producer的使⽤流程 ,⼤致分为两个步骤:—是调⽤start⽅法  进⾏—⼤堆的准备⼯作 。 ⼆是各种send⽅法  进⾏消息发送。

那我们重点关注以下⼏个问题:

1 Producer启动过程中启动了哪些服务 。也就是了RocketMQClient客户端的基础结构。

2 Producer如何管理broker路由信息 。 可以设想—下 ,如果Producer启动了之后  NameServer挂了 ,那么Producer还能不能发送消息?希望你先从源码中 进⾏猜想 ,然后⾃⼰设计实验进⾏验证。

3 、关于Producer的负载均衡 。也就是Producer到底将消息发到哪个MessageQueue中。这⾥可以结合顺序消息机制来理解—下 。消息中那个莫名奇妙的 MessageSelector到底是如何⼯作的。

5.2源码重点

5.2.1Producer的核⼼启动流程

所有Producer的启动过程 ,最终都会调⽤到DefaultMQProducerImpl#start⽅法。在start⽅法中的通过—个mQClientFactory对象 ,启动⽣产者的—⼤堆重要 服务。

这个mQClientFactory是最为重要的—个对象  负责⽣产所有的Client,包括ProducerConsumer

这⾥其实就是—种设计模式 ,虽然有很多种不同的客户端 ,但是这些客户端的启动流程最终都是统—的 ,全是交由mQClientFactory对象来启动。 ⽽不同之处 在于这些客户端在启动过程中 ,按照服务端的要求注册不同的信息 。例如⽣产者注册到producerTable ,消费者注册到consumerTable ,管理控制端注册到adminExtTable

 

 5.2.2发送消息的核⼼流程 

核⼼流程如下:

 

1、发送消息时 ,会维护—个本地的topicPublishInfoTable缓存  DefaultMQProducer会尽量保证这个缓存数据是最新的。 但是 ,如果NameServer挂了 ,那么  DefaultMQProducer还是会基于这个本地缓存去找Broker 。只要能找到Broker  还是可以正常发送消息到Broker的。 --可以在⽣产者示例中 start打—个断  ,然后把NameServer停掉  这时  Producer还是可以发送消息的。

2 、⽣产者如何找MessageQueue 默认情况下 ,⽣产者是按照轮训的⽅式 ,依次轮训各个MessageQueue 。但是如果某—次往—个Broker发送请求失败后, 下—次就会跳过这个Broker

 

//org.apache.rocketmq.client.impl.producer.TopicPublishInfo
//QueueFilter是⽤来过滤掉上—次失败的Broker的 ,表示上—次向这个Broker发送消息是失败的 ,这时就尽量不要再往这个Broker发送消息了。
private MessageQueue selectOneMessageQueue(List<MessageQueue> messageQueueList, ThreadLocalIndex sendQueue, QueueFilter
...filter) {
if (messageQueueList == null || messageQueueList.isEmpty()) {
return null;
}
if (filter != null && filter.length != 0) {
for (int i = 0; i < messageQueueList.size(); i++) {
int index = Math.abs(sendQueue.incrementAndGet() % messageQueueList.size());
MessageQueue mq = messageQueueList.get(index);
boolean filterResult = true;
for (QueueFilter f: filter) {
Preconditions.checkNotNull(f);
filterResult &= f.filter(mq);
}
if (filterResult) {
return mq;
}
}
return null;
}
int index = Math.abs(sendQueue.incrementAndGet() % messageQueueList.size());
return messageQueueList.get(index);
}

3 、如果在发送消息时传了Selector ,那么Producer就不会⾛这个负载均衡的逻辑 ⽽是会使⽤Selector去寻找—个队列 。 具体参⻅ org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendSelectImpl ⽅法。

//K4 Producer顺序消息的发送⽅法
public MessageQueue invokeMessageQueueSelector(Message msg, MessageQueueSelector selector, Object arg,
final long timeout) throws MQClientException, RemotingTooMuchRequestException {
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 {
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选择出⽬标mq
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 mq;
} else {
throw new MQClientException("select message queue return null.", null);
}
}
validateNameServerSetting();
throw new MQClientException("No route info for this topic, " + msg.getTopic(), null);
}

6 Consumer拉取消息过程

6.1关注重点

结合我们之前的示例  回顾下消费者这—块的⼏个重点问题:

  •     ·消费者也是有两种 推模式消费者和拉模式消费者 。优秀的MQ产品都会有—个⾼级的⽬标 ,就是要提升整个消息处理的性能 。⽽要提升性能 ,服务端的优 化⼿段往往不够直接 ,最为直接的优化⼿段就是对消费者进⾏优化 。所以在RocketMQ ,整个消费者的业务逻辑是⾮常复杂的  甚⾄某种程度上来说  服务端更复杂 ,所以 ,在这⾥我们重点关注⽤得最多的推模式的消费者。
  •  ·消费者组之间有集群模式和⼴播模式两种消费模式 。我们就要了解下这两种集群模式是如何做的逻辑封装。
  •   · 然后我们关注下消费者端的负载均衡的原理 。即消费者是如何绑定消费队列的  哪些消费策略到底是如何落地的。
  • .  最后我们来关注下在推模式的消费者中  MessageListenerConcurrently MessageListenerOrderly这两种消息监听器的处理逻辑到底有什么不同 ,为什 么后者能保持消息顺序。

6.2源码重点

Consumer的核⼼启动过程和Producer是—样的  最终都是通过mQClientFactory对象启动。 不过之间添加了—些注册信息 。整体的启动过程如下:

 

6.3⼴播模式与集群模式的Offset处理 

DefaultMQPushConsumerImplstart⽅法中 ,启动了⾮常多的核⼼服务 。 ⽐如 对于⼴播模式与集群模式的Offset处理

if (this.defaultMQPushConsumer.getOffsetStore() != null) {
this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
} else {
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
this.offsetStore
= new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
case CLUSTERING:
this.offsetStore
= new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
default:
break;
}
this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
}
this.offsetStore.load();

 可以看到 ,⼴播模式是使⽤LocalFileOffsetStore ,在Consumer本地保存Offset ,⽽集群模式是使⽤RemoteBrokerOffsetStore ,在Broker端远程保offset ⽽这两种Offset的存储⽅式 ,最终都是通过维护本地的offsetTable缓存来管理Offset

6.4ConsumerMessageQueue建⽴绑定关系

start⽅法中还—个⽐较重要的东⻄是给rebalanceImpl设定了—个AllocateMessageQueueStrategy ,⽤来给Consumer分配MessageQueue的。

this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel()); //Consumer负载均衡策略
this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());

这个AllocateMessageQueueStrategy就是⽤来给ConsumerMessageQueue之间建⽴—种对应关系的。 也就是说  只要Topic 当中的MessageQueue以及同 —个ConsumerGroup中的Consumer实例都没有变动 ,那么某—个Consumer实例只是消费固定的—个或多个MessageQueue上的消息 ,其他Consumer不会   来抢这个Consumer对应的MessageQueue

关于具体的分配策略 ,可以看下RocketMQ中提供的AllocateMessageQueueStrategy接⼝实现类 。你能⾃⼰尝试看懂他的分配策略吗?

这⾥ ,你可以想—下为什么要让—个MessageQueue只能由同—个ConsumerGroup中的—个Consumer实例来消费。

其实原因很简单  因为Broker需要按照ConsumerGroup管理每个MessageQueue上的Offset ,如果—MessageQueue上有多个同属—个ConsumerGroup Consumer实例 ,他们的处理进度就会不—样。这样的话 Offset就乱套了。

6.5顺序消费与并发消费

同样在start⽅法中 ,启动了consumerMessageService线程  进⾏消息拉取。

 

//K6 消费者顺序消费与并发消费的区别
if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {//顺序消费监听器
this.consumeOrderly = true;
this.consumeMessageService =
new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
//POPTODO reuse Executor ? —种新的MessageQueue⼯作模式 。还在TODO中 ,就暂不关注了。
this.consumeMessagePopService = new ConsumeMessagePopOrderlyService(this,
(MessageListenerOrderly) this.getMessageListenerInner());
} else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) { //并发消费监听器
this.consumeOrderly = false;
this.consumeMessageService =
new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
//POPTODO reuse Executor ?
this.consumeMessagePopService =
new ConsumeMessagePopConcurrentlyService(this,
(MessageListenerConcurrently) this.getMessageListenerInner());
}
this.consumeMessageService.start();
// POPTODO
this.consumeMessagePopService.start();

可以看到  Consumer通过registerMessageListener⽅法指定的回调函数 ,都被封装成了ConsumerMessageService的⼦实现类。

当前版本新增了—个POP模式。这是—种新增的⼯作模式  ⽬前在TODO ,就暂不关注了。有兴趣可以⾃⼰看看。具体⼊⼝在org.apache.rocketmq.client.impl.consumer.PullMessageService run⽅法。这个服务会随着客户端—起启动。

 

@Override
public void run() {
logger.info(this.getServiceName() + " service started");
    while ( !this.isStopped()) {
    try {
        MessageRequest messageRequest = this.messageRequestQueue.take();
        if (messageRequest.getMessageRequestMode() == MessageRequestMode.POP) {
            this.popMessage((PopRequest) messageRequest);
        } else {
            this.pullMessage((PullRequest) messageRequest);
        }
    } catch (InterruptedException ignored) {
    } catch (Exception e) {
        logger.error("Pull Message Service Run Method exception", e);
    }
    }
        logger.info(this.getServiceName() + " service end");
}

⽽对于这两个服务实现类的调⽤ ,会延续到DefaultMQPushConsumerImplpullCallback对象中。 也就是Consumer每拉过来—批消息后 ,就向Broker提交下 —个拉取消息的的请求。

这⾥也可以印证—个点 ,就是顺序消息  只对异步消费也就是推模式有效 。同步消费的拉模式是⽆法进⾏顺序消费的。 因为这个pullCallback对象 ,在拉 模式的同步消费时 ,根本就没有往下传。当然  这并不是说拉模式不能锁定队列进⾏顺序消费 ,拉模式在Consumer端应⽤就可以指定从哪个队列上拿消息。


PullCallback pullCallback = new PullCallback() {
    @Override
    public void onSuccess(PullResult pullResult) {
        if (pullResult != null) {
            //...
        switch (pullResult.getPullStatus()) {
        case FOUND:
                //...
        DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
        pullResult.getMsgFoundList(),
                processQueue,
                pullRequest.getMessageQueue(),
        dispatchToConsume);
        //...
        break;
        //...
    }
    }
}

⽽这⾥提交的  实际上是—个ConsumeRequest线程 。⽽提交的这个ConsumeRequest线程 ,在两个不同的ConsumerService中有不同的实现。

这其中 ,两者最为核⼼的区别在于ConsumeMessageConcurrentlyService只是控制—批请求的⼤⼩ ,⽽并不控制从哪个MessageQueue上拉取消息。

@Override
public void submitConsumeRequest(
final List<MessageExt> msgs,
final ProcessQueue processQueue,
final MessageQueue messageQueue,
final boolean dispatchToConsume) {
final int consumeBatchSize = this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();
if (msgs.size() <= consumeBatchSize) {
ConsumeRequest consumeRequest = new ConsumeRequest(msgs, processQueue, messageQueue);
try {//往线程池中提交ConsumeRequest。注意 ,在线程池中 , 多个ConsumeRequest是并发执⾏的 。所以如果没有并发控制 ,会有多个线程同时拉取消息。 this.consumeExecutor.submit(consumeRequest);
} catch (RejectedExecutionException e) {
this.submitConsumeRequestLater(consumeRequest);
}
} else {
for (int total = 0; total < msgs.size(); ) {
List<MessageExt> msgThis = new ArrayList<>(consumeBatchSize);
for (int i = 0; i < consumeBatchSize; i++, total++) {
if (total < msgs.size()) {
msgThis.add(msgs.get(total));
} else {
break;
}
}
ConsumeRequest consumeRequest = new ConsumeRequest(msgThis, processQueue, messageQueue);
try {
this.consumeExecutor.submit(consumeRequest);
} catch (RejectedExecutionException e) {
for (; total < msgs.size(); total++) {
msgThis.add(msgs.get(total));
}
this.submitConsumeRequestLater(consumeRequest);
}
}
}
}

ConsumerMessageOrderlyService是锁定了—个队列 ,处理完了之后 ,再消费下—个队列。

@Override
public void submitConsumeRequest(
final List<MessageExt> msgs,
final ProcessQueue processQueue,
final MessageQueue messageQueue,
final boolean dispatchToConsume) {
if (dispatchToConsume) {//不做请求批量⼤⼩控制 ,直接提交请求
ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue);
this.consumeExecutor.submit(consumeRequest);
}
}
//ConsumerMessageOrderlyService中定义的consumerRequest public void run() {
// ....
final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);
synchronized (objLock) {
//....
}
}

为什么给队列加个锁 ,就能保证顺序消费呢?结合顺序消息的实现机制理解—下。从源码中可以看到 Consumer提交请求时 ,都是往线程池⾥异步提交的请求 。如果不加队列锁 ,那么就算Consumer提交针对同—个MessageQueue的拉取 消息请求  这些请求都是异步执⾏ ,他们的返回顺序是乱的 ,⽆法进⾏控制。 给队列加个锁之后 ,就保证了针对同—个队列的第⼆个请求 ,必须等第—个请求 处理完了之后 ,释放了锁 ,才可以提交。这也是在异步情况下保证顺序的基础思路.

6.6实际拉取消息还是通过PullMessageService完成的。

start⽅法中 ,相当于对很多消费者的服务进⾏初始化  包括指定—些服务的实现类  以及启动—些定时的任务线程  ⽐如清理过期的请求缓存等 。最后 ,会随 mQClientFactory组件的启动 ,启动—个PullMessageService 。实际的消息拉取都交由PullMesasgeService⾏。

所谓消息推模式 ,其实还是通过Consumer拉消息实现的。

//org.apache.rocketmq.client.impl.consumer.PullMessageService下的pullMessage⽅法
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);
}
}

 另外还—种pop⼯作模式也是在PullMessagService下的popMessage⽅法触发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值