我正在参与掘金创作者训练营第5期,点击了解活动详情
这个需要从消费任务实现类ConsumerRequest和本地缓存队列ProcessQueue的设计来看主要差异。
并发消息的消费请求
class ConsumeRequest implements Runnable {
private final List<MessageExt> msgs;
private final ProcessQueue processQueue;
private final MessageQueue messageQueue;
public ConsumeRequest(List<MessageExt> msgs, ProcessQueue processQueue, MessageQueue messageQueue) {
this.msgs = msgs;
this.processQueue = processQueue;
this.messageQueue = messageQueue;
}
public List<MessageExt> getMsgs() {
return msgs;
}
public ProcessQueue getProcessQueue() {
return processQueue;
}
@Override
public void run() {
...
}
顺序消息的消费请求
class ConsumeRequest implements Runnable {
private final ProcessQueue processQueue;
private final MessageQueue messageQueue;
public ConsumeRequest(ProcessQueue processQueue, MessageQueue messageQueue) {
this.processQueue = processQueue;
this.messageQueue = messageQueue;
}
public ProcessQueue getProcessQueue() {
return processQueue;
}
public MessageQueue getMessageQueue() {
return messageQueue;
}
@Override
public void run() {
...
}
从以上代码可以看出顺序消息的ConsumeRequest中并没有保存需要消费的消息。它会通过
List<MessageExt> msgs = this.processQueue.takeMessages(consumeBatchSize);
获取需要消费的消息并同步消费。
这里我们看看ProcessQueue对于顺序消费有什么特殊的支持。
public class ProcessQueue {
...
private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<Long, MessageExt>();
...
private final TreeMap<Long, MessageExt> consumingMsgOrderlyTreeMap = new TreeMap<Long, MessageExt>();
...
}
在他的属性里边有两个较为重要的属性
msgTreeMap的key是消息的物理位点值,value是消息对象,这个容器是ProcessQueue用来缓存本地消息的,他会按照key值按顺序排列。consumingMsgOrderlyTreeMap的key也是消息物理位点值,value是消息对象,保存的是当前正在处理的消息集合。
再下来看看takeMessages方法:
public List<MessageExt> takeMessages(final int batchSize) {
List<MessageExt> result = new ArrayList<MessageExt>(batchSize);
final long now = System.currentTimeMillis();
try {
this.treeMapLock.writeLock().lockInterruptibly();
this.lastConsumeTimestamp = now;
try {
if (!this.msgTreeMap.isEmpty()) {
for (int i = 0; i < batchSize; i++) {
Map.Entry<Long, MessageExt> entry = this.msgTreeMap.pollFirstEntry();
if (entry != null) {
result.add(entry.getValue());
consumingMsgOrderlyTreeMap.put(entry.getKey(), entry.getValue());
} else {
break;
}
}
}
if (result.isEmpty()) {
consuming = false;
}
} finally {
this.treeMapLock.writeLock().unlock();
}
} catch (InterruptedException e) {
log.error("take Messages exception", e);
}
return result;
}
这段代码从msgTreeMap中取batchSize个消息放入consumingMsgOrderlyTreeMap给客户消费,由于被 this.treeMapLock.writeLock().lockInterruptibly();锁定,并且获取的消息是按照物理位点按顺序排列的,所以消费时只能按照物理位点顺序消费。
可是重点来了,若是消费失败了,又如何保证消费顺序呢?
当处理消费结果的时候有一定的支持,我们一起看看这个方法。
continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, this);
这里的三个参数意义是:
- msgs 当前处理的一批消息
- ststus消费结果的状态
- context和this对于顺序消费作用不大
这个方法里分为自动提交offset和手动提交offset,以自动提交为例说明:
switch (status) {
case COMMIT:
case ROLLBACK:
log.warn("the message queue consume result is illegal, we think you want to ack these message {}",
consumeRequest.getMessageQueue());
case SUCCESS:
//成功后执行commit提交当前消费位点
commitOffset = consumeRequest.getProcessQueue().commit();
this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
break;
case SUSPEND_CURRENT_QUEUE_A_MOMENT:
//统计失败的TPS
this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
if (checkReconsumeTimes(msgs)) {
//从consumingMsgOrderlyTreeMap中删除当前消息,放回msgTreeMap中
consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs);
this.submitConsumeRequestLater(
consumeRequest.getProcessQueue(),
consumeRequest.getMessageQueue(),
context.getSuspendCurrentQueueTimeMillis());
continueConsume = false;
} else {
commitOffset = consumeRequest.getProcessQueue().commit();
}
break;
default:
break;
}
- 对于成功消费的消息,直接提交当前消费位点
- 对于消费失败的消息,则从
consumingMsgOrderlyTreeMap中删除当前消息,并放回msgTreeMap中重新消费
放回msgTreeMap中之后,submitConsumeRequestLater方法会执行一个定时任务,延迟一定时间后重新将消息消费请求发送到消费线程池中,供下一轮消费。
private void submitConsumeRequestLater(
final ProcessQueue processQueue,
final MessageQueue messageQueue,
final long suspendTimeMillis
) {
long timeMillis = suspendTimeMillis;
if (timeMillis == -1) {
timeMillis = this.defaultMQPushConsumer.getSuspendCurrentQueueTimeMillis();
}
if (timeMillis < 10) {
timeMillis = 10;
} else if (timeMillis > 30000) {
timeMillis = 30000;
}
this.scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
ConsumeMessageOrderlyService.this.submitConsumeRequest(null, processQueue, messageQueue, true);
}
}, timeMillis, TimeUnit.MILLISECONDS);
}
也就是说,当顺序消息消费失败的时候,这个失败的消息又会被放入待消费的队列,当继续从待消费队列获取消息的时候,还是会取到这个失败的消息进行消费,以此来保证消费消息的顺序性。