推荐关注公众号: sharedCode, 在这里可以直接联系我哦。
前言
上文中我们介绍的客户端普通消息和顺序消息的原理,在消息发送失败的时候会有一个重试的过程,接下来我们来看下消息重试的源码是什么样子的,以及重试到了最后是在什么情况下进入死信队列的
sendMessageBack
public void sendMessageBack(MessageExt msg, int delayLevel, final String brokerName)
throws RemotingException, MQBrokerException, InterruptedException, MQClientException {
try {
// 获取broker地址
String brokerAddr = (null != brokerName) ? this.mQClientFactory.findBrokerAddressInPublish(brokerName)
: RemotingHelper.parseSocketAddressAddr(msg.getStoreHost());
// 发起请求
this.mQClientFactory.getMQClientAPIImpl().consumerSendMessageBack(brokerAddr, msg,
this.defaultMQPushConsumer.getConsumerGroup(), delayLevel, 5000, getMaxReconsumeTimes());
} catch (Exception e) {
log.error("sendMessageBack Exception, " + this.defaultMQPushConsumer.getConsumerGroup(), e);
// 当发送失败的时候,继续重试。
Message newMsg = new Message(MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup()), msg.getBody());
String originMsgId = MessageAccessor.getOriginMessageId(msg);
MessageAccessor.setOriginMessageId(newMsg, UtilAll.isBlank(originMsgId) ? msg.getMsgId() : originMsgId);
newMsg.setFlag(msg.getFlag());
MessageAccessor.setProperties(newMsg, msg.getProperties());
MessageAccessor.putProperty(newMsg, MessageConst.PROPERTY_RETRY_TOPIC, msg.getTopic());
MessageAccessor.setReconsumeTime(newMsg, String.valueOf(msg.getReconsumeTimes() + 1));
MessageAccessor.setMaxReconsumeTimes(newMsg, String.valueOf(getMaxReconsumeTimes()));
// 重置延迟级别
newMsg.setDelayTimeLevel(3 + msg.getReconsumeTimes());
// 调用消息发送接口进行发送
this.mQClientFactory.getDefaultMQProducer().send(newMsg);
} finally {
msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQPushConsumer.getNamespace()));
}
}
步骤说明:
- 获取borker地址,调用
mQClientFactory
进行发送 - 发送失败的时候,组装消息继续发送,这个原生的send接口默认有三次重试
最大重试次数
private int getMaxReconsumeTimes() {
// default reconsume times: 16
if (this.defaultMQPushConsumer.getMaxReconsumeTimes() == -1) {
return 16;
} else {
return this.defaultMQPushConsumer.getMaxReconsumeTimes();
}
}
发送重试消息的时候,最大重试次数是根据我们客户端配置的,默认配置是 -1 ,所以得到最大重试次数是 16
, 其实相当于重试16次之后默认会进入死信队列
, 当然客户端也可以自定义
public void consumerSendMessageBack(
final String addr,
final MessageExt msg,
final String consumerGroup,
final int delayLevel,
final long timeoutMillis,
final int maxConsumeRetryTimes
) throws RemotingException, MQBrokerException, InterruptedException {
ConsumerSendMsgBackRequestHeader requestHeader = new ConsumerSendMsgBackRequestHeader();
// 注意这里,构建请求的命令
RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONSUMER_SEND_MSG_BACK, requestHeader);
// 设置消费组
requestHeader.setGroup(consumerGroup);
// 设置topic
requestHeader.setOriginTopic(msg.getTopic());
// 设置当前需要重试消息的offset
requestHeader.setOffset(msg.getCommitLogOffset());
// 设置消息级别
requestHeader.setDelayLevel(delayLevel);
requestHeader.setOriginMsgId(msg.getMsgId());
//设置最高可重试的次数
requestHeader.setMaxReconsumeTimes(maxConsumeRetryTimes);
// 执行远程方法
RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr),
request, timeoutMillis);
assert response != null;
switch (response.getCode()) {
case ResponseCode.SUCCESS: {
return;
}
default:
break;
}
throw new MQBrokerException(response.getCode(), response.getRemark());
}
步骤说明:
- 构建请求的
RemotingCommand
,这个很重要,是我们如何找到broker代码的关键,这里有个RequestCode.CONSUMER_SEND_MSG_BACK
, 通过这个值我们可以找到broker的处理代码,因为在broker的原理里面,是通过RequestCode来进行switch选择的。 - 从上面可以看出来,整个的消息发送过去,是没有把实际的消息内容重新发送回去borker, 而是通过旧消息的
offset
的去broker上面去寻找消息的,这点在后面的broker的源码上会讲解。 - 执行远程的方法,发送命令给broker
Broker接收
SendMessageProcessor
@Override
public RemotingCommand processRequest(ChannelHandlerContext ctx,
RemotingCommand request) throws RemotingCommandException {
SendMessageContext mqtraceContext;
switch (request.getCode()) {
//重试消息
case RequestCode.CONSUMER_SEND_MSG_BACK:
// 执行重试消息
return this.consumerSendMsgBack(ctx, request);
default:
// 这里是普通的消息,普通的消息处理在这边,这个后续会单独开文讲解
SendMessageRequestHeader requestHeader = parseRequestHeader(request);
if (requestHeader == null) {
return null;
}
System.out.println(JSON.toJSONString(requestHeader));
mqtraceContext = buildMsgContext(ctx, requestHeader);
this.executeSendMessageHookBefore(ctx, request, mqtraceContext);
RemotingCommand response;
// 是否批量发送
if (requestHeader.isBatch()) {
// 批量发送
response = this.sendBatchMessage(ctx, request, mqtraceContext, requestHeader);
} else {
// 单个发送
response = this.sendMessage(ctx, request, mqtraceContext, requestHeader);
}
this.executeSendMessageHookAfter(response, mqtraceContext);
return response;
}
}
说明:
从上面我们可以看到,这个是broker的消息处理,switch就两个选择,一个是重试消息的,一个是正常消息的处理,正常消息的处理后面我们会单独开文讲解,本文仅讲解重试消息
consumerSendMsgBack
private RemotingCommand consumerSendMsgBack(final ChannelHandlerContext ctx, final RemotingCommand request)
throws RemotingCommandException {
// 构建响应的Response
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
// 获取请求header
final ConsumerSendMsgBackRequestHeader requestHeader =
(ConsumerSendMsgBackRequestHeader)request.decodeCommandCustomHeader(ConsumerSendMsgBackRequestHeader.class);
// 去除%DLQ%(死信) ,%RETRY%"(重试) 这两种消息消费组的前缀
String namespace = NamespaceUtil.getNamespaceFromResource(requestHeader.getGroup());
// 执行hook接口
if (this.hasConsumeMessageHook() && !UtilAll.isBlank(requestHeader.getOriginMsgId())) {
ConsumeMessageContext context = new ConsumeMessageContext();
context.setNamespace(namespace);
context.setConsumerGroup(requestHeader.getGroup());
context.setTopic(requestHeader.getOriginTopic());
context.setCommercialRcvStats(BrokerStatsManager.StatsType.SEND_BACK);
context.setCommercialRcvTimes(1);
context.setCommercialOwner(request.getExtFields().get(BrokerStatsManager.COMMERCIAL_OWNER));
this.executeConsumeMessageHookAfter(context);
}
// 根据消费组名称,获取订阅组配置
SubscriptionGroupConfig subscriptionGroupConfig =
this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(requestHeader.getGroup());
if (null == subscriptionGroupConfig) {
// 订阅组不存在
response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST);
response.setRemark("subscription group not exist, " + requestHeader.getGroup() + " "
+ FAQUrl.suggestTodo(FAQUrl.SUBSCRIPTION_GROUP_NOT_EXIST));
return response;
}
// 判断broker是否有写的全新,
if (!PermName.isWriteable(this.brokerController.getBrokerConfig().getBrokerPermission())) {
response.setCode(ResponseCode.NO_PERMISSION);
response.setRemark("the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1() + "] sending message is forbidden");
return response;
}
// 消费组的重试队列数量为0
if (subscriptionGroupConfig.getRetryQueueNums() <= 0) {
response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return response;
}
// 发送消息的TOPIC, 重新设置TOPIC,前面加上 RETRY
String newTopic = MixAll.getRetryTopic(requestHeader.getGroup());
// 随机重试队列
int queueIdInt = Math.abs(this.random.nextInt() % 99999999) % subscriptionGroupConfig.getRetryQueueNums();
int topicSysFlag = 0;
if (requestHeader.isUnitMode()) {
topicSysFlag = TopicSysFlag.buildSysFlag(false, true);
}
// 负责获取并创建topicConfig (不存在就创建)
TopicConfig topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(
newTopic,
subscriptionGroupConfig.getRetryQueueNums(),
PermName.PERM_WRITE | PermName.PERM_READ, topicSysFlag);
if (null == topicConfig) {
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("topic[" + newTopic + "] not exist");
return response;
}
if (!PermName.isWriteable(topicConfig.getPerm())) {
response.setCode(ResponseCode.NO_PERMISSION);
response.setRemark(String.format("the topic[%s] sending message is forbidden", newTopic));
return response;
}
// 通过原消息的offset获取消息实际的内容。
MessageExt msgExt = this.brokerController.getMessageStore().lookMessageByOffset(requestHeader.getOffset());
if (null == msgExt) {
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("look message by offset failed, " + requestHeader.getOffset());
return response;
}
// 设置重试TOPIC
final String retryTopic = msgExt.getProperty(MessageConst.PROPERTY_RETRY_TOPIC);
if (null == retryTopic) {
MessageAccessor.putProperty(msgExt, MessageConst.PROPERTY_RETRY_TOPIC, msgExt.getTopic());
}
msgExt.setWaitStoreMsgOK(false);
int delayLevel = requestHeader.getDelayLevel();
// 获取最大消息重试次数
int maxReconsumeTimes = subscriptionGroupConfig.getRetryMaxTimes();
if (request.getVersion() >= MQVersion.Version.V3_4_9.ordinal()) {
// RocketMq 3.4.9之后以客户端的配置为准
maxReconsumeTimes = requestHeader.getMaxReconsumeTimes();
}
// 当消息的重试次数大于 规定的次数, 或者重试等级小于0
if (msgExt.getReconsumeTimes() >= maxReconsumeTimes
|| delayLevel < 0) {
// 设置新的TOPIC, %DLQ%+ 实际的消费组
newTopic = MixAll.getDLQTopic(requestHeader.getGroup());
// 随机队列
queueIdInt = Math.abs(this.random.nextInt() % 99999999) % DLQ_NUMS_PER_GROUP;
//
topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(newTopic,
DLQ_NUMS_PER_GROUP,
PermName.PERM_WRITE, 0
);
if (null == topicConfig) {
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("topic[" + newTopic + "] not exist");
return response;
}
} else {
if (0 == delayLevel) {
delayLevel = 3 + msgExt.getReconsumeTimes();
}
msgExt.setDelayTimeLevel(delayLevel);
}
// 发送消息
MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
msgInner.setTopic(newTopic);
msgInner.setBody(msgExt.getBody());
msgInner.setFlag(msgExt.getFlag());
MessageAccessor.setProperties(msgInner, msgExt.getProperties());
msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties()));
msgInner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(null, msgExt.getTags()));
msgInner.setQueueId(queueIdInt);
msgInner.setSysFlag(msgExt.getSysFlag());
msgInner.setBornTimestamp(msgExt.getBornTimestamp());
msgInner.setBornHost(msgExt.getBornHost());
msgInner.setStoreHost(this.getStoreHost());
msgInner.setReconsumeTimes(msgExt.getReconsumeTimes() + 1);
String originMsgId = MessageAccessor.getOriginMessageId(msgExt);
MessageAccessor.setOriginMessageId(msgInner, UtilAll.isBlank(originMsgId) ? msgExt.getMsgId() : originMsgId);
// 消息持久化,持久化这一块的代码后续我们会继续讲解
PutMessageResult putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner);
if (putMessageResult != null) {
switch (putMessageResult.getPutMessageStatus()) {
case PUT_OK:
// 发送完成
String backTopic = msgExt.getTopic();
String correctTopic = msgExt.getProperty(MessageConst.PROPERTY_RETRY_TOPIC);
if (correctTopic != null) {
backTopic = correctTopic;
}
// 统计
this.brokerController.getBrokerStatsManager().incSendBackNums(requestHeader.getGroup(), backTopic);
response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return response;
default:
break;
}
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark(putMessageResult.getPutMessageStatus().name());
return response;
}
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("putMessageResult is null");
return response;
}
步骤说明:
-
获取消费组,重试队列等准备信息,这个不做过多说明,此处不多说
-
判断broker的写权限,这个在broker端的配置文件中有配置,
brokerPermission
6 : 同时支持读写 4 : 禁写 2 禁读 -
设置重试消息的TOPIC, 重试队列名称为:
%RETRY%+consumergroup
, 负责获取并创建topicConfig (不存在就创建) -
topic是否支持写,这个都会做好判断
这里跟broker一样,6 : 同时支持读写 4 : 禁写 2 禁读 -
通过原消息的offset获取消息内容,这个在客户端发送的时候说过,这样做可以极大的减少消息传输量
-
判断重试次数,
RocketMq3.4.9
版本之后的最大重试次数以客户端的为主 , 如果当前消息的重试次数大于最大重试次数,那么就开始走死信队列。 跟重试消息一样,也是设置死信队列的TOPIC%DLQ%+ 实际的消费组
, -
以上步骤走完之后,需要走重试topic还是走死信topic这个已经确定好了,
-
接下来就是走发送消息的流程了。
这里需要注意的是,死信队列的perm
设置是 0x1 << 1
也就是 2 , 2 的意思就是说死信队列可写不可读
因此死信队列在控制台上查询,默认是查询不到的
如果需要查询的话,那么需要在topic的配置里面对topic 的权限进行修改一下,就可以查看到了
推荐关注公众号: sharedCode, 在这里可以直接联系我哦。