主要关注有以下两点:
如何获取消息?
消息如何应答?
ActiveMQMessageConsumer.receive
这个方法是同步阻塞获取消息的方法,尝试从源码分析其工作原理。
@Override
public Message receive() throws JMSException {
//检查连接
checkClosed();
//检查是否定义了listener,listener和当前的同步监听冲突,抛出异常
checkMessageListener();
//发送拉取消息指令
sendPullCommand(0);
//从本地队列获取消息
MessageDispatch md = dequeue(-1);
if (md == null) {
return null;
}
//消息消费前准备
beforeMessageIsConsumed(md);
//消息消费后准备
afterMessageIsConsumed(md, false);
return createActiveMQMessage(md);
}
以上是客户端消费消息的总体流程,看一下拉取消息指令sendPullCommand的源码如下
sendPullCommand
clearDeliveredList是清空在deliveredMessages中还未ack的消息,具体操作是事务性的会话则放入另一个列表previouslyDeliveredMessages做本地重发消息,非事务的会话就标记这些消息已经被接收了
sendPullCommand检查unconsumedMessages(未曾消费消息)是否为空和PrefetchSize[后面说这个变量含义]是不是等于0,满足才会异步发送拉取消息指令到broker,broker会推送消息到客户端的unconsumedMessages里面.
protected void sendPullCommand(long timeout) throws JMSException {
//清空在deliveredMessages中还未ack的消息
clearDeliveredList();
if (info.getCurrentPrefetchSize() == 0 && unconsumedMessages.isEmpty()) {
MessagePull messagePull = new MessagePull();
messagePull.configure(info);
messagePull.setTimeout(timeout);
session.asyncSendPacket(messagePull);
}
}
dequeue
这个方法是从本地消息队列unconsumedMessages出队列一条消息进行消费,那么broker是如何把消息推送到unconsumedMessages里面的呢?简单流程如下图所示。
然后就是消费unconsumedMessages的消息流程,如下所示。
//获取消息操作 MessageDispatch md = unconsumedMessages.dequeue(timeout);
public MessageDispatch dequeue(long timeout) throws InterruptedException {
synchronized (mutex) {
// Wait until the consumer is ready to deliver messages.
while (timeout != 0 && !closed && (list.isEmpty() || !running)) {
if (timeout == -1) {
mutex.wait();
} else {
mutex.wait(timeout);
break;
}
}
if (closed || !running || list.isEmpty()) {
return null;
}
return list.removeFirst();
}
}
可以看出unconsumedMessages.dequeue获取不到消息会阻塞,也实现了receive方法的阻塞实现。
beforeMessageIsConsumed
private void beforeMessageIsConsumed(MessageDispatch md) throws JMSException {
md.setDeliverySequenceId(session.getNextDeliveryId());
lastDeliveredSequenceId = md.getMessage().getMessageId().getBrokerSequenceId();
if (!isAutoAcknowledgeBatch()) {
synchronized(deliveredMessages) {
//放入待确认消息列表
deliveredMessages.addFirst(md);
}
if (session.getTransacted()) {
if (transactedIndividualAck) {
//可以马上确认的消息
immediateIndividualTransactedAck(md);
} else {
//放入稍后在事务commit的时候一起确认的消息
ackLater(md, MessageAck.DELIVERED_ACK_TYPE);
}
}
}
}
这里面主要是做消息消费之前的一些准备工作,如果ack不是【topic的DUPS_OK_ACKNOWLEDGE】,则所有的消息先放到deliveredMessages链表的开头。
如果当前是 事务类型的会话,还需要特殊操作,判断transactedIndividualAck,如果为true,表示单条消息直接返回ack。 否则,调用ackLater,消息放入pendingack,等待处理。批量应答, client端在消费消息后暂且不发送ACK,而是把它缓存下来(pendingACK),等到这 些消息的条数达到一定阀值时,session.commit指令把它们全部确认;这比对每条消息都逐个确认,在性能上 要提高很多,pendingACK就是事务的批量ack;
afterMessageIsConsumed
这个方法主要就是对消息进行响应的ack操作
private void afterMessageIsConsumed(MessageDispatch md, boolean messageExpired) throws JMSException {
if (unconsumedMessages.isClosed()) {
return;
}
if (messageExpired) {
//消息过期了,就ack过期了
acknowledge(md, MessageAck.EXPIRED_ACK_TYPE);
stats.getExpiredMessageCount().increment();
} else {
stats.onMessage();
if (session.getTransacted()) {
// Do nothing.
//会话是事务型的,就不在这里处理,是在commit的时候进行批量ack
} else if (isAutoAcknowledgeEach()) {
//session.isAutoAcknowledge() || ( session.isDupsOkAcknowledge() && getDestination().isQueue() );
//非事务性的 (队列延迟确认[Dups])或者(自动确认)执行以下逻辑
if (deliveryingAcknowledgements.compareAndSet(false, true)) {
synchronized (deliveredMessages) {
if (!deliveredMessages.isEmpty()) {
//是否开启优化ack,开启则【等同开启批量确认】
if (optimizeAcknowledge) {
ackCounter++;
// AMQ-3956 evaluate both expired and normal msgs as
// otherwise consumer may get stalled
if (ackCounter + deliveredCounter >= (info.getPrefetchSize() * .65) || (optimizeAcknowledgeTimeOut > 0 && System.currentTimeMillis() >= (optimizeAckTimestamp + optimizeAcknowledgeTimeOut))) {
MessageAck ack = makeAckForAllDeliveredMessages(MessageAck.STANDARD_ACK_TYPE);
if (ack != null) {
deliveredMessages.clear();
ackCounter = 0;
session.sendAck(ack);
optimizeAckTimestamp = System.currentTimeMillis();
}
// AMQ-3956 - as further optimization send
// ack for expired msgs when there are any.
// This resets the deliveredCounter to 0 so that
// we won't sent standard acks with every msg just
// because the deliveredCounter just below
// 0.5 * prefetch as used in ackLater()
if (pendingAck != null && deliveredCounter > 0) {
//达到批量确认阀值,进行批量确认
session.sendAck(pendingAck);
pendingAck = null;
deliveredCounter = 0;
}
}
} else {
//没开启优化,自动回传单条
MessageAck ack = makeAckForAllDeliveredMessages(MessageAck.STANDARD_ACK_TYPE);
if (ack!=null) {
deliveredMessages.clear();
session.sendAck(ack);
}
}
}
}
deliveryingAcknowledgements.set(false);
}
} else if (isAutoAcknowledgeBatch()) {
//非事务性的 topic的延时确认【等同开启批量确认】
ackLater(md, MessageAck.STANDARD_ACK_TYPE);
} else if (session.isClientAcknowledge()||session.isIndividualAcknowledge()) {
//客户端手动确认,CLIENT_ACKNOWLEDGE 批量确认之前所有,INDIVIDUAL_ACKNOWLEDGE确认当前单条
boolean messageUnackedByConsumer = false;
synchronized (deliveredMessages) {
messageUnackedByConsumer = deliveredMessages.contains(md);
}
if (messageUnackedByConsumer) {
ackLater(md, MessageAck.DELIVERED_ACK_TYPE);
}
}
else {
throw new IllegalStateException("Invalid session state.");
}
}
}
optimizeAcknowledge是开启批量回传的标记,可以设置批量回传阀值来优化回传效率。
**
总结消息确认模式
**
事务性会话
SESSION_TRANSACTED
所有的事务性会话,不管如何设置ack模式,都是按照以下原则:同一事务内部,在执行commit之前接收到的消息,都是没有ack的,放在pendingAck里面。在进行session.commit的时候会把pendingAck里面的消息全部一次确认,视为手动批量确认也。事务会话要求效率高,因此都是批量进行ack的,没有别的方式。
非事务性会话
1.(队列延迟确认[DUPS_OK_ACKNOWLEDGE])或者(自动确认[AUTO_ACKNOWLEDGE])= auto each
1.1 开启optimizeAcknowledge优化回传,相当于批量回传,达到设置的阀值之后自动批量回传ack
1.2 没有开启的话就都是马上自动发送标准的ack,回传单条数据
2.( topic的延时确认[DUPS_OK_ACKNOWLEDGE])= auto batch
2.1 统一都是批量确认,达到设置的阀值之后自动批量回传ack
3 . (CLIENT_ACKNOWLEDGE | INDIVIDUAL_ACKNOWLEDGE)
3.1 CLIENT_ACKNOWLEDGE 按照客户端手动确认处理,类似于session.commit,如果堆积多条才手动确认,可以是等同手动批量确认,只不过是没有阀值的批量确认。INDIVIDUAL_ACKNOWLEDGE手动确认单条。
ack_type
上面的正常消息确认ack_type都是MessageAck.STANDARD_ACK_TYPE,ack_type总共有以下几种:
DELIVERED_ACK_TYPE:通知broker已经消息已经投递到消费者,但还没做处理
STANDARD_ACK_TYPE:通知broker消息处理成功
REDELIVERED_ACK_TYPE:通知broker消息重发
INDIVIDUAL_ACK_TYPE:表示手动确认时,只确认单条消息
UNMATCHED_ACK_TYPE :表示Topic中,如果一条消息在转发给“订阅者”时,发现此消息不符合Selector过滤条件,那么此消息将 不会转发给订阅者,消息将会被存储引擎删除(相当于在Broker上确认了消息)。
POSION_ACK_TYPE :消息”错误”,通常表示”抛弃”此有毒消息,比如消息重发多次后,都无法正确处理时,消息将会被删除或者DLQ(死信队列)