前言
Producer是RocketMQ中的消息发送者,本篇文章将介绍Producer是如何将消息发送到broker中。先看下官方文档中的例子,消息推送总共包括4个步骤
- 创建Producer对象,并设置namesrv
- 启动producer
- 推送消息
- 关闭producer
public class Producer {
public static final String PRODUCER_GROUP = "ProducerGroupName";
public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
public static final String TOPIC = "TestTopic";
public static final String TAG = "TagA";
public static void main(String[] args) throws MQClientException, InterruptedException {
// 1. 创建producer
DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
// 2. 启动Producer
producer.start();
// 3. 推送消息
for (int i = 0; i < 1; i++) {
try {
Message msg = new Message(TOPIC, TAG, "OrderID188", "Hello world".getBytes(StandardCharsets.UTF_8));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
} catch (Exception e) {
e.printStackTrace();
}
}
// 4. 关闭producer
producer.shutdown();
}
}
MQProducer还有另一个实现TransactionMQProducer事务消息生产者,它是从4.1.3版本开始支持事务消息的,事务消息的推送流程与普通消息略有不同,本文不详细说明。
Producer相关类介绍
Producer的默认实现是DefaultMQProducer,从下面类图可以看到它实现了MQProducer接口,继承了ClientConfig类。
MQAdmin接口
MQAdmin接口是RocketMQ管理的根接口,它提供了Topic创建,MessageQueue offset操作以及消息查询的方法
// 创建topic
void createTopic(final String key, final String newTopic, final int queueNum, Map<String, String> attributes)
throws MQClientException;
// 创建topic
void createTopic(String key, String newTopic, int queueNum, int topicSysFlag, Map<String, String> attributes)
throws MQClientException;
// 根据某个时间(以毫秒为单位)获取消息队列偏移量
long searchOffset(final MessageQueue mq, final long timestamp) throws MQClientException;
// 获取最大offset
long maxOffset(final MessageQueue mq) throws MQClientException;
// 获取最小offset
long minOffset(final MessageQueue mq) throws MQClientException;
// 获取最早存储消息的时间
long earliestMsgStoreTime(final MessageQueue mq) throws MQClientException;
// 根据消息id查询消息
MessageExt viewMessage(final String offsetMsgId) throws RemotingException, MQBrokerException,
InterruptedException, MQClientException;
// 查询消息
QueryResult queryMessage(final String topic, final String key, final int maxNum, final long begin,
final long end) throws MQClientException, InterruptedException;
// 根据topic和消息id查询消息
MessageExt viewMessage(String topic,
String msgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException;
MQProducer接口
MQProducer是Producer的接口方法,它定义了Producer启动、停止、根据topic获取MessageQueue以及消息发送的方法。MQProducer提供的消息推送的方法超过20个,参考下面代码,提取了关键的消息推送代码
// 打开Producer
void start() throws MQClientException;
// 关闭Producer
void shutdown();
// 根据topic获取MessageQueue
List<MessageQueue> fetchPublishMessageQueues(final String topic) throws MQClientException;
// 推送消息
SendResult send(final Message msg) throws MQClientException, RemotingException, MQBrokerException,
InterruptedException;
// 省略部分方法
// 推送异步消息
void send(final Message msg, final SendCallback sendCallback) throws MQClientException,
RemotingException, InterruptedException;
// 省略部分方法
// 单向详细推送
void sendOneway(final Message msg) throws MQClientException, RemotingException,
InterruptedException;
// 将消息推送到指定MessageQueue中
SendResult send(final Message msg, final MessageQueue mq) throws MQClientException,
RemotingException, MQBrokerException, InterruptedException;
// 省略部分方法
// 单向消息推送,并将消息推送到指定的类中
void sendOneway(final Message msg, final MessageQueue mq) throws MQClientException,
RemotingException, InterruptedException;
// 推送消息,并根据selector推送到指定的MessageQueue中
SendResult send(final Message msg, final MessageQueueSelector selector, final Object arg)
throws MQClientException, RemotingException, MQBrokerException, InterruptedException;
// 推送事务消息
TransactionSendResult sendMessageInTransaction(final Message msg,
final LocalTransactionExecuter tranExecuter, final Object arg) throws MQClientException;
TransactionSendResult sendMessageInTransaction(final Message msg,
final Object arg) throws MQClientException;
// 推送批量消息
SendResult send(final Collection<Message> msgs) throws MQClientException, RemotingException, MQBrokerException,
InterruptedException;
// 省略部分推送代码
send方法从消息类型分可以分为下面两种
- 普通消息
普通消息是普通的生产者消费者模型的消息。
- 事务消息
事务消息是在一些对数据一致性有强需求的场景,保证上下游数据的一致性,它在普通消息的基础上支持两阶段提交的能力,将两阶段提交和本地事务绑定,实现全局提交结果的一致性。
send方法从消息推送的方式分可以分为以下三种
- 同步发送消息
同步发送是指Producer发送消息后,当前线程会被阻塞,知道服务端返回结果或发生超时异常,我们在发送消息时需要同步指导消息发送成功还是失败,一般会采用这种方式
- 异步发送消息
异步发送消息不会阻塞线程,发送后会立即返回,发送的结果会在异步线程里面执行回调来获取。异步发送在发送时会传入SendCallback接口的实现类,它包含成功和异常时的回调方法。
public interface SendCallback {
// 成功时返回发送结果
void onSuccess(final SendResult sendResult);
// 失败时返回异常信息
void onException(final Throwable e);
}
- 单向发送消息
单向发送消息是指客户端发送消息后不会等待服务端返回结果,并且会忽略服务端返回的结果,客户端也不会被阻塞。
send方法从发送消息数量分可以分为下面两种
- 单条消息推送
- 消息批量推送
ClientConfig类
ClientConfig是客户端的公共配置类,这里的客户端包括Producer和Concumer,它总共有5个子类,其中和Producer相关的子类有DefaultMQProducer和TransactionMQProducer。
ClientConfig中包含的配置如下所示
public static final String SEND_MESSAGE_WITH_VIP_CHANNEL_PROPERTY = "com.rocketmq.sendMessageWithVIPChannel";
public static final String SOCKS_PROXY_CONFIG = "com.rocketmq.socks.proxy.config";
public static final String DECODE_READ_BODY = "com.rocketmq.read.body";
public static final String DECODE_DECOMPRESS_BODY = "com.rocketmq.decompress.body";
// namesrv地址信息
private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses();
// 本机ip
private String clientIP = NetworkUtil.getLocalAddress();
// 实例名称
private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT");
private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors();
protected String namespace;
private boolean namespaceInitialized = false;
protected AccessChannel accessChannel = AccessChannel.LOCAL;
// 从name server轮询拉去topic信息的时间间隔,单位是ms
private int pollNameServerInterval = 1000 * 30;
// 向Broker发送心跳的时间间隔,单位是ms
private int heartbeatBrokerInterval = 1000 * 30;
// 持久化Consumer消费进度的时间间隔,单位是ms
private int persistConsumerOffsetInterval = 1000 * 5;
private long pullTimeDelayMillsWhenException = 1000;
private boolean unitMode = false;
private String unitName;
private boolean decodeReadBody = Boolean.parseBoolean(System.getProperty(DECODE_READ_BODY, "true"));
private boolean decodeDecompressBody = Boolean.parseBoolean(System.getProperty(DECODE_DECOMPRESS_BODY, "true"));
private boolean vipChannelEnabled = Boolean.parseBoolean(System.getProperty(SEND_MESSAGE_WITH_VIP_CHANNEL_PROPERTY, "false"));
private boolean useTLS = TlsSystemConfig.tlsEnable;
private String socksProxyConfig = System.getProperty(SOCKS_PROXY_CONFIG, "{}");
private int mqClientApiTimeout = 3 * 1000;
private LanguageCode language = LanguageCode.JAVA;
DefaultMQProducer源码分析
关键属性分析
DefaultMQProducer的关键属性如下所示,它主要包含了包装了所有方法的实现类以及一些消息推送时会用到的配置
// 它是包装类所有方法的内部实现类
protected final transient DefaultMQProducerImpl defaultMQProducerImpl;
// 返回结果需要重试的ResponseCode
private final Set<Integer> retryResponseCodes = new CopyOnWriteArraySet<>(Arrays.asList(
// Topic不存在
ResponseCode.TOPIC_NOT_EXIST,
// 服务不可以用
ResponseCode.SERVICE_NOT_AVAILABLE,
// 系统异常
ResponseCode.SYSTEM_ERROR,
// 无权限
ResponseCode.NO_PERMISSION,
ResponseCode.NO_BUYER_ID,
ResponseCode.NOT_IN_CURRENT_UNIT
));
// producerGroup是相同角色生产者的聚合概念,它对于事务消息很重要,对于普通消息不重要
private String producerGroup;
// 自动创建topic的key TBW102
private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC;
// 默认topic的队列数量
private volatile int defaultTopicQueueNums = 4;
// 消息发送超时时间,默认3秒
private int sendMsgTimeout = 3000;
// 压缩消息体阈值,如果消息体大于4K,则默认会被压缩
private int compressMsgBodyOverHowmuch = 1024 * 4;
// 同步模式下消息推送失败重试次数,这个配置可能会导致消息重复
private int retryTimesWhenSendFailed = 2;
// 异步模式下消息推送失败重试次数,这个配置可能会导致消息重复
private int retryTimesWhenSendAsyncFailed = 2;
// 消息推送失败是否要换选择另外一个broker重试
private boolean retryAnotherBrokerWhenNotStoreOK = false;
// 消息允许的最大size,默认4M
private int maxMessageSize = 1024 * 1024 * 4;
// 异步消息追踪器
private TraceDispatcher traceDispatcher = null;
start()源码分析
DefaultMQProducer#start底层调用的是DefaultMQProducerImpl#start。
public void start() throws MQClientException {
// 包装producerGroup
this.setProducerGroup(withNamespace(this.producerGroup));
this.defaultMQProducerImpl.start();
if (null != traceDispatcher) {
try {
traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
} catch (MQClientException e) {
logger.warn("trace dispatcher start failed ", e);
}
}
}
在调用DefaultMQProducerImpl#start方法之前,将producerGroup用namespace包装了一遍,并重新set到Producer中,拼接方法如下图所示。如果是重试队列,则会拼接%RETRY%
,如果是死信队列,则会拼接%DLQ%
,如果包含namespace,则会拼接namespace
,如果都不包含,则会返回原producerGroup,如果创建Producer时没有显式传入producerGroup,则默认producerGroup的值为DEFAULT_PRODUCER
。
DefaultMQProducerImpl#start源码如下所示,这个方法的主要功能如下
- 获取/创建MQClientInstance
DefaultMQProducerImpl获取/创建MQClientInstance后,会将该Producer的信息注册到MQClientInstance中,并且把MQClientInstance赋值给DefaultMQProducerImpl的成员变量mQClientFactory
- 向Broker发送心跳
向所有Broker发送心跳
- 启动超时请求定时任务
这个定时任务每秒都会扫描队列中的请求是否超时
public void start(final boolean startFactory) throws MQClientException {
switch (this.serviceState) {
case CREATE_JUST:
// 状态先改成开始失败
this.serviceState = ServiceState.START_FAILED;
// 省略部分代码
// 创建MQClientInstance类,它是负责网络通信的工具类
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
// 将当前producer注册到MQClientInstance
boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
// 省略部分代码
this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());
if (startFactory) {
// 启动MQClientInstance
mQClientFactory.start();
}
// 省略部分代码
this.serviceState = ServiceState.RUNNING;
break;
// 省略部分代码
}
// 向Broker心跳
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
// 定时扫描超时的请求
RequestFutureHolder.getInstance().startScheduledTask(this);
}
shutdown()源码分析
DefaultMQProducerImpl#shutdown主要完成下面3个工作
- 从MQClientInstance中注销Producer
- 关闭异步线程池
- 注销返回结果定时扫描线程池
public void shutdown(final boolean shutdownFactory) {
// 省略部分代码
// 注销Producer
this.mQClientFactory.unregisterProducer(this.defaultMQProducer.getProducerGroup());
// 关闭异步线程池
this.defaultAsyncSenderExecutor.shutdown();
if (shutdownFactory) {
this.mQClientFactory.shutdown();
}
// 注销返回结果定时扫描线程池
RequestFutureHolder.getInstance().shutdown(this);
// 省略部分代码
}
}
send()源码分析
下面以同步send源码举例,send的方法较长,整个推送过程可以分为以下部分
- 根据topic获取topic路由信息(
TopicPublishInfo
),如果是同步,则会循环``1+重试次数`执行推送 - 从topic路由信息中选择一个要推送的MessageQueue
- 记录推送延迟信息
- 如果是同步,并且推送失败,则会选择其他Queue,并重新推送,如果是单向推送或者是
- 处理其他异常代码
private SendResult sendDefaultImpl(
Message msg,
final CommunicationMode communicationMode /*同步,异步,oneWay模式*/,
final SendCallback sendCallback,
final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
this.makeSureStateOK(); // producer 状态是否是RUNNING
Validators.checkMessage(msg, this.defaultMQProducer); // 校验topic和MessageBody
//省略部分代码
// 根据topic获取topic路由信息
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
if (topicPublishInfo != null && topicPublishInfo.ok()) {
// 省略部分代码
// 同步模式下重试次数 1+重试次数
int timesTotal = communicationMode==CommunicationMode.SYNC?1+this.defaultMQProducer.getRetryTimesWhenSendFailed():1;
int times = 0;
// 记录每次推送broker的数组
String[] brokersSent = new String[timesTotal];
// 最多推送timesTotal次
for (; times < timesTotal; times++) {
String lastBrokerName = null == mq ? null : mq.getBrokerName();
// 重新选择MessageQueue
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
if (mqSelected != null) {
mq = mqSelected;
brokersSent[times] = mq.getBrokerName();
try {
// 推送开始时间
beginTimestampPrev = System.currentTimeMillis();
// 省略部分代码
long costTime = beginTimestampPrev - beginTimestampFirst;
if (timeout < costTime) { // 总推送时间超过超时时间,就不推了
callTimeout = true;
break;
}
// 推送消息
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback,topicPublishInfo,timeout-costTime);
endTimestamp = System.currentTimeMillis();
// 记录推送延迟
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
switch (communicationMode) {
// 如果同步时推送失败,并且开启了换一个broker重推的方式,则会重新推送,其他模式则返回空result
case SYNC:
if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
continue;
}
}
case ASYNC:
return null;
case ONEWAY:
return null;
// 省略其他模式代码
}
} catch (RemotingException | MQClientException e) {
endTimestamp = System.currentTimeMillis();
// 更新延迟信息
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
exception = e;
continue;
} catch // 省略其他异常处理代码
} else {
break;
}
}
if (sendResult != null) {
return sendResult;
}
// 构建异常信息
String info = String.format("Send [%d] times, still failed, cost [%d]ms, Topic: %s, BrokersSent: %s", times, System.currentTimeMillis() - beginTimestampFirst,msg.getTopic(), Arrays.toString(brokersSent));
info += FAQUrl.suggestTodo(FAQUrl.SEND_MSG_FAILED);
// 构建MQ Client 异常
MQClientException mqClientException = new MQClientException(info, exception);
// 如果超时了,则抛出超时异常
if (callTimeout) {
throw new RemotingTooMuchRequestException("sendDefaultImpl call timeout");
}
// 根据Exception,set不同的responseCode
if (exception instanceof MQBrokerException) {
mqClientException.setResponseCode(((MQBrokerException) exception).getResponseCode());
} // 省略部分代码
throw mqClientException;
}
validateNameServerSetting();
throw new MQClientException("No route info of this topic: " + msg.getTopic() + FAQUrl.suggestTodo(FAQUrl.NO_TOPIC_ROUTE_INFO),
null).setResponseCode(ClientErrorCode.NOT_FOUND_TOPIC_EXCEPTION);
}