背景
在巡更组件中,通过下图所示流程获取门禁,报警器事件来完成巡更任务,流程中有两处使用了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