RocketMQ源码阅读之生产者
目标:本文的目的是梳理RocketMQ生产者从创建到启动及最终发送的源码主体逻辑,掌握生产者代码的主体流程。
生产者创建及启动流程
1. 创建生产者
DefaultMQProducer producer = new DefaultMQProducer("hr_order_group");
在创建生产者的时候,内部创建了生产者的业务流程实现类DefaultMQProducerImpl ,这个类负责对接生产者的发送等行为的实现,创建的时候初始化了发送使用的线程池。
public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) {
this.namespace = namespace;
this.producerGroup = producerGroup;
defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
}
2. 启动生产者
入口:producer.start() 使用内部的实现类启动生产者,如果开启了消息轨迹,则同时开启消息轨迹追踪。
public void start() throws MQClientException {
//设置生产者所属组: 会拼接组所属的名称空间,形式 namespace%producerGroup
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);
}
}
}
2.1 defaultMQProducerImpl.start() 启动生产者
public void start(final boolean startFactory) throws MQClientException {
switch (this.serviceState) {
case CREATE_JUST:
this.serviceState = ServiceState.START_FAILED;
//校验配置:主要是对producerGroup的长度,字符等校验
this.checkConfig();
//如果不是客户端内置的生产者组则设置生产者的实例名称
if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
//设置生产者的实例名称,内部会判断是不是默认的实例名称,如果是,则将实例名称设置为当前进程的pid,所以同一JVM中创建多个生产者要设置不同的实例名称
this.defaultMQProducer.changeInstanceNameToPID();
}
//创建客户端实例
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
//注册生产者,缓存本地:实际上就是 producerGroup映射 Producer的缓存
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);
}
//缓存createTopicKey与发布信息的映射, createTopicKey 默认值是 TBW102
this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());
if (startFactory) {
//TODO 入口:真正启动生产者的方法
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;
}
//向所有的broker发送心跳
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);
}
启动生产者的方法代码如上,最主要的流程代码是
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
第一行代码创建了一个客户端实例 MQClientInstance ,在RocketMQ中,无论生产者还是消费者,对于Broker服务端都是客户端,所以通过MQClientInstance抽象进行,通过 MQClientManager 通过factoryTable缓存进行所有实例的管理。
创建MQClientInstance 实例的时候创建了很多服务,如上图,不管消费者还是生产者,上面的逻辑都是一致的,但是是注册的逻辑不一样。
调用客户端实例的start方法启动所有的服务,方法体如下:
public void start() throws MQClientException {
synchronized (this) {
switch (this.serviceState) {
case CREATE_JUST:
this.serviceState = ServiceState.START_FAILED;
//TODO 获取远程设置的namserver地址
//没有设置nameservere地址,则默认请求: http://{jmenv.tbsite.net}:8080/rocketmq/{nsaddr}{-unitName}?nofix=1 获取,
// 可以设置系统属性修改domain和url后缀,unitName可以在procducer中设置属性值
//String wsDomainName = System.getProperty("rocketmq.namesrv.domain", "jmenv.tbsite.net");
//String wsDomainSubgroup = System.getProperty("rocketmq.namesrv.domain.subgroup", "nsaddr");
if (null == this.clientConfig.getNamesrvAddr()) {
this.mQClientAPIImpl.fetchNameServerAddr();
}
//启动客户端负责api调用的线程: 初始化netty相关组件,启动定时任务守护线程间隔1去响应缓存的请求:包括异步拉取消息,异步发送消息 ,查询消息的请求 responseTable
//定时任务的目的是将失效的消息移除缓存,然后执行回调
this.mQClientAPIImpl.start();
//启动定时任务线程,负责定时任务调度
this.startScheduledTask();
//启动消息拉取服务线程,负责消息的拉取,调用PullMessageService的run方法 pullRequestQueue pullMessage(pullRequest)
this.pullMessageService.start();
//启动负载均衡线程:守护线程,每执行一次等待20秒后再执行,调本类中的doRebalance方法,同时触发一个拉取消息的请求 consumerTable缓存操作
//也就是说
this.rebalanceService.start();
//启动(内部生产者)
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;
}
}
}
上面启动的四个服务中 pullMessageService 和 pullMessageService 这个两个服务是跟消费者相关的,生产者启动线程后会一直阻塞,因为没有数据可以消费。这个地方的设计个人没有完全明白,为什么不剥离出来消费者启动的时候才启动这两个服务。
定时任务线程startScheduledTask() 启动的任务如下:
重点查看 客户端负责api调用的线程 mQClientAPIImpl.start() ,这个线程负责处理发送完成后的异步消息。同步消息是阻塞的,所以不需要这个线程处理。方法代码如下:
这里先是创建了一个长连接的Netty客户端,然后启动了一个守护线程作定时任务,去扫描缓存的超时的响应信息,然后执行回调的方法。
结合图片,综合说明流程: 当用户调用生产者的异步发送消息接口时候,发送的消息会交给线程池处理,处理后会调用Netty的API接口发送给Broker ,由于调用时异步的,所以处理完成后会回调一个方法将响应的消息放到 responseTable这个HashMap中,这个定时任务就是除了响应后的消息,回调用户的 SendCallback方法。具体结合下面的发送消息流程
生产者发送消息流程
发送消息整个过程中最复杂的就是发送异步消息,涉及很多异步调用后回调。发送同步消息由于是阻塞的,所以流程相对简单。发送顺序消息,则只是发送请调用自定义的获取发送队列的方法,后面逻辑与前两种保持一致。
发送异步消息
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("onSuccess");
}
@Override
public void onException(Throwable e) {
}
});
这个异步发送的方法,跟踪后发现最终是将任务交给线程池处理,同时将回调的实现类传递了,如下:
继续跟踪代码调用,在DefaultMQProducerImpl类中有 private SendResult sendKernelImpl 这个方法,通过方法名可知,这是发送消息调用的核心方法,发送的消息都会调用这个方法。
如上图,这个方法中根据发送的模式,匹配不同的发送方式,异步消息调用的了异步的发送方式。
这里最后调用了netty客户端异步调用的方法,异步调用设置了回调函数,同时将我们自定的回调函数设置到回调函数中进行调用。
查看netty接口定义类如下:
最后跟踪代码发现最后调用如下:
发送成功后回调会修改消息状态,然后等到设置的超时时间过了,上面启动的时候提到的定时任务会将这个缓存剔除,然后回调我们的方法。
发送失败则是立即调用剔除缓存,进行回调,到这里整个发送流程形成闭环。
发送同步消息
发送同步消息在发送消息的核心方法后都是一样的,只是根据发送模式匹配不同的发送方式而已。调用的方式则是同步阻塞,不适用线程池的方式,所以逻辑也相对简单,就不赘述了。
由于源码中涉及很多细节,方法调用链上比较复杂,有什么疑问可以留言一起探讨。