1. 概述
在业务中,我们通常考虑如何发送可靠的消息?如何评估消息系统的发送能力?基于这2个问题,我们来阅读rocketmq的生产者模块。主要分2个部分的内容:1. 生产者如何启动和关闭 2. 生产者如何发送各类消息:普通消息、事务消息和延迟消息。普通消息:主要用来解耦业务;事务消息,是在普通消息的能力上,加上了消息发送的可靠性;延迟消息,主要为了来解决一些延迟发生的事情,例如:订单自动关闭等问题。这里已rocketmq为实例,探索mq的设计,假如有机会,可以应用对其他mq组件的学习。
2. 生产者启动和关闭
RocketMQ的生产者源码阅读是通过阅读MQProducer(org.apache.rocketmq.client.producer.MQProducer)的接口是如何被实现。这里我们主要通过start(),shutdown()和send()三个方法。MQProducer的send()主要扩展三个维度:发送方式(同步、异步、oneway)、发送事务消息sendMessageInTransaction()和是否批量。同时,我们通过消息的本身的三种类型(org.apache.rocketmq.common.message.MessageType):正常消息,事务(half和commit)和延迟(delay),来更加深入的理解RocketMQ的能力。
目标:
1. MQ本身是用来解决系统之间的解耦、削峰的目的。
2. 事务消息是为了解决消息发送可靠性的问题
3. 延迟消息是为了提升性能,满足更多复杂的业务场景,例如:支付定时取消,订单超时关闭等等
4. 有序消息(待定)
2.1 生产者启动过程
后续补;核心几个实例的创建:
- MQClientManager
- MQClientInstance
- MQClientApiImpl
- 自定义serviceThread
- RequestFutureTable的处理
- MQ的tracer
2.2 生产者关闭过程
关闭过程相对简单,就直接贴代码了。理论上一个应用的关闭主要做几件事情:
- 告诉别人要要关闭
- 各个业务线程池或线程进行关闭(易于java正常杀死)
- 核心对象赋值为null(易于gc)
而从下面代码来看,确实做了这件事情
public void shutdown(final boolean shutdownFactory) {
switch (this.serviceState) {
case CREATE_JUST:
break;
case RUNNING:
this.mQClientFactory.unregisterProducer(this.defaultMQProducer.getProducerGroup());
this.defaultAsyncSenderExecutor.shutdown();
if (shutdownFactory) {
this.mQClientFactory.shutdown();
}
this.timer.cancel();
log.info("the producer [{}] shutdown OK", this.defaultMQProducer.getProducerGroup());
this.serviceState = ServiceState.SHUTDOWN_ALREADY;
break;
case SHUTDOWN_ALREADY:
break;
default:
break;
}
}
3. 生产者的消息发送
RocketMQ的消费发送我们从方法org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl开始。看它是在发送中做了哪些处理。在这之前,最好去了解Message的结构和TopicPublishInfo的结构。所有的消息发送逻辑都是从这2个地方开始。核心解决的是:
- 现在topic对应队列的环境是什么样的
- 目前选择哪个队列,可以选择哪个broker,策略是什么
- 发送失败了,策略又应该是什么
3.1 普通消息发送逻辑
3.1.1 业务流程
3.1.1.1 选队列
selectOneMessageQueue是TopicPublishInfo的核心方法之一,下面是它的一个默认方法。通过对代码的理解,它选队列的默认策略是轮询。而更加复杂的策略是通过org.apache.rocketmq.client.latency.MQFaultStrategy类来实现。
public MessageQueue selectOneMessageQueue() {
int index = this.sendWhichQueue.incrementAndGet();
int pos = Math.abs(index) % this.messageQueueList.size();
if (pos < 0)
pos = 0;
return this.messageQueueList.get(pos);
}
这里逻辑一些选队列的场景:
- 失败重选
- 时延(sendLatencyFaultEnable)
3.1.1.2 发送内核实现
发送内核方法主要解决几件事情:
- Broker Address的获取
- 封装SendMessageRequestHeader
- mQClientFactory .getMQClientAPIImpl().sendMessage
这里说为什么主要说这三件事情。是因为通过这里的学习,你可能能够看到大多数的RocketMQ的源码。
Broker Address
对于一次通信来说,RocketMQ保证了是通过点对点通信。但是这里里面有没有什么优化,可以更加深入的去理解。例如连接共享,比如Broker Address同步。 这不去细说,可以去看代码。
封装SendMessageRequestHeader
这个才是这部分的重点,也许会觉得也是 最没有技术含量的。因为我们点进它所在的包路径:org.apache.rocketmq.common.protocol。它定义了整个RocketMQ的通信协议,假如能够看懂这个,其他的交互指令有什么困难。
mQClientFactory .getMQClientAPIImpl().sendMessage
这个是rocketmq在Remote之上封装的一层api。remote主要定义RemoteCommand和RemoteService的实现。如何将Protocol中的定义转换成RemoteCommand都是通过这一层来做。这里层解决了协议(Protocol)的问题。
下面这段代码,是对以上逻辑的一段验证。最终,MQClientAPIImpl转换成了一个RemotingCommand。
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 {
request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader);
}
这里要关注一个关键信息是RequestCode.SEND_MESSAGE。它帮助我们追踪Broker是如何进行处理生产者发送的消息的。
3.1.1.3 SendResult处理
根据RocketMQ的发送模式:Asyn、OneWay和Sync。只有Sync对SendResult做了特殊处理。
- 返回结果
- 返回结果失败时,可以通过配置,让其重试
switch (communicationMode) {
case ASYNC:
return null;
case ONEWAY:
return null;
case SYNC:
if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
continue;
}
}
return sendResult;
default:
break;
}
SendResult 也是经过了 MQClientAPIImpl处理。具体可以阅读如下代码org.apache.rocketmq.client.impl.MQClientAPIImpl#processSendResponse
SendMessageResponseHeader responseHeader =
(SendMessageResponseHeader) response.decodeCommandCustomHeader(SendMessageResponseHeader.class);
这里普通消息的发送过程已经讲完了,更加细节的内容,大家可以去代码中领略。
3.2 事务消息
上文我们讲了阅读发送正常消息的过程。接下来我们看看RocketMQ是如何实现事务消息的。在接口MQProducer中有如下接口。可以去看他的实现,发现一个问题:DefaultMQProducerImpl是没有实现这个功能的,只有TransactionMQProducer实现。我始终认为:事务消息不是一个很好的概念,容易跟数据库acid特性搞混或者隐喻,可以把它定义为"可靠消息"似乎更加合理。其核心目的是保证消息发送成功和业务执行的一致。(这里的场景需要细细分析,这里先忽略)
/**
* This method is used to send transactional messages.
*
* @param msg Transactional message to send.
* @param arg Argument used along with local transaction executor.
* @return Transaction result.
* @throws MQClientException
*/
@Override
public TransactionSendResult sendMessageInTransaction(Message msg,
Object arg) throws MQClientException {
throw new RuntimeException("sendMessageInTransaction not implement, please use TransactionMQProducer class");
}
TransactionMQProducer
在深入分析sendMessageInTransaction之前,可以先看看TransactionMQProducer的结构
public class TransactionMQProducer extends DefaultMQProducer {
private int checkThreadPoolMinSize = 1;
private int checkThreadPoolMaxSize = 1;
private int checkRequestHoldMax = 2000;
private ExecutorService executorService;
private TransactionListener transactionListener;
}
移除废弃的字段,就新增2个关键信息:1. 线程执行器,2. TransactionListener。 TransactionListener是用来回调本地事务的。
\
sendMessageInTransaction
事务发送核心逻辑与正常消息发送的逻辑差异是:
- 发送前,添加了实现相关属性(PROPERTY_TRANSACTION_PREPARED)
- 发送后,封装了事务SendResult
- 调用了TransactionListener
- 调用endTransaction方法
这里我给自己提了了2个问题:1. RemotingCommand有什么差异 2. Broker是如何处理事务消息
RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, requestHeader);
request.setRemark(remark);
this.remotingClient.invokeOneway(addr, request, timeoutMillis);
这段代码展示了RequestCode.END_TRANSACTION,这个指令要记住,看它如何被broker处理。
经典问题:二段提交如何解决一致性问题?
3.3 延迟消息
延迟消息没有专门的发送方法,主要在RocketMQ中broker中实现延迟机制。而发送端,只能靠Message的方法setDelayTimeLevel()。 总共18个级别,可自定义。具体查看引用的文章[1]
4 总结
这篇主要是通过抓住MQProducer这个接口,通过解析其核心能力发送消息(Send)和Message对象来分开解释了其实现原理。其中,事务消息主要通过二段提交的经典解决方案来保证一致性,正常消息和延迟消息居然只是个别属性的差异。底层依赖于Remoting模块,在这模块构建了MQClientAPIImpl。 合理的分层和封装,简化了业务复杂度,可以模仿和学习。 这里少了异步消息的处理方式。