1. DefaultMQPushConsumer类主要内容
public class DefaultMQPushConsumer extends ClientConfig implements MQPushConsumer {
// 消费者实现对象
protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;
// 消费者组名
private String consumerGroup;
// 消费模式:默认集群模式,还支持 广播模式
private MessageModel messageModel = MessageModel.CLUSTERING;
// 当从broker获取当前组内 该 queue 的offset 不存在时,consumeFromWhere 才有效
// 默认:CONSUME_FROM_LAST_OFFSET 表示从队列的最后offset开始消费,当队列内再有一条新的msg加入进去时,消费者才会去消息
private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET;
// 当ConsumeFromWhere配置的是CONSUME_FROM_TIMESTAMP 的时候,并且服务器中不存在 group 内该queue 的offset 时,会使用该时间戳
// 值:消费者创建时间 - 30秒 转换成 格式: 年月日小时分钟秒 (20131223171201)
private String consumeTimestamp = UtilAll.timeMillisToHumanString3(System.currentTimeMillis() - (1000 * 60 * 30));
// 主题下的队列分配策略,rbl对象依赖该算法。
private AllocateMessageQueueStrategy allocateMessageQueueStrategy;
// 订阅信息集合,key :主题 val:过滤表达式 (一般都是tag)
private Map<String /* topic */, String /* sub expression */> subscription = new HashMap<String, String>();
// 消息监听器,消息处理逻辑 全部由它提供(具体怎么消费)..
// 两种接口:1. MessageListenerConcurrently 2. MessageListenerOrderly
private MessageListener messageListener;
// 消费者本地消费进度存储(一般这里都是 RemoteBrokerOffsetStore)
private OffsetStore offsetStore;
// 消费服务线程池 线程数最小值,默认:20
private int consumeThreadMin = 20;
// 消费服务线程池 线程数最大值,默认:20
private int consumeThreadMax = 20;
// 该参数已经作废了..
private long adjustThreadPoolNumsThreshold = 100000;
// 本地队列快照(processQueue)中,第一条消息 和 最后一条消息 它俩之间的offset跨度 不能超过 2000 (队列流控使用)
private int consumeConcurrentlyMaxSpan = 2000;
// 本地队列快照(processQueue)中,消息数量限制 (队列流控使用)
private int pullThresholdForQueue = 1000;
// 本地队列快照(processQueue)中,消息总size 不能超过 “100”mib (队列流控使用)
private int pullThresholdSizeForQueue = 100;
// 主题流控,消费者消费的指定主题的所有消息 在本地 不能超过 “pullThresholdForTopic” 值。 -1 表示不限制
private int pullThresholdForTopic = -1;
// 主题流控,消费者消费的指定主题的所有消息 在本地 不能超过 “pullThresholdSizeForTopic”mib大小。 -1 表示不限制
private int pullThresholdSizeForTopic = -1;
// 消费任务最多可消费的消息数量(默认是:1,每一个消费任务 只消费一条消息)
private int consumeMessageBatchMaxSize = 1;
// 一次拉请求,最多可以从服务器拉取的消息数量
private int pullBatchSize = 32;
// 拉请求时,是否提交本地“订阅数据(过滤信息)”,默认不提交(心跳时,订阅数据已经同步到broker,没必要再提交了)
private boolean postSubscriptionWhenPull = false;
private boolean unitMode = false;
// 消息最大重试次数, (-1)
private int maxReconsumeTimes = -1;
// 消费者本地如果某条消息 “15” min 还未被消费,则需要回退该消息
private long consumeTimeout = 15;
}
1.1 ConsumerFromWhere
public enum ConsumeFromWhere {
//默认
CONSUME_FROM_LAST_OFFSET,
// 从有效offset的开始 进行消费
CONSUME_FROM_FIRST_OFFSET,
// 从指定时间戳的消息开始消费(距离该时间戳最近的一条消息 offset 开始)
CONSUME_FROM_TIMESTAMP,
}
2 DefaultMQPushConsumerImpl
2.1 消费者实现类字段
public class DefaultMQPushConsumerImpl implements MQConsumerInner {// 门面对象(config)
private final DefaultMQPushConsumer defaultMQPushConsumer;
// rbl对象,职责:分配订阅主题的队列给当前消费者,20秒钟一个周期执行rbl算法(客户端实例触发)
private final RebalanceImpl rebalanceImpl = new RebalancePushImpl(this);
// 过滤消息hook
private final ArrayList<FilterMessageHook> filterMessageHookList = new ArrayList<FilterMessageHook>();
// 消费者启动时间
private final long consumerStartTimestamp = System.currentTimeMillis();
// 消息执行hook,在消息处理前 和 处理后 分别执行 hook.before hook.after 系列方法 ,留给用户扩展的接口。
private final ArrayList<ConsumeMessageHook> consumeMessageHookList = new ArrayList<ConsumeMessageHook>();
// ...
private final RPCHook rpcHook;
// 消费者状态
private volatile ServiceState serviceState = ServiceState.CREATE_JUST;
// 客户端实例(整个进程内只有一个客户端实例对象)
private MQClientInstance mQClientFactory;
// 拉消息API封装(这里为什么要封装? 内部有推荐下次pull时 主机的算法,服务器broker 返回结果中,包装着 下次 pull 推荐的brokerId,
// 根据本次请求数据 冷热 来进行推荐。)
private PullAPIWrapper pullAPIWrapper;
// 是否暂停
private volatile boolean pause = false;
// 是否顺序消费
private boolean consumeOrderly = false;
// 消息监听器
private MessageListener messageListenerInner;
// 消息进度存储器
private OffsetStore offsetStore;
// 消费消息服务
// 1. ConsumeMessageConcurrentlyService
// 2. ConsumeMessageOrderlyService
private ConsumeMessageService consumeMessageService;
// 队列流控次数(打印日志使用..默认每1000次流控,进行一次日志打印)
private long queueFlowControlTimes = 0;
// 流控使用 (控制打印日志..)
private long queueMaxSpanFlowControlTimes = 0;
}
2.2 消费者启动类源码分 public synchronized void start() throws MQClientException { switch (this.serviceState) {
case CREATE_JUST:
log.info("the consumer [{}] start beginning. messageModel={}, isUnitMode={}", this.defaultMQPushConsumer.getConsumerGroup(),
this.defaultMQPushConsumer.getMessageModel(), this.defaultMQPushConsumer.isUnitMode());
// 首先将启动状态设置为“失败”,启动成功之后,立马就改为“启动成功”
this.serviceState = ServiceState.START_FAILED;
// 检查配置(很多细节,检查consumerGroup、messageModel等是否为null)
this.checkConfig();
// 拷贝“订阅信息”到“rbl对象”(负载均衡实现对象)
this.copySubscription();
if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
// 修改消费者 实例名称:pid
this.defaultMQPushConsumer.changeInstanceNameToPID();
}
// 获取客户端实例对象(进程内只有一个该实例)
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
// 初始化 rbl 对象
this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
// 将队列分配策略对象,赋值给rbl
this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);
// 创建 拉消息API 对象(内部封装了查询推荐主机算法)
this.pullAPIWrapper = new PullAPIWrapper(
mQClientFactory,
this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
// 将 “过滤hook列表”注册到 pullApi 内,消息拉取下来之后,会执行该hook,再进行一次自定义的过滤
this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
if (this.defaultMQPushConsumer.getOffsetStore() != null) {
this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
} else {
// 一般走这里
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
case CLUSTERING:// 只考虑“集群模式”
// 创建 offsetStore 实例
this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
default:
break;
}
// 将创建的 store 对象 回写 到 门面对象
this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
}
// 集群模式:load方法 什么也不干..广播模式下offset存储在消费者本地的,集群模式下,offset存储在broker上的,因此广播模式下load方法有内容
this.offsetStore.load();
if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
// 是否顺序消费 属性 设置为 true ,表示是顺序消费
this.consumeOrderly = true;
// 顺序消费的话,创建的消费服务,是顺序消费服务(ConsumeMessageOrderlyService)
this.consumeMessageService =
new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
} else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
// 是否顺序消费 属性 设置为 false,表示当前是 并发消费
this.consumeOrderly = false;
// 并发消费的话,创建的消费服务,是并发消费服务(ConsumeMessageConcurrentlyService)
this.consumeMessageService =
new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
}
// 启动消费服务(后面看相关源码)
this.consumeMessageService.start();
// 将消费者注册到客户端实例内
// 为什么注册?客户端实例给消费者提供了什么服务?
// 1.心跳服务(把订阅数据同步到关注主题的broker)
// 2.拉消息服务(内部PullMessageService 启动线程,基于 PullRequestQueue 工作,消费者负载均衡分配到队列后 会向该队列提交 pullRequest,后面细聊)
// 3.队列负载服务(每20秒,调用一次consumer.doRebalance() 接口)
// 4.消息进度持久化
// 5.动态调整消费者 消费服务线程池(ps:看源码得知,这块逻辑被注释掉了...)
boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
if (!registerOK) {
this.serviceState = ServiceState.CREATE_JUST;
this.consumeMessageService.shutdown(defaultMQPushConsumer.getAwaitTerminationMillisWhenShutdown());
throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
+ "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
null);
}
// 启动客户端实例
mQClientFactory.start();
log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
// 消费者状态设置为:RUNNING 运行中
this.serviceState = ServiceState.RUNNING;
break;
case RUNNING:
case START_FAILED:
case SHUTDOWN_ALREADY:
throw new MQClientException("The PushConsumer service state not OK, maybe started once, "
+ this.serviceState
+ FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
null);
default:
break;
}
// 从nameserver 强制加载 主题路由数据,生成 主题的 set<mq> 交给 rbl 的 table。遍历订阅的队列,遍历每一个主题和订阅数据,调用客户端实例的updateTopicRouteInfoFromNameServer方法。该方法首先到nameserver拉取最新的 路由数据,跟本地的路由数据做一个对比,
//本地如果没有或者路由发生变化会更新broker节点映射表,看topicRouteData主题都分布在哪几个主机上,将主题队列信息生成出来,每一个队列都会有一个MessageQueue对象,最终将队列主题信息交给了rebalanceImpl对象的topicSubscribeInfoTable(消费者订阅主题队列信息),就知道当前队列主题订阅了哪些信息,
//有了这些信息才能够用分配算法
this.updateTopicSubscribeInfoWhenSubscriptionChanged();
// 检查服务器是否支持“消息过滤模式”(一般咱们都是 tag 过滤,服务器默认支持),如果不支持,这里会抛异常
this.mQClientFactory.checkClientInBroker();
// 向所有已知的broker节点,发送心跳
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
// 唤醒rbl 线程,让rbl线程 立马去做 触发负载均衡的事情。
this.mQClientFactory.rebalanceImmediately();
}
2.2.1 拷贝“订阅信息”到“rbl对象”的方法源码学习
private void copySubscription() throws MQClientException {
try {
// 获取门面对象“Subscription”
Map<String, String> sub = this.defaultMQPushConsumer.getSubscription();
if (sub != null) {
for (final Map.Entry<String, String> entry : sub.entrySet()) {
final String topic = entry.getKey();
final String subString = entry.getValue();
SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPushConsumer.getConsumerGroup(),
topic, subString);
// 将创建的“订阅数据对象”加入到 “rbl对象的map内”
this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData);
}
}
if (null == this.messageListenerInner) {
// 将门面的messageListener 保存到 实现对象内
this.messageListenerInner = this.defaultMQPushConsumer.getMessageListener();
}
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
break;
case CLUSTERING:// 集群模式
// 获取当前“消费者组”的重试主题,规则:%RETRY%ConsumerGroup
final String retryTopic = MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup());
// 创建“%RETRY%ConsumerGroup”主题的 订阅数据对象
SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPushConsumer.getConsumerGroup(),
retryTopic, SubscriptionData.SUB_ALL);
// 将创建的“%RETRY%ConsumerGroup”主题 加入到 “rbl对象的map内”
// 这里为什么要订阅这样一个主题?
// 消息重试时,消息最终会再次加入到该主题,消费者订阅这个主题之后,就有机会再次拿到该“消息”,再次进行消费处理。
this.rebalanceImpl.getSubscriptionInner().put(retryTopic, subscriptionData);
break;
default:
break;
}
} catch (Exception e) {
throw new MQClientException("subscription exception", e);
}
}