ActiveMQ源码分析之Queue消费者

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,这样就维护了一个队列可以对应多个消费者的关系,这也就是为什么同一个队列的消费者只会获取到属于该队列的消息,而不会获取到别的队列消息的原因。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值