RocketMQ消息发送
RocketMQ 支持3 种消息发送方式: 同步 (sync)、异步(async)、单向(oneway)。
-
同步:发送者向 MQ 执行发送消息API 时,同步等待,直到消息服务器返回发送结果。
-
异步:发送者向MQ 执行发送消息API 时,指定消息发送成功后的回调函数,然后调用消息发送API 后,立即返回,消息发送者线程不阻塞,直到运行结束,消息发送成功或失败的回调任务在一个新的线程中返回。
-
单向:消息发送者向MQ 执行发送消息API 时,直接返回,不等待消息服务器的结果,也不注册回调函数,只管发,不管是否成功存储在消息服务器上。
一、Message 类
org.apache.rocketmq.common.message.Message
private String topic;
private int flag;
//存储 TAGS ,KEYS ,DELAY,WAIT 等键值数据
private Map<String, String> properties;
private byte[] body;
private String transactionId;
//TODO Message 索引键,多个用空格隔开,RocketMQ 可以根据这些 key 快速检索到消息
public void setKeys(Collection<String> keys) {
StringBuffer sb = new StringBuffer();
for (String k : keys) {
sb.append(k);
sb.append(MessageConst.KEY_SEPARATOR);
}
this.setKeys(sb.toString().trim());
}
//TODO 消息延迟界别,用于定时消息或消息重试。
public int getDelayTimeLevel() {
String t = this.getProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL);
if (t != null) {
return Integer.parseInt(t);
}
return 0;
}
public void setDelayTimeLevel(int level) {
this.putProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL, String.valueOf(level));
}
//TODO WaitStoreMsgOK 消息发送时是否等消息存储完成后再返回
public boolean isWaitStoreMsgOK() {
String result = this.getProperty(MessageConst.PROPERTY_WAIT_STORE_MSG_OK);
if (null == result)
return true;
return Boolean.parseBoolean(result);
}
public void setWaitStoreMsgOK(boolean waitStoreMsgOK) {
this.putProperty(MessageConst.PROPERTY_WAIT_STORE_MSG_OK, Boolean.toString(waitStoreMsgOK));
}
二、DefaultMQProducer
(一)、核心属性
成员变量 | 解释 |
---|---|
producerGroup | 生产者所属组,消息服务器在回查事务状态时会随机选择该组中任何一个生产者发起事务回查请求。 |
createTopicKey | TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; 默认topic |
defaultTopicQueueNums | 默认主题在每一个Broker队列数量 |
sendMsgTimeout | 发送消息默认超时时间,默认3s |
compressMsgBodyOverHowmuch | 消息体超过该值则启用压缩 默认 4 k |
retryTimesWhenSendFailed | 同步方式发送消息重试次数,默认为 2 ,总共执行 3 次 |
retryTimesWhenSendAsyncFailed | 异步方式发送消息重试次数,默认为 2 |
retryAnotherBrokerWhenNotStoreOK | 消息重试时选择另外一个 Broker 时,是否不等待存储结果就返回,默认为false |
maxMessageSize | 允许发送的最大消息长度,默认为 4M ,该值最大值为 2^32-1 。 |
(二)、核心方法
发送方法比较多,这里不再挨个列举,方法很好识别,异步与同步的区分就是是否加上了 SendCallBack , 单向方式方法名都有Oneway标识
(三)启动流程:
启动流程是从 DefaultProducer. start() 方法开始 ,
public void start() throws MQClientException {
this.setProducerGroup(withNamespace(this.producerGroup));
//TODO DefaultMQProducerImpl.start
this.defaultMQProducerImpl.start();
if (null != traceDispatcher) {
try {
traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
} catch (MQClientException e) {
log.warn("trace dispatcher start failed ", e);
}
}
}
可以看出实际的启动流程是从 DefaultMQProducerImpl.start 开始的。
this.serviceState = ServiceState.START_FAILED;
//TODO Step1 : 检查 productGroup 是否符合要求 ; 并改变生产者的 instanceName 为进程 ID。
this.checkConfig();
if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
this.defaultMQProducer.changeInstanceNameToPID(); //并改变生产者的 instanceName 为进程 ID。
}
//TODO Step2 : 创建 MQClientInstance 实例。整个JVM 实例中只存在一个 MQClientManager 实例,维护一个 MQClienInstance 缓存表
//TODO ConcurrentMap<String /* clientId */,MQClientInstance> factoryTable = new ConcurrentMap<String,MQClientInstance>(),也就是同一个 cliendId 只会创建一个MQClienInstance。
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
//TODO Step3 : 向 MQClientInstance 注册,将当前生产者加入到 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());
//TODO Step4 : 启动 MQClientInstance ,如果 MQClientInstance 已经启动,则本次启动不会真正执行
if (startFactory) {
mQClientFactory.start();
}
log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(),
this.defaultMQProducer.isSendMessageWithVIPChannel());
this.serviceState = ServiceState.RUNNING;
break;
public MQClientInstance getOrCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) {
//TODO clientId 为客户端 IP + instance + (unitname 可选) ,为了防止同一台服务器上部署两个 client 造成 clienId 混乱
String clientId = clientConfig.buildMQClientId();
MQClientInstance instance = this.factoryTable.get(clientId);
if (null == instance) {
instance =
new MQClientInstance(clientConfig.cloneClientConfig(),
this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook);
MQClientInstance prev = this.factoryTable.putIfAbsent(clientId, instance);
if (prev != null) {
instance = prev;
log.warn("Returned Previous MQClientInstance for clientId:[{}]", clientId);
} else {
log.info("Created new MQClientInstance for clientId:[{}]", clientId);
}
}
return instance;
}
(四)消息发送:
1. 消息发送的基本流程
消息发送主要步骤:验证消息、查找路由、消息发送(包含异常处理机制)
DefaultMQProducer#send
//同步发送
public SendResult send(
Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
Validators.checkMessage(msg, this);
msg.setTopic(withNamespace(msg.getTopic()));
return this.defaultMQProducerImpl.send(msg);
}
//
public void send(Message msg, MessageQueue mq, SendCallback sendCallback)
throws MQClientException, RemotingException, InterruptedException {
msg.setTopic(withNamespace(msg.getTopic()));
this.defaultMQProducerImpl.send(msg, queueWithNamespace(mq), sendCallback);
}
public void sendOneway(Message msg,
MessageQueue mq) throws MQClientException, RemotingException, InterruptedException {
msg.setTopic(withNamespace(msg.getTopic()));
this.defaultMQProducerImpl.sendOneway(msg, queueWithNamespace(mq));
}
默认消息以同步方式发送,默认超时时间是 3s
-
消息长度验证
消息发送之前,首先确保生产者处于运行状态,然后验证消息是否符合响应的规范,具体的规范要求是主题名称、消息提不能为空、消息长度不能等于0且默认不能超过允许发送消息的最大长度4M(maxMessageSize = 1024*1024*4)
-
查找主题路由信息
发送消息之前,首先要获取主题的路由信息,只有获取了这些信息我们才知道消息要大送到具体的Broker节点。
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) { TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic); if (null == topicPublishInfo || !topicPublishInfo.ok()) { this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo()); this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic); topicPublishInfo = this.topicPublishInfoTable.get(topic); } if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) { return topicPublishInfo; } else { this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer); topicPublishInfo = this.topicPublishInfoTable.get(topic); return topicPublishInfo; } }
tryToFindTopicPublishInfo 是查找主题的路由信息的方法。如果生产者中缓存了 topic 的路由信息,如果该路由信息中包含了消息队列,则直接返回路由信息,如果没有缓存或没有包含消息队列,则向NameServer 查询topic 的路由信息。如果最终未找到路由信息,则抛出异常。
TopicPublishInfo
TopicPublishInfo //TODO 是否是顺序消息 private boolean orderTopic = false; private boolean haveTopicRouterInfo = false; //TODO 该主题队列的消息队列。 private List<MessageQueue> messageQueueList = new ArrayList<MessageQueue>(); //TODO 每选择一次消息队列,该值会自增 1 ,如果 Integer.MAX_VALUE 则重置为 0 private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); private TopicRouteData topicRouteData; TopicRouteData private String orderTopicConf; //TODO topic 队列元数据 private List<QueueData> queueDatas; //TODO topic 分布的broker 元数据 private List<BrokerData> brokerDatas; private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
-
选择消息队列
消息发送端采用重试机制,由 **retryTimesWhenSendFailed 指定同步方式重试次数,异步重试机制在收到消息发送结构后执行回调之前进行重试,由retryTimesWhenSendAsyncFailed **指定,接下来就是循环执行,选择消息队列、发送消息,发送成功则返回,收到异常则重试。
选择消息队列有两种方式
1). sendLatencyFaultEnable = false ,默认不起用故障延迟机制。
2). sendLatencyFaultEnable = true , 启用Broker 启用故障延迟机制
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) { if (this.sendLatencyFaultEnable) { try { 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); if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) return mq; } final String notBestBroker = latencyFaultTolerance.pickOneAtLeast(); int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker); if (writeQueueNums > 0) { final MessageQueue mq = tpInfo.selectOneMessageQueue(); if (notBestBroker != null) { mq.setBrokerName(notBestBroker); mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums); } return mq; } else { latencyFaultTolerance.remove(notBestBroker); } } catch (Exception e) { log.error("Error occurred when selecting message queue", e); } return tpInfo.selectOneMessageQueue(); } return tpInfo.selectOneMessageQueue(lastBrokerName); } public MessageQueue selectOneMessageQueue(final String lastBrokerName) { if (lastBrokerName == null) { return selectOneMessageQueue(); } else { for (int i = 0; i < this.messageQueueList.size(); i++) { int index = this.sendWhichQueue.getAndIncrement(); int pos = Math.abs(index) % this.messageQueueList.size(); if (pos < 0) pos = 0; MessageQueue mq = this.messageQueueList.get(pos); if (!mq.getBrokerName().equals(lastBrokerName)) { return mq; } } return selectOneMessageQueue(); } } public MessageQueue selectOneMessageQueue() { int index = this.sendWhichQueue.getAndIncrement(); int pos = Math.abs(index) % this.messageQueueList.size(); if (pos < 0) pos = 0; return this.messageQueueList.get(pos); }
默认机制:
在消息发送的过程中,可能会多次执行选择消息队列这个方法,lastBrokerName 就是上一次选择的执行发送消息失败的Broker。第一次发送消息时lastBrokerName 为null , 此时直接用 sendWhichQueue 与 当前路由表中消息队列个数取模,返回改位置的MessageQueue ,如果之前消息发送失败,下次消息队列选择时会规避上次失败的MessageQueue所在的Broker。
为什么Broker不可用后,路由信息还会包含Broker信息呢?因为NameServer 检测 Broker是有延迟的,一次心跳检测间隔(10s), NameServer 不会检测到 Broker 宕机后马上推送消息给生产者,而是生产者每隔30s更新一次路由信息,所以生产者最快感知Broker最新的路由信息也需要30s。
故障延迟机制:
-
采用默认方式的取模运算选取消息队列,然后判断该队列是否可用,若可用直接返回。
-
当所有的消息队列都在故障列表中且都不可用,此时会在故障列表中选择一个broker进行尝试。
//故障延迟机制接口定义 public interface LatencyFaultTolerance<T> { //更新故障列表 void updateFaultItem(final T name, final long currentLatency, final long notAvailableDuration); //判断是否可用 boolean isAvailable(final T name); //移除 void remove(final T name); //至少选择一个 T pickOneAtLeast(); } // FaultItem 失败条目 class FaultItem implements Comparable<FaultItem> { //TODO brokerName private final String name; //TODO 本次消息发送延迟 private volatile long currentLatency; //TODO 故障规避开始时间 private volatile long startTimestamp;
这里我们看下故障延迟时间的计算方式,以及判断是否可用的判断逻辑
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime); endTimestamp = System.currentTimeMillis(); this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false); public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) { this.mqFaultStrategy.updateFaultItem(brokerName, currentLatency, isolation); }
在执行发送消息完成之后无论成功失败都会执行 updateFaultItem 方法,而真正实现故障策略的类是 MQFaultStrategy。
MQFaultStrategy.class
// currentLatency 为本次发送消息的延迟时间 //isolation 是否隔离,该参数如果为 true ,则采用默认时长30s来计算Broker故障规避时间,如果为false 则使用本次延迟时间来计算Broker故障规避时间 public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) { if (this.sendLatencyFaultEnable) { long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency); this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration); } } //TODO 根据发送延迟时间计算 需要 隔离时间 private long computeNotAvailableDuration(final long currentLatency) { for (int i = latencyMax.length - 1; i >= 0; i--) { if (currentLatency >= latencyMax[i]) return this.notAvailableDuration[i]; } return 0; } //TODO 根据currentLatency 本次消息发送延迟,从latencyMax 尾部向前找到第一个比 根据currentLatency 小的索引index, 如果没有找到,返回0。 然后根据这个索引从 notAvailableDuration 数组中取出对应的时间,在这个时长内,Broker将设置为不可用 private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L}; private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};
据currentLatency 本次消息发送延迟,从latencyMax 尾部向前找到第一个比 根据currentLatency 小的索引index, 如果没有找到,返回0。 然后根据这个索引从 notAvailableDuration 数组中取出对应的时间,在这个时长内,Broker将设置为不可用。
LatencyFaultToleranceImpl 中实现了故障列表更新,以及判断是否可用,此处代码简单,不在赘述。
public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration) { FaultItem old = this.faultItemTable.get(name); if (null == old) { final FaultItem faultItem = new FaultItem(name); faultItem.setCurrentLatency(currentLatency); faultItem.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration); old = this.faultItemTable.putIfAbsent(name, faultItem); if (old != null) { old.setCurrentLatency(currentLatency); old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration); } } else { old.setCurrentLatency(currentLatency); old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration); } } public boolean isAvailable() { return (System.currentTimeMillis() - startTimestamp) >= 0; }
-
-
消息发送
消息发送的核心API 是 DefaultMQProducerImpl#sendKernelImpl
private SendResult sendKernelImpl(final Message msg, final MessageQueue mq, final CommunicationMode communicationMode, final SendCallback sendCallback, final TopicPublishInfo topicPublishInfo, final long timeout)
方法参数:
final Message msg 需要发送的消息
MessageQueue mq 目标消息队列
CommunicationMode communicationMode 发送模式,sync, async ,oneway
final SendCallback sendCallback , 异步发送回调函数
TopicPublishInfo topicPublishInfo 主题消息路由
final long timeout 超时时间
//TODO 为消息设置全局ID if (!(msg instanceof MessageBatch)) { MessageClientIDSetter.setUniqID(msg); } boolean topicWithNamespace = false; if (null != this.mQClientFactory.getClientConfig().getNamespace()) { msg.setInstanceId(this.mQClientFactory.getClientConfig().getNamespace()); topicWithNamespace = true; } int sysFlag = 0; boolean msgBodyCompressed = false; //TODO 如果消息长度超过 4K ,采用zip 压缩,设置消息的系统标记为 MessageSysFlag.COMPRESSED_FLAG if (this.tryToCompressMessage(msg)) { sysFlag |= MessageSysFlag.COMPRESSED_FLAG; msgBodyCompressed = true; } //TODO 如果是事务消息,设置消息的系统标记为 MessageConst.PROPERTY_TRANSACTION_PREPARED final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED); if (tranMsg != null && Boolean.parseBoolean(tranMsg)) { sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE; }
//TODO 执行消息发送钩子函数,执行消息发送之前的增强逻辑。通过registerSendMessageHook 方法进行注册 if (this.hasSendMessageHook()) { context = new SendMessageContext(); context.setProducer(this); context.setProducerGroup(this.defaultMQProducer.getProducerGroup()); context.setCommunicationMode(communicationMode); context.setBornHost(this.defaultMQProducer.getClientIP()); context.setBrokerAddr(brokerAddr); context.setMessage(msg); context.setMq(mq); context.setNamespace(this.defaultMQProducer.getNamespace()); String isTrans = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED); if (isTrans != null && isTrans.equals("true")) { context.setMsgType(MessageType.Trans_Msg_Half); } if (msg.getProperty("__STARTDELIVERTIME") != null || msg.getProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL) != null) { context.setMsgType(MessageType.Delay_Msg); } this.executeSendMessageHookBefore(context); } //TODO 构建消息发送请求包,主要包含以下信息:、,, SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup()); //生产者组、 requestHeader.setTopic(msg.getTopic()); //主题名称 requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey()); //默认创建主题 key, requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums()); //该主题在单个 Broker默认队列数 requestHeader.setQueueId(mq.getQueueId()); //队列ID requestHeader.setSysFlag(sysFlag); //消息系统标记 requestHeader.setBornTimestamp(System.currentTimeMillis()); //消息发送时间 requestHeader.setFlag(msg.getFlag()); //Flag 标记,系统不处理,应用程序处理 requestHeader.setProperties(MessageDecoder.messageProperties2String(msg.getProperties())); //消息扩展属性 requestHeader.setReconsumeTimes(0); //重试次数 requestHeader.setUnitMode(this.isUnitMode()); requestHeader.setBatch(msg instanceof MessageBatch); //是否是批量消息 //TODO 如果topic 是重试队列,会将msg中的重试次数 和 最大重试次数设置到请求头中 if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { String reconsumeTimes = MessageAccessor.getReconsumeTime(msg); if (reconsumeTimes != null) { requestHeader.setReconsumeTimes(Integer.valueOf(reconsumeTimes)); MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_RECONSUME_TIME); } String maxReconsumeTimes = MessageAccessor.getMaxReconsumeTimes(msg); if (maxReconsumeTimes != null) { requestHeader.setMaxReconsumeTimes(Integer.valueOf(maxReconsumeTimes)); MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_MAX_RECONSUME_TIMES); } }
SendResult sendResult = null; switch (communicationMode) { case ASYNC: Message tmpMessage = msg; boolean messageCloned = false; if (msgBodyCompressed) { //If msg body was compressed, msgbody should be reset using prevBody. //Clone new message using commpressed message body and recover origin massage. //Fix bug:https://github.com/apache/rocketmq-externals/issues/66 tmpMessage = MessageAccessor.cloneMessage(msg); messageCloned = true; msg.setBody(prevBody); } if (topicWithNamespace) { if (!messageCloned) { tmpMessage = MessageAccessor.cloneMessage(msg); messageCloned = true; } msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQProducer.getNamespace())); } long costTimeAsync = System.currentTimeMillis() - beginStartTime; if (timeout < costTimeAsync) { throw new RemotingTooMuchRequestException("sendKernelImpl call timeout"); } sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage( brokerAddr, mq.getBrokerName(), tmpMessage, requestHeader, timeout - costTimeAsync, communicationMode, sendCallback, topicPublishInfo, this.mQClientFactory, this.defaultMQProducer.getRetryTimesWhenSendAsyncFailed(), context, this); break; case ONEWAY: case SYNC: long costTimeSync = System.currentTimeMillis() - beginStartTime; if (timeout < costTimeSync) { throw new RemotingTooMuchRequestException("sendKernelImpl call timeout"); } sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage( brokerAddr, mq.getBrokerName(), msg, requestHeader, timeout - costTimeSync, communicationMode, context, this); break; default: assert false; break; } if (this.hasSendMessageHook()) { context.setSendResult(sendResult); this.executeSendMessageHookAfter(context); } return sendResult;