ActiveMQ源碼分析之消息確認

ActiveMQ中消息只有在被Broker確認之後才能認爲被成功消費。消息的成功消費通常包括三個階段:1、客戶端接收消息,2、客戶端處理消息,3、Broker確認消息。其中第2階段和第3階段的順序根據客戶端接收消息的方式而定。如果客戶端採用receive的方式接收,則階段2和階段3是異步執行的,也就是說用戶在真正處理消息之時,Broker可能已經確認完了。客戶端如果採用listener的方式,則客戶端會在處理完listener中的邏輯之後再發送確認消息到Broker,這種方式下階段2和階段3是同步的,比起receive方式保證了消息不會丟失,增加了消息的可靠性,但是同時也降低了客戶端處理消息的效率。要效率還是可靠性,請根據實際業務場景衡量。

在事務性會話中,當一個事務被提交,確認會自動發生。在非事務性會話中,消息何時被確認取決於客戶端創建會話時的應答模式(acknowledgement  mode)。ActiveMQ中Session用於表示會話,在Session中該參數有以下四個可選值:

public interface Session extends Runnable {

    static final int AUTO_ACKNOWLEDGE = 1;

    static final int CLIENT_ACKNOWLEDGE = 2;

    static final int DUPS_OK_ACKNOWLEDGE = 3;

    static final int SESSION_TRANSACTED = 0;
}

SESSION_TRANSACTED: 表示事務性會話,Broker會批量發送消息到客戶端,客戶端消費這批消息的過程都處在一個事務中,在消費完這批消息的時候切記不要忘了session.commit();提交事務,否則會重複消費消費過的消息。

AUTO_ACKNOWLEDGE:當客戶端成功的從receive方法返回的時候,或者從MessageListener.onMessage方法成功返回的時候,會話自動確認客戶收到的消息。

CLIENT_ACKNOWLEDGE:客戶端手動調用消息的acknowledge方法確認消息。需要注意的是,在這種模式中,確認是在會話層上進行,確認一個被消費的消息將自動確認所有已被會話消費的消息。例如,如果一個消息消費者消費了100條消息,然後確認其中第30條消息,那麼所有100條消息都會被確認。

DUPS_OK_ACKNOWLEDGE:客戶端手動延遲確認消息的提交。在這種模式下,允許客戶端不必急於發送消息確認信息,允許在收到多個消息之後一次完成確認。這種模式可以提升默認單條確認的效率,但是由於是延遲確認,在系統崩潰或者網絡出現問題的時候會有重複消費的情況。

客戶端除了有以上四種應答模式之外,還有如下幾種應答類型:

public class MessageAck extends BaseCommand {

    /**
     * Used to let the broker know that the message has been delivered to the
     * client. Message will still be retained until an standard ack is received.
     * This is used get the broker to send more messages past prefetch limits
     * when an standard ack has not been sent.
     */
    public static final byte DELIVERED_ACK_TYPE = 0;

    /**
     * The standard ack case where a client wants the message to be discarded.
     */
    public static final byte STANDARD_ACK_TYPE = 2;

    /**
     * In case the client want's to explicitly let the broker know that a
     * message was not processed and the message was considered a poison
     * message.
     */
    public static final byte POSION_ACK_TYPE = 1;

    /**
     * In case the client want's to explicitly let the broker know that a
     * message was not processed and it was re-delivered to the consumer
     * but it was not yet considered to be a poison message.  The messageCount 
     * field will hold the number of times the message was re-delivered. 
     */
    public static final byte REDELIVERED_ACK_TYPE = 3;
    
    /**
     * The  ack case where a client wants only an individual message to be discarded.
     */
    public static final byte INDIVIDUAL_ACK_TYPE = 4;

/**
     * The ack case where a durable topic subscription does not match a selector.
     */
    public static final byte UNMATCHED_ACK_TYPE = 5;

    /**
     * the case where a consumer does not dispatch because message has expired inflight
     */
    public static final byte EXPIRED_ACK_TYPE = 6;

}

 DELIVERED_ACK_TYPE:消息"已接收",但尚未處理結束。事務性會話中,消息在消費過程中Broker收到的應答類型就爲0,此時並不會執行確認相關的刪除操作,如果最終Broker沒有收到事務commit操作,則之前消費過的消息下次還會繼續推送給客戶端。

STANDARD_ACK_TYPE:消息"已處理",通常表示消息消費成功,Broker可以執行相關的刪除操作了。

POSION_ACK_TYPE:消息"錯誤",通常表示"拋棄"此消息,比如消息重發多次,默認6次,都無法正確處理時,消息將會被刪除或者發送到 DLQ(死信隊列),在消息處理的時候,dispatch方法內會判斷該消息是否爲重發消息。

REDELIVERED_ACK_TYPE:當客戶端在處理消息時異常了,Broker會重新發送這條消息。

INDIVIDUAL_ACK_TYPE:表示只確認"單條消息"。

UNMATCHED_ACK_TYPE:在Topic消費模式下 ,如果一條消息在轉發給“訂閱者”時,發現此消息不符合 Selector 過濾條件,那麼此消息將不會推送給訂閱者,消息也會被Broker刪除。

EXPIRED_ACK_TYPE:當客戶端發現消息已經過期時,不會消費該條消息,並且提交消息過期的應答。Broker接收到應答之後當做已被成功消費處理,執行相關的刪除操作。

我們先來看看AUTO_ACKNOWLEDGE模式下,receive和MessageListener.onMessage兩種消費方式發送確認消息時機上的不同。

先來看看receive接收消息方式:

    @Override
    public Message receive(long timeout) throws JMSException {
        checkClosed();
        checkMessageListener();
        if (timeout == 0) {
            return this.receive();
        }

        sendPullCommand(timeout);
        while (timeout > 0) {

            MessageDispatch md;
            if (info.getPrefetchSize() == 0) {
                md = dequeue(-1); // We let the broker let us know when we timeout.
            } else {
                md = dequeue(timeout);
            }

            if (md == null) {
                return null;
            }

            beforeMessageIsConsumed(md);
            afterMessageIsConsumed(md, false);
            return createActiveMQMessage(md);
        }
        return null;
    }

 其中afterMessageIsConsumed方法會向Broker發送消息確認信息,而在發送確認消息的時候還沒有到客戶端真正處理消息的邏輯。所以使用receive方式接收消息,消息處理和消息確認是一個異步過程。

再來看看MessageListener.onMessage方式:

    @Override
    public void dispatch(MessageDispatch md) {
        MessageListener listener = this.messageListener.get();
        try {
            clearMessagesInProgress();
            clearDeliveredList();
            synchronized (unconsumedMessages.getMutex()) {
                if (!unconsumedMessages.isClosed()) {
                    if (this.info.isBrowser() || !session.connection.isDuplicate(this, md.getMessage())) {
                        if (listener != null && unconsumedMessages.isRunning()) {
                            if (redeliveryExceeded(md)) {
                                posionAck(md, "listener dispatch[" + md.getRedeliveryCounter() + "] to " + getConsumerId() + " exceeds redelivery policy limit:" + redeliveryPolicy);
                                return;
                            }
                            ActiveMQMessage message = createActiveMQMessage(md);
                            beforeMessageIsConsumed(md);
                            try {
                                boolean expired = isConsumerExpiryCheckEnabled() && message.isExpired();
                                if (!expired) {
                                    listener.onMessage(message);
                                }
                                afterMessageIsConsumed(md, expired);
                            } catch (RuntimeException e) {
                                LOG.error("{} Exception while processing message: {}", getConsumerId(), md.getMessage().getMessageId(), e);
                                md.setRollbackCause(e);
                                if (isAutoAcknowledgeBatch() || isAutoAcknowledgeEach() || session.isIndividualAcknowledge()) {
                                    // schedual redelivery and possible dlq processing
                                    rollback();
                                } else {
                                    // Transacted or Client ack: Deliver the next message.
                                    afterMessageIsConsumed(md, false);
                                }
                            }
                        } 
                    } 
                }
            }
            if (++dispatchedCount % 1000 == 0) {
                dispatchedCount = 0;
                Thread.yield();
            }
        } catch (Exception e) {
            session.connection.onClientInternalException(e);
        }
    }

其中listener.onMessage(message)會執行客戶端消息處理邏輯,在處理完消息之後再afterMessageIsConsumed(md, expired);提交消息確認信息。所以使用onMessage(message)方式接收消息,消息處理和消息確認是一個同步過程。

我們接着看CLIENT_ACKNOWLEDGE模式在該應答模式下,客戶端不會主動提交消息確認信息,只是通過ackLater方法記錄已消費的消息,需要客戶端手動調用acknowledge方法發送消息確認信息。但是在滿足(0.5 * info.getPrefetchSize()) <= (deliveredCounter + ackCounter - additionalWindowSize)的情況下則會觸發自動提交DELIVERED_ACK_TYPE類型的應答,Broker在接到DELIVERED_ACK_TYPE類型的應答時會更新prefetchExtension的值,這個值在Broker向客戶端推送消息的時候用於判斷客戶端堆積的消息是否超過了預取值prefetchSize,如果到達預取值,則Broker將不會再向客戶端發送新的消息這樣做是避免了在客戶端消費過多消息而不確認,而導致消息堆積在Broker端,增加Broker的壓力。如果消息有設置過期時間,則可能會導致Broker端部分消息過期,造成消息丟失。

接着再來看下DUPS_OK_ACKNOWLEDGE模式,如果是Queue方式消費的話,該應答模式下每消費一條消息就會確認一條,因此該模式對於Queue方式消費不起作用。

private boolean isAutoAcknowledgeEach() {
    return session.isAutoAcknowledge() || ( session.isDupsOkAcknowledge() && getDestination().isQueue() );
}

 Topic模式下跟CLIENT_ACKNOWLEDGE模式很相似,客戶端不會主動提交確認消息,不過ackLater方法記錄已消費的確認信息時應答類型都是STANDARD_ACK_TYPE。當客戶端手動調用acknowledge方法或者在滿足(0.5 * info.getPrefetchSize()) <= (deliveredCounter + ackCounter - additionalWindowSize)的情況下則會觸發自動提交STANDARD_ACK_TYPE類型的應答

最後來看下Broker接收到消息確認信息之後處理邏輯

     protected void removeMessage(ConnectionContext context, Subscription sub,          final QueueMessageReference reference,
            MessageAck ack) throws IOException {
        LOG.trace("ack of {} with {}", reference.getMessageId(), ack);
        // This sends the ack the the journal..
        if (!ack.isInTransaction()) {
            acknowledge(context, sub, ack, reference);
            dropMessage(reference);
        } else {
            try {
                acknowledge(context, sub, ack, reference);
            } finally {
                context.getTransaction().addSynchronization(new Synchronization() {

                    @Override
                    public void afterCommit() throws Exception {
                        dropMessage(reference);
                        wakeup();
                    }

                    @Override
                    public void afterRollback() throws Exception {
                        reference.setAcked(false);
                        wakeup();
                    }
                });
            }
        }
        if (ack.isPoisonAck() || (sub != null && sub.getConsumerInfo().isNetworkSubscription())) {
            // message gone to DLQ, is ok to allow redelivery
            messagesLock.writeLock().lock();
            try {
                messages.rollback(reference.getMessageId());
            } finally {
                messagesLock.writeLock().unlock();
            }
            if (sub != null && sub.getConsumerInfo().isNetworkSubscription()) {
                getDestinationStatistics().getForwards().increment();
            }
        }
        // after successful store update
        reference.setAcked(true);
    }

Broker收到STANDARD_ACK_TYPE類型的應答之後,主要執行刪除操作,acknowledge方法刪除消息對應的索引信息,dropMessage方法刪除內存中的消息緩存,而kahadb中對應的持久化消息則沒有被刪除,不刪除kahadb中的消息的原因是消息存儲是追加append的方式,爲順序存儲,沒有刪除的必要。比如一條消息的索引是1:6480504,我們在確認消費消息之後會發現該索引已經從對應的Queue中刪除了,但是根據該索引取獲取消息,依然能從kahadb中獲取到消息內容,說明Broker端進行消息確認的時候並不會刪除磁盤中持久化的消息內容。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章