我们通常使用的DefaultMQPushConsumer是通过推拉两种模式结合的长轮询机制去进行工作的,消费者通过这种机制对消息进行消费,既能保证主动权在客户端,又能保证数据的实时性。而什么是长轮询呢?长轮询的本质仍然是轮询,只不过又与普通的轮询不同,不同点在于:当服务端接收到客户端的请求后,服务端不会立即将数据返回给客户端,而是会先将这个请求hold住,判断服务器端数据是否有更新。如果有更新,则对客户端进行响应,如果一直没有数据,则它会在长轮询超时时间之前一直hold住请求并检测是否有数据更新,直到有数据或者超时后才返回。
broker端处理拉取消息请求
org.apache.rocketmq.broker.processor.PullMessageProcessor#processRequest(io.netty.channel.Channel, org.apache.rocketmq.remoting.protocol.RemotingCommand, boolean)
// 查询的队列还没有任何一条新消息
case ResponseCode.PULL_NOT_FOUND:
// 如果此时没有查询到新消息 并且 客户端请求的flag标识了允许开启长轮询,那么就去长轮询判断该队列对应的consumequeue目录中是否有新增的数据
if (brokerAllowSuspend && hasSuspendFlag) {
long pollingTimeMills = suspendTimeoutMillisLong;
// 如果客户端允许长轮询但是broker端没开启长轮询,那么长轮询的时间使用shortPollingTimeMills
if (!this.brokerController.getBrokerConfig().isLongPollingEnable()) {
pollingTimeMills = this.brokerController.getBrokerConfig().getShortPollingTimeMills();
}
String topic = requestHeader.getTopic();
long offset = requestHeader.getQueueOffset();
int queueId = requestHeader.getQueueId();
// 构建一个长轮询请求
PullRequest pullRequest = new PullRequest(request, channel, pollingTimeMills,
this.brokerController.getMessageStore().now(), offset, subscriptionData, messageFilter);
this.brokerController.getPullRequestHoldService().suspendPullRequest(topic, queueId, pullRequest);
// 这里response = null的作用?当消费者发起拉取消息请求发现没新消息的时候就会触发长轮询,broker端会判断response是否等于null去来进行是否需要发送响应请求给消费者
// 而这里response == null,broker是不会发送响应请求给消费者的,所以消费者端的异步回调也不会被调用,最终就会导致消费者在长轮询的这段时间中不会向broker再发起拉消息请求了
response = null;
break;
}
broker端收到消费端的拉取消息请求之后会从commitlog中查找到最新的消息结果,得到拉取结果之后就会来到一个switch...case分支中,在这个switch...case中会去根据拉取结果的状态进行判断,如果拉取的结果是该队列没有任何一条新数据就会来到上面所示的分支,在这个分支中会先去判断broker端和消费端是否允许开启长轮询,默认消费端发送过来的拉取消息请求都是允许开启长轮询的,然后就构造出一个长轮询请求对象PullRequest,再把这个PullRequest对象交给负责长轮询的组件PullRequestHoldService,最后响应对象response设置为null,这里为什么要设置为null呢?当response == null之后,broker端在处理完拉取消息的逻辑之后,就不会对消费者进行响应了,而当消费者那边收不到broker端的响应之后,就不会再继续发起下一次的拉消息请求,那么消费者就一直不会再发起拉取消息请求了吗?当然不是,具体我们继续往下看就知道了。
PullRequestHoldService执行长轮询
public void run() {
log.info("{} service started", this.getServiceName());
while (!this.isStopped()) {
try {
// 判断是否开启了长轮询,默认开启
if (this.brokerController.getBrokerConfig().isLongPollingEnable()) {
// 每一次长轮询时间间隔默认5s
this.waitForRunning(5 * 1000);
} else {
// 使用短轮询,时间默认为1s
this.waitForRunning(this.brokerController.getBrokerConfig().getShortPollingTimeMills());
}
long beginLockTimestamp = this.systemClock.now();
this.checkHoldRequest();
long costTime = this.systemClock.now() - beginLockTimestamp;
if (costTime > 5 * 1000) {
log.info("[NOTIFYME] check hold request cost {} ms.", costTime);
}
} catch (Throwable e) {
log.warn(this.getServiceName() + " service has exception. ", e);
}
}
log.info("{} service end", this.getServiceName());
}
判断是否开启了长轮询,长轮询每隔5s轮询一次,否则使用短轮询,每隔1s轮询一次,具体执行checkHoldRequest()方法进行轮询的核心逻辑
private void checkHoldRequest() {
for (String key : this.pullRequestTable.keySet()) {
String[] kArray = key.split(TOPIC_QUEUEID_SEPARATOR);
if (2 == kArray.length) {
String topic = kArray[0];
int queueId = Integer.parseInt(kArray[1]);
// 找到对应consumequeue目录的最大偏移位
final long offset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId);
try {
// 对目标consumequeue目录进行长轮询判断是否有新消息
this.notifyMessageArriving(topic, queueId, offset);
} catch (Throwable e) {
log.error("check hold request failed. topic={}, queueId={}", topic, queueId, e);
}
}
}
}
在该方法中会遍历pullRequestTable表,该表保存的key是topic@queueId,value是ManyPullRequest,上面构造的PullRequest对象就是放到了ManyPullRequest里面,所以pullRequestTable表就表示保存了需要长轮询的队列
public void notifyMessageArriving(final String topic, final int queueId, final long maxOffset, final Long tagsCode,
long msgStoreTime, byte[] filterBitMap, Map<String, String> properties) {
String key = this.buildKey(topic, queueId);
ManyPullRequest mpr = this.pullRequestTable.get(key);
if (mpr != null) {
List<PullRequest> requestList = mpr.cloneListAndClear();
if (requestList != null) {
List<PullRequest> replayList = new ArrayList<PullRequest>();
for (PullRequest request : requestList) {
long newestOffset = maxOffset;
// 调整开始查询的偏移位
if (newestOffset <= request.getPullFromThisOffset()) {
newestOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId);
}
// 条件成立: 说明有新消息
if (newestOffset > request.getPullFromThisOffset()) {
// 对消息进行过滤
boolean match = request.getMessageFilter().isMatchedByConsumeQueue(tagsCode,
new ConsumeQueueExt.CqExtUnit(tagsCode, msgStoreTime, filterBitMap));
// match by bit map, need eval again when properties is not null.
if (match && properties != null) {
match = request.getMessageFilter().isMatchedByCommitLog(null, properties);
}
// 如果过滤之后存在符合条件的消息,那么就再调用一次 消息查询的流程
if (match) {
try {
this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(),
request.getRequestCommand());
} catch (Throwable e) {
log.error("execute request when wakeup failed.", e);
}
continue;
}
}
// 代码执行到这里,说明此时还没有新消息
// 条件成立: 说明长轮询超时了
if (System.currentTimeMillis() >= (request.getSuspendTimestamp() + request.getTimeoutMillis())) {
try {
// 长轮询超时了之后会再调用一次拉取消息的方法(PullMessageProcessor#processRequest),
// 但是这一次调用就不会触发长轮询了,所以消费者就会收到不等于null的response响应,然后主动地再去向broker发起下一次拉消息请求
this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(),
request.getRequestCommand());
} catch (Throwable e) {
log.error("execute request when wakeup failed.", e);
}
continue;
}
// 代码执行到这里说明 此时还没有新消息并且长轮询还未超时
replayList.add(request);
}
if (!replayList.isEmpty()) {
// 把PullRequest放回ManyPullRequest中,下一次循环的时候会继续拿到这个PullRequest
mpr.addPullRequest(replayList);
}
}
}
}
在进行长轮询的过程中,会判断当前这个队列的consumequeue文件的最大消息偏移位是否有改变,如果有改变,说明有新的消息,然后再把这个新消息去进行匹配过滤,如果消息匹配就调用executeRequestWhenWakeup()方法,反之则会判断当前长轮询的时间是否已经超时了,如果超时了,也会调用一次executeRequestWhenWakeup()方法。也就是说只要退出长轮询了就会调用executeRequestWhenWakeup()方法
public void executeRequestWhenWakeup(final Channel channel,
final RemotingCommand request) throws RemotingCommandException {
Runnable run = new Runnable() {
@Override
public void run() {
try {
// 调用查询消息流程,注意的是,由长轮询调用的查询消息方法第三个参数传的是false
final RemotingCommand response = PullMessageProcessor.this.processRequest(channel, request, false);
if (response != null) {
response.setOpaque(request.getOpaque());
response.markResponseType();
try {
channel.writeAndFlush(response).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
log.error("processRequestWrapper response to {} failed",
future.channel().remoteAddress(), future.cause());
log.error(request.toString());
log.error(response.toString());
}
}
});
} catch (Throwable e) {
log.error("processRequestWrapper process request over, but response failed", e);
log.error(request.toString());
log.error(response.toString());
}
}
} catch (RemotingCommandException e1) {
log.error("excuteRequestWhenWakeup run", e1);
}
}
};
// 交给拉消息线程池去执行 再次查询消息的流程
this.brokerController.getPullMessageExecutor().submit(new RequestTask(run, channel, request));
}
而在executeRequestWhenWakeup()方法中,主要还是调用了一次拉取消息的方法,但是与消费者请求调用不一样的是第三个参数传的是false,也就是这次调用不会触发长轮询,当不触发长轮询的情况下,response就不会等于null,所以broker端就会对消费者发起响应请求,所以接上面的问题,消费端后续还会发起拉取消息请求吗?看到这里应该就会知道肯定是会的,当长轮询结束之后,消费端就会继续恢复发起拉取消息的请求。但是上面的长轮询方式就又会导致一个问题,就是消息不实时推送给消费者,所以RocketMQ在触发消息存储的时候也会调用notifyMessageArriving()方法,以此来达到实时推送新消息的效果
总结
当broker端接收到消费端对于某个队列的拉取新消息请求的时候,如果发现该队列此时并没有新消息,就会触发长轮询机制,在长轮询的过程中,每5s或者1s就会主动去检查下当前队列的consumequeue文件的最大偏移位是否发生改变,如果没有改变并且长轮询还没有超时,那么就继续等下一次的轮询判断,直到长轮询超时。如果发现当前队列的consumequeue文件的最大偏移位发生了改变或者此时长轮询超时了,就会跳出长轮询,然后再去调用一次拉取消息方法响应给消费端。消费端在broker长轮询的过程中会停止继续向broker发起拉消息请求,直到broker端长轮询结束再继续发起拉消息请求,如果broker端发现下一次的拉消息请求又没有新消息,那么就继续进行新一轮的长轮询,以此重复上面的过程