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端进行消息确认的时候并不会删除磁盘中持久化的消息内容。