接上文的集群模式,监听器返回RECONSUME_LATER,需要将将这些消息发送给Broker延迟消息。如果发送ack消息失败,将延迟5s后提交线程池进行消费。
入口:ConsumeMessageConcurrentlyService#sendMessageBack
命令编码:RequestCode.CONSUMER_SEND_MSG_BACK;
MQClientAPIImpl#consumerSendMessageBack:
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); requestHeader.setOriginTopic(msg.getTopic()); 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()); }
public class ConsumerSendMsgBackRequestHeader implements CommandCustomHeader { @CFNotNull private Long offset;//消息物理偏移量 @CFNotNull private String group;//消费组名 @CFNotNull private Integer delayLevel;//延迟级别,mq不支持精确的定时消息调度,而是提供几个延时级别 private String originMsgId;//消息ID private String originTopic;//消息主题 @CFNullable private boolean unitMode = false; private Integer maxReconsumeTimes;//最大重新消费次数,默认为16秒
服务端处理:
SendMessageProcessor#consumerSendMsgBack
获取消费组的订阅配置信息,如果配置信息为空返回配置组信息不存在
public class SubscriptionGroupConfig { private String groupName;//消费组名 private boolean consumeEnable = true;//是否可以消费 private boolean consumeFromMinEnable = true;//是否允许从队列最小偏移量开始消费 private boolean consumeBroadcastEnable = true;//能否以广播模式消费,若为false只能以集群模式消费 private int retryQueueNums = 1;//重试队列个数,默认为1,每一个Broker上一个重试队列 private int retryMaxTimes = 16;消息最大重试次数 private long brokerId = MixAll.MASTER_ID;//masterId private long whichBrokerWhenConsumeSlowly = 1;//如果消息堵塞,将转向改brokerId的服务器上拉取消息默认1 private boolean notifyConsumerIdsChangedEnable = true;//当消息发送变化时是否立即进行消息队列重新负载。
消费组订阅信息配置信息存储在Broker的${ROCKET_HOME}/store/config/subscription/Group.json。默认情况下BrokerConfig.autoCreateSubscriptionGroup默认为true,表示在第一次使用消费组配置信息时如果不存在,则使用上述默认值自动创建一个,如果为false,则只能通过客户端命令mqadmin updateSubGrouop创建后修改相关参数。
创建重试主题,重试主题名称:%RETRY%+消费组名称,并从重试队列汇总随机选择一个队列,并构建TopicConfig主题配置信息。
根据消息物理偏移量从commitlog文件中获取消息,同时将消息的主题存入属性中。
设置消息重试次数,如果消息已重试次数超过maxReconsumeTimes,再次改变newTopic主体为DLQ(%DLQ%),该主题的权限为只写,说明消息一旦进入到DLQ队列中,RocketMQ将不负责再次调度进行消费了需要人工干预。
根据原先的消息创建一个新的消息对象,重试消息会拥有自己的唯一消息ID并存入commitlog文件中,并不会去更新原先消息,而是会将原先的主题,消息ID存入消息的属性中,主题名称为重试主题,其他属性与原先消息保持相同。
消息持久化。
这里再重新回顾一下delayTimeLevel>0的情况:
在存入commitlog文件之前,如果消息的延迟级别delayTimeLevel大于0,替换消息的主题与队列为定时任务主题"SCHEDULE_TOPIC_XXXX",队列ID为延迟级别减1。再次将消息主题,队列存入消息的属性中。
ack消息存入CommitLog文件后,将依托RocketMQ定时消息机制在延迟时间到期后再次将消息拉取,提交消费线程池。ACK消息是同步发送的,如果在发送过程中出现错误,将记录所有发送ACK消息失败的消息,然后再次封装成ConsumeRequest,延迟5s执行。