ActiveMQ消息确认机制

背景

在巡更组件中,通过下图所示流程获取门禁,报警器事件来完成巡更任务,流程中有两处使用了ActiveMQ来传输消息,那么消息在传输的过程中ActiveMQ是如何保障消息传输的可靠性,本文将通过介绍ActiveMQ的消息确认机制来进行分析。
[外链图片转存失败(img-yoMs2Vp3-1565192181485)(http://10.1.65.34/group1/M00/00/98/CgFBIlx32SOAHP4NAABLfIVNcBU489.png)]

消息生命周期

我们先看一下activemq中,一条消息的生命周期如下图所示:
在这里插入图片描述
图片中简单的描述了一条消息的生命周期,不过在不同的架构环境中,message的流动性可能更加复杂。一条消息从producer端发出之后,一旦被broker正确保存,那么它将会被consumer消费,然后ACK,broker端才会删除;不过当消息过期或者存储设备溢出时,也会终结它。

ACK机制

ACK模式描述了Consumer与broker确认消息的方式(时机),比如当消息被Consumer接收之后,Consumer将在何时确认消息。对于broker而言,只有接收到ACK指令,才会认为消息被正确的接收或者处理成功了,通过ACK,可以在consumer(/producer)与Broker之间建立一种简单的“担保”机制。

在JMS的API中定义了如下4中确认机制

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;
static final int INDIVIDUAL_ACKNOWLEDGE = 4;

AUTO_ACKNOWLEDGE = 1:自动确认
当client端成功的从receive方法或从onMessage(Message message) 方法返回的时候,会话自动确认client收到消息
CLIENT_ACKNOWLEDGE = 2: 客户端手动确认
客户端通过调用acknowledge方法来确认客户端收到消息。但需要注意在这种应答模式下,确认是在会话层上进行的,确认一个被消费的消息将自动确认所有已消费的其他消息。比如一个消费者已经消费了10条消息,然后确认了第5条消息被消费,则这10条都被确认消费了
DUPS_OK_ACKNOWLEDGE = 3:自动批量确认
不是必须签收,消息可能会重复发送。在第二次重新传送消息的时候,消息头的JmsDelivered会被置为true标示当前消息已经传送过一次,客户端需要进行消息的重复处理控制
SESSION_TRANSACTED = 0:事务提交并确认
当session使用事务时,就是使用此模式。在事务开启之后,和session.commit()之前,所有消费的消息,要么全部正常确认,要么全部redelivery

此外ActiveMQ补充了一个自定义的ACK模式:
INDIVIDUAL_ACKNOWLEDGE = 4 : 单条确认模式
单条消息确认,这种确认模式,我们很少使用,它的确认时机和CLIENT_ACKNOWLEDGE几乎一样,当消息消费成功之后,需要调用message.acknowledege来确认此消息(单条),而CLIENT_ACKNOWLEDGE模式先message.acknowledge()方法将导致整个session中所有消息被确认(批量确认)

ACK类型

public static final byte DELIVERED_ACK_TYPE = 0;
public static final byte STANDARD_ACK_TYPE = 2;
public static final byte POSION_ACK_TYPE = 1;
public static final byte REDELIVERED_ACK_TYPE = 3;
public static final byte INDIVIDUAL_ACK_TYPE = 4;
public static final byte UNMATCHED_ACK_TYPE = 5;
public static final byte EXPIRED_ACK_TYPE = 6;

Client端指定了ACK_MODE,但是在Client与broker在交换ACK指令的时候,还需要告知ACK_TYPE,ACK_TYPE表示此确认指令的类型,不同的ACK_TYPE将传递着消息的状态,broker可以根据不同的ACK_TYPE对消息进行不同的操作。
ActiveMQ中定义了如下几种ACK_TYPE:
DELIVERED_ACK_TYPE = 0 消息"已接收",但尚未处理结束
STANDARD_ACK_TYPE = 2 “标准"类型,通常表示为消息"处理成功”,broker端可以删除消息了
POSION_ACK_TYPE = 1 消息"错误",通常表示"抛弃"此消息,比如消息重发多次后,都无法正确处理时,消息将会被删除或者DLQ(死信队列)
REDELIVERED_ACK_TYPE = 3 消息需"重发",比如consumer处理消息时抛出了异常,broker稍后会重新发送此消息
INDIVIDUAL_ACK_TYPE = 4 表示只确认"单条消息",无论在任何ACK_MODE下
UNMATCHED_ACK_TYPE = 5 在Topic中,如果一条消息在转发给“订阅者”时,发现此消息不符合Selector过滤条件,那么此消息将 不会转发给订阅者,消息将会被存储引擎删除(相当于在Broker上确认了消息)
EXPIRED_ACK_TYPE=6 消息如果过期了,则发送过期指令

源码分析

下面从同步阻塞获取消息的方法的源码中分析其中的原理。
我们通过consumer.receive()来同步接收消息。

public Message receive() throws JMSException {
	  //检查连接是否已关闭
	  checkClosed();
	  //检查是否配置了消息监听器,listener与当前同步接收消息冲突,抛出异常
	  checkMessageListener();
	  //发送拉取消息指令
	  sendPullCommand(0);
	  //从本地队列获取消息
	  MessageDispatch md = dequeue(-1);
	  if (md == null) {
	      return null;
	  }
	  //消息消费前准备
	  beforeMessageIsConsumed(md);
	  //消息消费后准备
	  afterMessageIsConsumed(md, false);
	
	  return createActiveMQMessage(md);
}

以上是客户端消费消息的总体流程,看一下拉取消息指令sendPullCommand的源码如下:

sendPullCommand

/**
 * If we have a zero prefetch specified then send a pull command to the
 * broker to pull a message we are about to receive
 */
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);
    }
}

clearDeliveredList 是为了清空再deliveredMessages中还未ack的消息,具体的操作如下,如果是事务性的会话,则将deliveredMessages中的消息放入previouslyDeliveredMessages列表做本地重发;如果是客户端应答方式,回滚重复的消息。
sendPullCommand方法还检查PrefetchSize这个变量是否为0和的unconsumedMessages(未曾消费的消息)是否为空,满足这两个条件才会发送拉取消息指令到broker端,broker端会推送消息到unconsumedMessages

private void clearDeliveredList() {
    if (clearDeliveredList) {
        synchronized (deliveredMessages) {
            if (clearDeliveredList) {
                if (!deliveredMessages.isEmpty()) {
                	//事务性会话
                    if (session.isTransacted()) {
                        if (previouslyDeliveredMessages == null) {
                            previouslyDeliveredMessages = new PreviouslyDeliveredMap<MessageId, Boolean>(session.getTransactionContext().getTransactionId());
                        }
                        //消息放入previouslyDeliveredMessages列表中
                        for (MessageDispatch delivered : deliveredMessages) {
                            previouslyDeliveredMessages.put(delivered.getMessage().getMessageId(), false);
                        }
                        LOG.debug("{} tracking existing transacted {} delivered list ({}) on transport interrupt",
                                  getConsumerId(), previouslyDeliveredMessages.transactionId, deliveredMessages.size());
                    } else {
                    	//客户端应答
                        if (session.isClientAcknowledge()) {
                            LOG.debug("{} rolling back delivered list ({}) on transport interrupt", getConsumerId(), deliveredMessages.size());
                            // allow redelivery
                            if (!this.info.isBrowser()) {
                            	//回滚发送的消息
                                for (MessageDispatch md: deliveredMessages) {
                                    this.session.connection.rollbackDuplicate(this, md.getMessage());
                                }
                            }
                        }
                        LOG.debug("{} clearing delivered list ({}) on transport interrupt", getConsumerId(), deliveredMessages.size());
                        //清空deliveredMessages队列
                        deliveredMessages.clear();
                        pendingAck = null;
                    }
                }
                clearDeliveredList = false;
            }
        }
    }
}

dequeue

这方法是从本地队列unconsumedMessages中出队列一条消息进行消费,那么broker是如何把消息推送到unconsumedMessages里面呢,简单流程图如下:
在这里插入图片描述
我们先看一下是如何处理从未消费的队列出队列的数据,如果未消费的队列为空,则返回null,如果消息为空,同样返回null,如果消息已过期则仍会被消费,且发送一个类型为EXPIRED_ACK_TYPE的ACK指令,如果消息重发失败,则发送一个类型为POSION_ACK_TYPE的ACK指令,broker会将消息删除或者移入死信队列。

private MessageDispatch dequeue(long timeout) throws JMSException {
    try {
        long deadline = 0;
        if (timeout > 0) {
            deadline = System.currentTimeMillis() + timeout;
        }
        while (true) {
            MessageDispatch md = unconsumedMessages.dequeue(timeout);
            //队列中无未消费的数据,返回null
            if (md == null) {
                if (timeout > 0 && !unconsumedMessages.isClosed()) {
                    timeout = Math.max(deadline - System.currentTimeMillis(), 0);
                } else {
                    if (failureError != null) {
                        throw JMSExceptionSupport.create(failureError);
                    } else {
                        return null;
                    }
                }
            } else if (md.getMessage() == null) {
            	//获取到的消费为空,返回null
                return null;
            } else if (consumeExpiredMessage(md)) {
            	//过期消息直接被消费
                LOG.debug("{} received expired message: {}", getConsumerId(), md);
                beforeMessageIsConsumed(md);
                afterMessageIsConsumed(md, true);
                if (timeout > 0) {
                    timeout = Math.max(deadline - System.currentTimeMillis(), 0);
                }
                sendPullCommand(timeout);
            } else if (redeliveryExceeded(md)) {
            	//重发消息失败,超过最大重发次数后,发送类型为POSION_ACK_TYPE的ACK指令
                LOG.debug("{} received with excessive redelivered: {}", getConsumerId(), md);
                posionAck(md, "Dispatch[" + md.getRedeliveryCounter() + "] to " + getConsumerId() + " exceeds redelivery policy limit:" + redeliveryPolicy);
                if (timeout > 0) {
                    timeout = Math.max(deadline - System.currentTimeMillis(), 0);
                }
                sendPullCommand(timeout);
            } else {
                if (LOG.isTraceEnabled()) {
                    LOG.trace(getConsumerId() + " received message: " + md);
                }
                return md;
            }
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw JMSExceptionSupport.create(e);
    }
}

然后我们看一下timeout对于消费unconsumedMessages消息的影响,如下图所示,我们可以看到unconsumedMessages.dequeue获取不到消息会阻塞,也实现了receive方法的阻塞实现

/* 
 * timeout值为-1,则会阻塞,直到有消息可以消费;
 * timeout值为0,则会尝试去获取数据,如果队列为空,则返回空数据
 * timeout值大于0,队列为空的时候,会阻塞一段时间后重新去读取队列
*/
public MessageDispatch dequeue(long timeout) throws InterruptedException {
    synchronized (mutex) {
        // Wait until the consumer is ready to deliver messages.
        while (timeout != 0 && !closed && (isEmpty() || !running)) {
            if (timeout == -1) {
                mutex.wait();
            } else {
                mutex.wait(timeout);
                break;
            }
        }
        if (closed || !running || isEmpty()) {
            return null;
        }
        return removeFirst();
    }
}

beforeMessageIsConsumed

取出消息后,在消息被消费前,还需做一些准备工作,如果ACK模式不是DUPS_OK_ACKNOWLEDGE,则所有的消息依次放入deliveredMessages链表的表头之中,
如果当前是事务类型的会话,则还需要判断transactedIndividualAck,如果为true,则单条消息直接确认,否则调用ackLater,将消息放入pendingAck中,稍后等待session.commit指令批量确认,相比单个处理,性能也会高出许多。

private void beforeMessageIsConsumed(MessageDispatch md) throws JMSException {
	//设置消息的一个序列ID
    md.setDeliverySequenceId(session.getNextDeliveryId());
    lastDeliveredSequenceId = md.getMessage().getMessageId().getBrokerSequenceId();
    //未开启批量确认
    if (!isAutoAcknowledgeBatch()) {
        synchronized(deliveredMessages) {
        	//将消息放入待确认队列中
            deliveredMessages.addFirst(md);
        }
        //如果是事务性操作
        if (session.getTransacted()) {
            if (transactedIndividualAck) {
            	//单条消息直接ACK确认
                immediateIndividualTransactedAck(md);
            } else {
            	//放入消息,稍后随事务commit的时候一起确认
                ackLater(md, MessageAck.DELIVERED_ACK_TYPE);
            }
        }
    }
}

afterMessageIsConsumed

这个方法主要是对消息进行响应的ACK操作

private void afterMessageIsConsumed(MessageDispatch md, boolean messageExpired) throws JMSException {
   if (unconsumedMessages.isClosed()) {
       return;
   }
   if (messageExpired) {
   	   //消息过期,则直接确认消息,发送EXPIRED_ACK_TYPE指令
       acknowledge(md, MessageAck.EXPIRED_ACK_TYPE);
       stats.getExpiredMessageCount().increment();
   } else {
       stats.onMessage();
       //事务性会话,这里不做任何操作,在session.commit的时候批量ACK
       if (session.getTransacted()) {
       	   // Do nothing.
       } else if (isAutoAcknowledgeEach()) {  
       		//非事务性的 (队列延迟确认[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
                           //当仍未确认的消息数量达到prefetch*0.65时,又或者离上一次ACK的时间间隔超过了optimizeAcknowledgeTimeOut毫秒时,客户端进行自动确认,发送STANDARD_ACK_TYPE的ACK指令
                           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 {
                       		//没开启优化,自动回传单条消息的ACK
                           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是开启批量回传的标记,可以设置批量回传阀值来优化回传效率

optimizeACK

在源码中,我们看到有个optimizeAcknowledge参数,这是ActiveMQ在对于Comsumer在消费消息的时候,对消息ACK的优化选项,也是Consumer端重要的优化参数之一,我们可以通过如下配置开启“可优化的ACK”。
(1)在brokerUrl中增加如下配置:

String brokerUrl = "tcp://localhost:61616?" +   
                   "jms.optimizeAcknowledge=true" +   
                   "&jms.optimizeAcknowledgeTimeOut=30000" +   
                   "&jms.redeliveryPolicy.maximumRedeliveries=6";  
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(brokerUrl);  

(2)在destinationUri中增加如下配置:

String queueName = "test-queue?customer.prefetchSize=100";  
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);  
Destination queue = session.createQueue(queueName);  
  1. 我们需要在brokerUrl指定optimizeACK选项,在destinationUri中指定prefetchSize(预获取)选项,其中brokerUrl参数选项是全局的,即当前factory下所有的connection/session/consumer都会默认使用这些值;而destinationUri中的选项,只会在使用此destination的consumer实例中有效;如果同时指定,brokerUrl中的参数选项值将会被覆盖。optimizeAck表示是否开启“优化ACK”,只有在为true的情况下,prefetchSize(下文中将会简写成prefetch)以及optimizeAcknowledgeTimeout参数才会有意义。此处需要注意"optimizeAcknowledgeTimeout"选项只能在brokerUrl中配置
  2. prefetch值建议在destinationUri中指定,因为在brokerUrl中指定比较繁琐;在brokerUrl中,queuePrefetchSizetopicPrefetchSize都需要单独设定:"&jms.prefetchPolicy.queuePrefetch=12&jms.prefetchPolicy.topicPrefetch=12"等来逐个指定
  3. 如果prefetchACK为true,那么prefetch必须大于0;当prefetchACK为false时,你可以指定prefetch为0以及任意大小的正数。不过,当prefetch=0是,表示consumer将使用PULL(拉取)的方式从broker端获取消息,broker端将不会主动push消息给client端,直到client端发送PullCommand时;当prefetch>0时,就开启了broker push模式,此后只要当client端消费且ACK了一定的消息之后,会立即push给client端多条消息。

总结

本文从ActiveMQ消息的生命周期,ACK的模式和类型介绍了消息被消费之后与borker之间是如何应答的,通过源码了解到了消息在被接收的过程中发生了什么,讲述了不同ACK模式情况下,消息是如何处理以及如何响应,最后通过可优化的ACK,介绍了如何配置参数来对消费者处理消息时进行一个性能优化。

发布了18 篇原创文章 · 获赞 0 · 访问量 285
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 游动-白 设计师: 上身试试

分享到微信朋友圈

×

扫一扫,手机浏览