ActiveMQ中有两种消费模式,Queue(点对点)和Topic (发布/订阅),存储模式也分为非持久化和持久化。由于使用非持久化存储消息只会存储在内存中,容易造成消息丢失,实际生产环境中使用较少,因此重点介绍持久化下Queue消费。
Queue模式下,允许同时有多个消费者,但是一条消息只会被其中一个消费者消费一次,ActiveMQ是如何实现这种机制的呢?我们先从Broker获取消费者需要的消息看起。
当生产者发送消息到Broker时,对于消费者有两种状态:
1、此时消费者消费者和Broker处于正常连接状态,则将内存中缓存的消息直接发送给消费者。
2、此时没有消费者连接到Broker,则消息默认会存储在Kahadb中,直到有消费者连接成功之后,从kahadb中获取历史信息给到消费者。
我们重点来看下第二种情况。
Queue类实现了Task接口中的iterate方法,PooledTaskRunner类中的定时任务会调用该iterate方法从kahadb中获取需要推送给消费者的消息。
iterate方法会先执行doPageInForDispatch方法从kahadb中获取消息,接着执行doDispatch方法发送数据。
doPageInForDispatch方法中会调用QueueStorePrefetch的doFillBatch获取消息,一次最大获取的消息数量默认为200条,如果消息实际数量小于200,则以实际数量为准。
@Override
protected void doFillBatch() throws Exception {
hadSpace = this.hasSpace();
if (!broker.getBrokerService().isPersistent() || hadSpace) {
this.store.recoverNextMessages(this.maxBatchSize, this);
dealWithDuplicates(); // without the index lock
}
}
实际执行获取操作的是KahaDBStore类的recoverNextMessages方法
@Override
public void recoverNextMessages(final int maxReturned, final MessageRecoveryListener listener) throws Exception {
indexLock.writeLock().lock();
try {
pageFile.tx().execute(new Transaction.Closure<Exception>() {
@Override
public void execute(Transaction tx) throws Exception {
StoredDestination sd = getStoredDestination(dest, tx);
Entry<Long, MessageKeys> entry = null;
int counter = recoverRolledBackAcks(sd, tx, maxReturned, listener);
for (Iterator<Entry<Long, MessageKeys>> iterator = sd.orderIndex.iterator(tx); iterator.hasNext(); ) {
entry = iterator.next();
if (ackedAndPrepared.contains(entry.getValue().messageId)) {
continue;
}
Message msg = loadMessage(entry.getValue().location);
msg.getMessageId().setFutureOrSequenceLong(entry.getKey());
listener.recoverMessage(msg);
counter++;
if (counter >= maxReturned) {
break;
}
}
sd.orderIndex.stoppedIterating();
}
});
} finally {
indexLock.writeLock().unlock();
}
}
protected final ReentrantReadWriteLock indexLock = new ReentrantReadWriteLock();
indexLock为读写锁,在开始从Kahabd中读取消息之前先加上写锁,避免别的线程这个时候还在往Kahadb中写数据,造成数据丢失或数据不一致。
首先从缓存storedDestinations中根据队列名称获取StoredDestination对象,该对象封装了locationIndex,messageIdIndex,orderIndex等索引数据,同时也包含Topic特有的subscriptions信息等。
接着根据获取的orderIndex信息从pageId = 2的page中获取消息的消息存储信息
[BTreeNode leaf: [0, 1, 2, 3, 4, 5]]
[[ID:jiangzhiqiangdeMacBook-Pro.local-50717-1564846232164-1:1:1:1:1,1:1368314], [ID:jiangzhiqiangdeMacBook-Pro.local-50730-1564847411075-1:1:1:1:1,1:1384842], [ID:jiangzhiqiangdeMacBook-Pro.local-50751-1564848746670-1:1:1:1:1,1:1427478], [ID:jiangzhiqiangdeMacBook-Pro.local-49523-1564899210203-1:1:1:1:1,1:1503422], [ID:jiangzhiqiangdeMacBook-Pro.local-49984-1564901853552-1:1:1:1:1,1:1525194], [ID:jiangzhiqiangdeMacBook-Pro.local-50178-1564903045391-1:1:1:1:1,1:1566270]]
其中1:1368314,1:1384842,1:1427478,1:1503422,1:1525194,1:1566270为消息存储的索引,或者说消息在存储文件中存储的偏移量。loadMessage(entry.getValue().location)根据索引信息获取到消息之后,会将消息消息缓存一份在PendingList类型的batchList对象中。前面说到当生产者发送消息到Broker时,消费者正常连接时,生产者发送的数据会缓存在该batchList对象中,并直接发送给消费者。当消费者发送消费确认的消息到Broker时,broker也会根据该对象缓存的消息进行消息确认后续逻辑处理。
获取到消息后,接着是发送流程。
执行消息发送之前,先判断下是否有正常连接的消费者,如果没有则消息不会发送。
List<Subscription> consumers;
consumersLock.readLock().lock();
try {
if (this.consumers.isEmpty()) {
// slave dispatch happens in processDispatchNotification
return list;
}
consumers = new ArrayList<Subscription>(this.consumers);
} finally {
consumersLock.readLock().unlock();
}
当有消费者连接上Broker之后,则开始推送消息。
Set<Subscription> fullConsumers = new HashSet<Subscription>(this.consumers.size());
for (Iterator<MessageReference> iterator = list.iterator(); iterator.hasNext();) {
MessageReference node = iterator.next();
Subscription target = null;
for (Subscription s : consumers) {
if (s instanceof QueueBrowserSubscription) {
continue;
}
if (!fullConsumers.contains(s)) {
if (!s.isFull()) {
if (dispatchSelector.canSelect(s, node) && assignMessageGroup(s, (QueueMessageReference)node) && !((QueueMessageReference) node).isAcked() ) {
// Dispatch it.
s.add(node);
LOG.info("assigned {} to consumer {}", node.getMessageId(), s.getConsumerInfo().getConsumerId());
iterator.remove();
target = s;
break;
}
} else {
// no further dispatch of list to a full consumer to
// avoid out of order message receipt
fullConsumers.add(s);
LOG.trace("Subscription full {}", s);
}
}
}
if (target == null && node.isDropped()) {
iterator.remove();
}
// return if there are no consumers or all consumers are full
if (target == null && consumers.size() == fullConsumers.size()) {
return list;
}
当同一个队列有多个消费者的时候,Broker是如何进行消息在多个消费者之间进行负载均衡的呢?
消费者consumers集合是一个list列表,consumers中只会存相同queue的消费者信息,不同的queue对应的consumers列表是不同的。因此在对同一个queue的消息进行分发时,则根据消费者列表进行均分,目的是避免消息分配的时候出现消息分配不均匀,导致有的消费者亚历山大,有的却无所事事。而对于多个不同queue获取消息,则是根据不同的queue创建不同的consumers,每次从Kahadb中获取一个queue的消息, 并且将这部分消息均分给对应consumers消费。
当有新的消费者连接到Broker之后,broker会在DestinationFactoryImpl类的createDestination方法中新创建一个Queue对象,而consumers列表是这个Queue对象的属性,接着在AbstractRegion类的addDestination方法中将消费者的队列名称和Queue进行关联,并保存在map中destinations.put(destination, dest);当同一个队列的其他消费者也连接到broker上时,就根据队列名称直接获取Queue对象,实际上是获取Queue中的consumers,这样就维护了一个队列可以对应多个消费者的关系,这也就是为什么同一个队列的消费者只会获取到属于该队列的消息,而不会获取到别的队列消息的原因。