2.RocketMQ消息发送
2.1消息Message
RocketMQ发送普通消息有三种实现方式:可靠同步发送、可靠异步发送、单向(Oneway)发送。
RocketMQ支持三种消息发送方式:同步(sync)、异步(async)、单向(oneway)
- 同步:发送者向MQ执行发送消息API时,同步等待,直到消息服务器返回发送结果
- 异步:发送者向MQ执行发送消息API时,指定消息发送成功后的回调函数,然后调用消息发送API后,立即返回,消息发送者线程不阻塞,直到运行结束,消息发送成功或失败的回调任务在一个新的线程中执行
- 单向:消息发送者向MQ执行发送消息API时,直接返回,不等待消息服务器的结果,也不注册回调函数,简单地说,就是只管发,不在乎消息是否成功存储在消息服务器上
Message的基础属性主要包括消息所属主题topic、消息Flag(RocketMQ不做处理)、扩展属性、消息体
Message的扩展属性,这些扩展属性存储在Message的properties中:
- tag:消息TAG,用于消息过滤
- keys:Message索引键,多个用空格隔开,RocketMQ可以根据这些key快速检索到消息
- waitStoreMsgOK:消息发送时是否等消息存储完成后再返回
- delayTimeLevel:消息延迟级别,用于定时消息或消息重试
2.2生产者启动流程
DefaultMQProducer
DefaultMQProducer是默认的消息生产者实现类,它实现MQAdmin的接口
-
创建主题:void createTopic(String key, String newTopic, int queueNum, int topicSysFlag)
- key:目前未实际作用,可以与newTopic相同
- newTopic:主题名称
- queueNum:队列数量
- topicSysFlag:主题系统标签,默认为0
-
根据时间戳从队列中查找其偏移量:long searchOffset(final MessageQueue mq, final long timestamp)
-
查找该消息队列中最大的物理偏移量:long maxOffset(final MessageQueue mq)
-
查找该消息队列中最小的物理偏移量:long minOffset(final MessageQueue mq)
-
根据消息偏移量查找消息:MessageExt viewMessage(final String offsetMsgId)
-
根据条件查询消息:QueryResult queryMessage(final String topic, final String key, final int maxNum, final long begin, final long end)
- topic:消息主题
- key:消息索引字段
- maxNum:本次最多取出消息条数
- begin:开始时间
- end:结束时间
-
根据主题与消息ID查找消息:MessageExt viewMessage(String topic, String msgId)
-
查找该主题下所有的消息队列:List fetchPublishMessageQueues(final String topic)
-
同步发送消息: SendResult send(final Message msg)
-
异步发送消息:void send(final Message msg, final SendCallback sendCallback)
-
单向消息发送(不在乎发送结果,消息发送出去该方法立即返回):void sendOneway(final Message msg)
DefaultMQProducer核心属性:
private String producerGroup; // 生产者所属组,消息服务器在回查事务状态时会随机选择该组中任何一个生产者发起事务回查请求
private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; // 默认topicKey
private volatile int defaultTopicQueueNums = 4; // 默认主题在每一个Broker队列数量
private int sendMsgTimeout = 3000; // 发送消息默认超时时间,默认3s
private int compressMsgBodyOverHowmuch = 1024 * 4; // 消息体超过该值则启用压缩,默认4K
private int retryTimesWhenSendFailed = 2; // 同步方式发送消息重试次数,默认为2,总共执行3次
private int retryTimesWhenSendAsyncFailed = 2; //异步方式发送消息重试次数,默认为2
// 消息重试时选择另外一个Broker时,是否不等待存储结果就返回,默认为false
private boolean retryAnotherBrokerWhenNotStoreOK = false;
private int maxMessageSize = 1024 * 1024 * 4; // 默认4M,允许消息发送的最大消息长度,最大为2^32-1
启动流程
Step1:检查producerGroup是否符合要求;并改变生产者的instanceName为进程ID
// DefaultMQProducerImpl#start
this.checkConfig();
if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
this.defaultMQProducer.changeInstanceNameToPID();
}
Step2:创建MQClientInstance实例。整个JVM实例中只存在一个MQClientManager实例,维护一个MQClientInstance缓存表,也就是同一个clientId只会创建一个MQClientInstance
// MQClientManager
private ConcurrentMap<String/* clientId */, MQClientInstance> factoryTable =
new ConcurrentHashMap<String, MQClientInstance>();
clientId为客户端IP+instance+(unitname)可选,如果同一台物理服务器部署两个应用程序,应用程序会由于clientId相同,造成混乱吗
- 为了避免这个问题,如果instance为默认值DEFAULT的话,RocketMQ会自动将instance设置为进程ID,这样避免了不同进程的相互影响,但同一个JVM的不同消费者和不同生产者 在启动时获取到的MQClientInstance实例都是同一个。
- MQClientInstance封装了RocketMQ网络处理API,是消息生产者(Producer)、消息消费者(Consumer)与NameServer、Broker打交道的网络通道
// DefaultMQProducerImpl#start
this.mQClientFactory = MQClientManager.getInstance().
getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
// MQClientManager#getOrCreateMQClientInstance
public MQClientInstance getOrCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) {
String clientId = clientConfig.buildMQClientId();
MQClientInstance instance = this.factoryTable.get(clientId);
if (null == instance) {
instance =
new MQClientInstance(clientConfig.cloneClientConfig(),
this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook);
// 相当于原子执行:if (!map.containsKey(key)) return map.put(key, value); else return map.get(key);
// 返回 null 表示factoryTable不存在该KV
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;
}
Step3:向MQClientInstance注册,将当前生产者加入到MQClientInstance管理中,方便后续调用网络请求、进行心跳检测等。
// DefaultMQProducerImpl#start
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);
}
Step4:启动MQClientInstance,如果MQClientInstance已经启动,则本次启动不会真正执行。
2.3消息发送基本流程
消息发送流程的主要步骤:验证消息、查找路由、消息发送(包含异常处理机制)
默认消息发送以同步方式发送,默认超时时间为3s
1.消息长度验证:
首先确保生产者处于运行状态,然后验证消息是否符合相应的规范:主题名称、消息体不能为空、消息长度不能等于0且默认不能超过允许发送消息的最大长度4M(DefaultMQProducer#maxMessageSize = 1024 * 1024 * 4)
2.查找主题路由信息:
DefaultMQProducerImpl#tryToFindTopicPublishInfo是查找主题的路由信息的方法。如果生产者中缓存了topic的路由信息或者该路由信息包含了消息队列,则直接返回该路由信息;如果没有缓存或没有包含消息队列,则向NameServer查询该topic的路由信息。若仍未找到则抛异常。
-
这里完成对消息队列的排序
// MQClientInstance#topicRouteData2TopicPublishInfo List<QueueData> qds = route.getQueueDatas(); Collections.sort(qds); // QueueData类重写了compareTo方法,按照消息队列的BrokerName进行排序
// DefaultMQProducerImpl,缓存主题路由信息
private final ConcurrentMap<String/* topic */, TopicPublishInfo> topicPublishInfoTable =
new ConcurrentHashMap<String, TopicPublishInfo>();
- orderTopc:是否是顺序消息
- List messageQueueList:该主题队列的消息队列
- volatile ThreadLocalIndex sendWhichQueue:每选择一次消息队列,该值会自增1,用于选择消息队列
- List queueDatas:topic队列元数据
- List brokerDatas:topic分布的broker元数据
- HashMap<String/* brokerAddr /, List/ Filter Server */> filterServerTable:broker上过滤服务器地址列表
3.选择消息队列:
根据路由消息选择消息队列,返回的消息队列按照broker、序号排序。
-
首先消息发送端采用重试机制,由retryTimesWhenSendFailed指定同步方式重试次数;异步重试机制在收到消息发送结构后执行回调之前进行重试,由retryTimesWhenSendAsyncFailed指定,接下来就是循环执行,选择消息队列发送消息,发送成功则返回,收到异常则重试。选择消息队列有两种方式:
-
sendLatencyFaultEnable=false,不启用Broker故障延迟机制
-
// TopicPublishInfo#selectOneMessageQueue public MessageQueue selectOneMessageQueue(final String lastBrokerName) { if (lastBrokerName == null) { // lastBrokerName是上次选择的执行发送消息失败的Broker return selectOneMessageQueue(); } else { // 如果上次消息发送失败 for (int i = 0; i < this.messageQueueList.size(); i++) { int index = this.sendWhichQueue.incrementAndGet(); int pos = Math.abs(index) % this.messageQueueList.size(); if (pos < 0) pos = 0; MessageQueue mq = this.messageQueueList.get(pos); if (!mq.getBrokerName().equals(lastBrokerName)) { // 规避上次失败的Broker return mq; } } return selectOneMessageQueue(); } } public MessageQueue selectOneMessageQueue() { // 每次选择消息队列时都会自增1 int index = this.sendWhichQueue.incrementAndGet(); int pos = Math.abs(index) % this.messageQueueList.size(); if (pos < 0) pos = 0; return this.messageQueueList.get(pos); }当Broker宕机后,由于路由算法的消息队列是按Broker排序的,如果上一次根据路由算法选择的宕机的Broker的第一个队列,那么下一次选择也是宕机Broker的第二个队列,消息会发送失败,再次引发消息重试,带来不必要的性能损耗,所以需要一种新的方法将宕机的Broker排除在消息队列选择范围之外
-
-
sendLatencyFaultEnable=true,默认启用Broker故障延迟机制
延迟机制接口:private final LatencyFaultTolerance latencyFaultTolerance = new LatencyFaultToleranceImpl();
-
// MQFaultStrategy#selectOneMessageQueue public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) { if (this.sendLatencyFaultEnable) { try { int index = tpInfo.getSendWhichQueue().incrementAndGet(); // 对主题路由信息进行遍历获得消息队列 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); // 验证该消息队列是否可用,重要! //如果可用,移除latencyFaultTolerance关于该topic条目,表示该Broker故障已经恢复 if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) return mq; } // 如果当前所有的消息队列都被规避 // 尝试从规避的Broker中选择一个可用的Broker,如果找不到则返回null final String notBestBroker = latencyFaultTolerance.pickOneAtLeast(); // 根据Broker拿到写队列 int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker); if (writeQueueNums > 0) { final MessageQueue mq = tpInfo.selectOneMessageQueue(); if (notBestBroker != null) { mq.setBrokerName(notBestBroker); mq.setQueueId(tpInfo.getSendWhichQueue().incrementAndGet() % 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); } -
// MQFaultStrategy:消息失败策略,延迟实现的门面类 private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L}; private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L}; // DefaultMQProducerImpl#sendDefaultImpl:消息发送的主类,选择消息队列也是在这个方法中调用 beginTimestampPrev = System.currentTimeMillis(); sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime); endTimestamp = System.currentTimeMillis(); // 发送过程中抛出了异常,才会调用updateFaultItem方法进行Broker规避 // updateFaultItem方法参数:(broker名称,本次消息发送延迟时间currentLatency,isolation) // isolation:如果为true表示使用默认时长30s计算Broker规避时间;如果为false表示使用currentLatency作为 this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false); //MQFaultStrategy#updateFaultItem:计算规避时长 public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) { if (this.sendLatencyFaultEnable) { long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency); // LatencyFaultToleranceImpl#updateFaultItem: // 从缓存表中获取FaultItem,如果找到则更新,否则创建FaultItem /* FaultItem类中属性: private final String name; // broker名称 private volatile long currentLatency; // 本次消息发送延迟时间 private volatile long startTimestamp; // 当前时间+需要规避时长 */ this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration); } } private long computeNotAvailableDuration(final long currentLatency) { // 对latencyMax数组倒序查找,找到latencyMax第一个小于currentLatency的索引下标 // 该索引下标指向的notAvailableDuration的值即为需要规避的时长 for (int i = latencyMax.length - 1; i >= 0; i--) { if (currentLatency >= latencyMax[i]) return this.notAvailableDuration[i]; } return 0; }
-
-
4.消息发送:
消息发送API核心入口:DefaultMQProducerImpl#sendKernelImpl
private SendResult sendKernelImpl(final Message msg, // 待发送消息
final MessageQueue mq, // 消息将发送到该消息队列上
final CommunicationMode communicationMode, // 消息发送模式(同步、异步、单向)
final SendCallback sendCallback, // 异步消息回调函数
final TopicPublishInfo topicPublishInfo, // 主题路由信息
final long timeout) // 消息发送超时时间
- Step1:根据MessageQueue中的BrokerName从brokerAddrTable中获取Broker的网络地址
String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
if (null == brokerAddr) {
tryToFindTopicPublishInfo(mq.getTopic());
brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
}
- Step2:为消息分配全局唯一ID
//for MessageBatch,ID has been set in the generating process
if (!(msg instanceof MessageBatch)) {
MessageClientIDSetter.setUniqID(msg);
}
int sysFlag = 0;
boolean msgBodyCompressed = false;
// 如果消息体默认超过4K,会对消息体采用zip压缩,并设置消息的系统标记为 MessageSysFlag.COMPRESSED_FLAG
if (this.tryToCompressMessage(msg)) {
sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
sysFlag |= compressType.getCompressionFlag();
msgBodyCompressed = true;
}
// 如果是事务Prepared消息,则设置消息的系统标记为 MessageSysFlag.TRANSACTION_PREPARED_TYPE
final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (Boolean.parseBoolean(tranMsg)) {
sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;
}
- Step3:如果注册了钩子函数,则执行消息发送之前的增强逻辑
// 这段代码的目的是让用户有机会自定义校验禁用的逻辑。
// 通过自定义钩子函数,用户可以在发送消息前校验一些条件,例如是否禁止发送某种类型的消息,或者是否禁止向某些特定的 Broker 发送消息等。用户可以根据自己的需求,在 executeCheckForbiddenHook() 方法中实现自定义的校验逻辑。
if (hasCheckForbiddenHook()){...}
// 对消息发送进行增强
if (this.hasSendMessageHook()){...}
- Step4:构建消息发送请求包
SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup()); // 设置生产者组
requestHeader.setTopic(msg.getTopic()); // 设置主题名称
requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey()); // 设置默认主题Key
// 设置该主题在单个Broker默认队列数:4个
requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums());
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); // 设置是否是批量消息
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);
}
}
- Step5:根据消息发送方式,同步、异步、单向方式进行网络传输
// MQClientAPIImpl#sendMessage
public SendResult sendMessage(
final String addr,
final String brokerName,
final Message msg,
final SendMessageRequestHeader requestHeader,
final long timeoutMillis,
final CommunicationMode communicationMode, // 消息发送方式(同步、异步、单向)
final SendCallback sendCallback,
final TopicPublishInfo topicPublishInfo,
final MQClientInstance instance,
final int retryTimesWhenSendFailed,
final SendMessageContext context,
final DefaultMQProducerImpl producer
) throws RemotingException, MQBrokerException, InterruptedException {
long beginStartTime = System.currentTimeMillis();
RemotingCommand request = null;
String msgType = msg.getProperty(MessageConst.PROPERTY_MESSAGE_TYPE);
boolean isReply = msgType != null && msgType.equals(MixAll.REPLY_MESSAGE_FLAG);
if (isReply) {
if (sendSmartMsg) {
SendMessageRequestHeaderV2 requestHeaderV2 = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader);
request = RemotingCommand.createRequestCommand(RequestCode.SEND_REPLY_MESSAGE_V2, requestHeaderV2);
} else {
request = RemotingCommand.createRequestCommand(RequestCode.SEND_REPLY_MESSAGE, requestHeader);
}
} else {
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);
}
}
request.setBody(msg.getBody());
switch (communicationMode) {
case ONEWAY:
this.remotingClient.invokeOneway(addr, request, timeoutMillis);
return null;
case ASYNC:
final AtomicInteger times = new AtomicInteger();
long costTimeAsync = System.currentTimeMillis() - beginStartTime;
if (timeoutMillis < costTimeAsync) {
throw new RemotingTooMuchRequestException("sendMessage call timeout");
}
this.sendMessageAsync(addr, brokerName, msg, timeoutMillis - costTimeAsync, request, sendCallback, topicPublishInfo, instance,
retryTimesWhenSendFailed, times, context, producer);
return null;
case SYNC:
long costTimeSync = System.currentTimeMillis() - beginStartTime;
if (timeoutMillis < costTimeSync) {
throw new RemotingTooMuchRequestException("sendMessage call timeout");
}
return this.sendMessageSync(addr, brokerName, msg, timeoutMillis - costTimeSync, request);
default:
assert false;
break;
}
return null;
}
- Step6:如果注册了钩子函数,则执行消息发送之后的增强逻辑(即使消息发送过程抛出异常该方法也会执行)
// DefaultMQProducerImpl#sendKernelImpl
if (this.hasSendMessageHook()) {
context.setSendResult(sendResult);
this.executeSendMessageHookAfter(context);
}
同步发送
- 首先会检查消息发送是否合理 :AbstractSendMessageProcessor#msgCheck()
- 如果消息重试次数超过允许的最大重试次数,消息将进入到DLQ延迟队列,延迟队列主题:%DLQ%+消费组名
- 调用DefaultMessageStore#putMessage进行消息存储
异步发送
- 消息生产者调用发送的API后,无须等待消息服务器返回本次消息发送结果,只需提供一个回调函数
- 相比于同步发送性能显著提高,但为了保护消息服务器的负载压力,RocketMQ对消息的异步进行了并发控制,通过参数clientAsyncSemaphoreValue来控制,默认为65535
- 异步消息虽然也可以通过DefaultMQProducer#retryTimesWhenSendAsyncFailed属性控制消息重试次数,但重试的调用入口是在收到服务端响应包时进行的,如果出现网络异常、网络超时等将不会重试(注意!)
单向发送
- 消息生产者调用消息发送的API后,无须等待消息服务器返回本次消息发送结果,并且无须提供回调函数,不关心消息发送是否成功
- 没有重试机制
5.批量消息发送
批量消息发送是将同一主题的多条消息一起打包发送到消息服务端,减少网络调用次数,提高网络传输效率。
批量消息发送要解决的问题是如何将消息编码以便服务端能正确解码出每条消息的消息内容
- code:请求命令编码,请求命令类型
- version:版本号
- opaque:客户端请求序号
- flag:标记。倒数第一位表示请求类型,0:请求;1:返回。倒数第二位,1:表示oneway
- remark:描述
- extFields:扩展属性
- customeHeader:每个请求对应的请求头信息
- body:消息体内容,被transient关键字修饰
批量消息发送,需要将多条消息体的内容存储在body中,RocketMQ采取的方式是对单条消息内容使用固定格式存储。
1.首先会在消息发送端调用batch()方法,将一批消息封装成MessageBatch对象
// DefaultMQProducer#send
@Override
public SendResult send(Collection<Message> msgs) {
return this.defaultMQProducerImpl.send(batch(msgs));
}
// MessageBatch对象继承自Message对象,内部持有 private final List<Message> messages;
// 将该集合中的每条消息的消息体body集合成一个body[]数组
private MessageBatch batch(Collection<Message> msgs) throws MQClientException {
MessageBatch msgBatch;
try {
msgBatch = MessageBatch.generateFromList(msgs);
for (Message message : msgBatch) {
Validators.checkMessage(message, this);
MessageClientIDSetter.setUniqID(message);
message.setTopic(withNamespace(message.getTopic()));
}
msgBatch.setBody(msgBatch.encode());
} catch (Exception e) {
throw new MQClientException("Failed to initiate the MessageBatch", e);
}
msgBatch.setTopic(withNamespace(msgBatch.getTopic()));
return msgBatch;
}
2.在创建RemotingCommand对象时将调用messageBatch#endode()方法进行编码填充到RemotingCommand的body域中
3.消息发送端按照编码结构进行解码,然后进行消息发送