1. unconsumedMessage 源码分析
2. 消费端的 PrefetchSize
3. 消息的确认过程
4. 消息重发机制
5. ActiveMQ 多节点高性能方案
消息消费流程图
unconsumedMessages数据的获取过程
那我们来看看 ActiveMQConnectionFactory. createConnection 里面做了什么 事情。
protected ActiveMQConnection createActiveMQConnection(String userName, String password) throws JMSException {
if (brokerURL == null) {
throw new ConfigurationException("brokerURL not set.");
}
ActiveMQConnection connection = null;
try {
Transport transport = createTransport();
connection = createActiveMQConnection(transport, factoryStats);
connection.setUserName(userName);
connection.setPassword(password);
configureConnection(connection);
transport.start();
if (clientID != null) {
connection.setDefaultClientID(clientID);
}
return connection;
} catch (JMSException e) {
// Clean up!
try {
connection.close();
} catch (Throwable ignore) {
}
throw e;
} catch (Exception e) {
// Clean up!
try {
connection.close();
} catch (Throwable ignore) {
}
throw JMSExceptionSupport.create("Could not connect to broker URL: " + brokerURL + ". Reason: " + e, e);
}
}
transport.start()
在之前文章中,我已经分析了,其实transport是一个链式调用,是一个多层包装的对象。
ResponseCorrelator(MutexTransport(WireFormatNegotiator(InactivityMonitor(TcpTransport())))
最终调用 TcpTransport.start()方法,然而这个类中并没有 start,而是在父类ServiceSupport.start()中。
public void start() throws Exception {
if (started.compareAndSet(false, true)) {
boolean success = false;
stopped.set(false);
try {
preStart();
doStart();
success = true;
} finally {
started.set(success);
}
for(ServiceListener l:this.serviceListeners) {
l.started(this);
}
}
}
这块代码看起来就比较熟悉了,我们之前看过的中间件的源码,通信层都是独 立来实现及解耦的。而 ActiveMQ 也是一样,提供了 Transport 接口和TransportSupport 类。
这个接口的主要作用是为了让客户端有消息被异步发送、同步发送和被消费的能力。
接下来沿着 doStart()往下看,又调用TcpTransport.doStart() ,接着通过 super.doStart(),调用TransportThreadSupport.doStart(). 创建了一个线程,传入的是 this,调用子类的 run 方法,也就是 TcpTransport.run()。
TcpTransport.run
run 方法主要是从 socket 中读取数据包,只要 TcpTransport 没有停止,它就会不断去调用 doRun。
@Override
public void run() {
LOG.trace("TCP consumer thread for " + this + " starting");
this.runnerThread=Thread.currentThread();
try {
while (!isStopped()) {
doRun();
}
} catch (IOException e) {
stoppedLatch.get().countDown();
onException(e);
} catch (Throwable e){
stoppedLatch.get().countDown();
IOException ioe=new IOException("Unexpected error occurred: " + e);
ioe.initCause(e);
onException(ioe);
}finally {
stoppedLatch.get().countDown();
}
}
TcpTransport.run
run 方法主要是从 socket 中读取数据包,只要 TcpTransport 没有停止,它就会不断去调用 doRun。
protected void doRun() throws IOException {
try {
Object command = readCommand();
doConsume(command);
} catch (SocketTimeoutException e) {
} catch (InterruptedIOException e) {
}
}
TcpTransport.readCommand
这里面,通过 wireFormat 对数据进行格式化,可以认为这是一个反序列化过程。wireFormat 默认实现是 OpenWireFormat,activeMQ 自定义的跨语言的
wire 协议 。
protected Object readCommand() throws IOException {
return wireFormat.unmarshal(dataIn);
}
分析到这,我们差不多明白了传输层的主要工作是获得数据并且把数据转换为对象,再把对象对象传给 ActiveMQConnection
TransportSupport.doConsume
TransportSupport 类中最重要的方法是 doConsume,它的作用就是用来“消费消息” 。
public void doConsume(Object command) {
if (command != null) {
if (transportListener != null) {
transportListener.onCommand(command);
} else {
LOG.error("No transportListener available to process inbound command: " + command);
}
}
}
TransportSupport 类中唯一的成员变量是 TransportListener ,这也意味着一个 Transport 支持类绑定一个传送监听器类,传送监听器接口TransportListener 最重要的方法就是 void onCommand(Object command);,它用来处理命令, 这个 transportListener 是在哪里赋值的呢?再回到 ActiveMQConnection 的构造方法中。传递了 ActiveMQConnection 自己本身,(ActiveMQConnection 是TransportListener 接口的实现类之一) 于是,消息就这样从传送层到达了我们的连接层上。
protected ActiveMQConnection(final Transport transport, IdGenerator clientIdGenerator, IdGenerator connectionIdGenerator, JMSStatsImpl factoryStats) throws Exception {
//绑定传输对象
this.transport = transport;
this.clientIdGenerator = clientIdGenerator;
this.factoryStats = factoryStats;
// Configure a single threaded executor who's core thread can timeout if
// idle
executor = new ThreadPoolExecutor(1, 1, 5, TimeUnit.SECONDS, new LinkedBlockingQueue(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "ActiveMQ Connection Executor: " + transport);
//Don't make these daemon threads - see https://issues.apache.org/jira/browse/AMQ-796
//thread.setDaemon(true);
return thread;
}
});
// asyncConnectionThread.allowCoreThreadTimeOut(true);
String uniqueId = connectionIdGenerator.generateId();
this.info = new ConnectionInfo(new ConnectionId(uniqueId));
this.info.setManageable(true);
this.info.setFaultTolerant(transport.isFaultTolerant());
this.connectionSessionId = new SessionId(info.getConnectionId(), -1);
//transport绑定为自己
this.transport.setTransportListener(this);
this.stats = new JMSConnectionStatsImpl(sessions, this instanceof XAConnection);
this.factoryStats.addConnection(this);
this.timeCreated = System.currentTimeMillis();
this.connectionAudit.setCheckForDuplicates(transport.isFaultTolerant());
}
从构造函数可以看出,创建 ActiveMQConnection 对象时,除了和 Transport相互绑定,还对线程池执行器 executor 进行了初始化。下面我们看看该类的核 心方法 。
onCommand
这里面会针对不同的消息做分发,比如传入的 command 是MessageDispatch,那么这个 command 的 visit 方法就会调用processMessageDispatch 方法
public void onCommand(final Object o) {
final Command command = (Command)o;
if (!closed.get() && command != null) {
try {
command.visit(new CommandVisitorAdapter() {
@Override
public Response processMessageDispatch(MessageDispatch md) throws Exception {
//等待Transport中断处理完成
waitForTransportInterruptionProcessingToComplete();
//这里通过消费者id获取要分发的消费者对象
ActiveMQDispatcher dispatcher = dispatchers.get(md.getConsumerId());
if (dispatcher != null) {
// Copy in case a embedded broker is dispatching via
// vm://
// md.getMessage() == null to signal end of queue
// browse.
Message msg = md.getMessage();
if (msg != null) {
msg = msg.copy();
msg.setReadOnlyBody(true);
msg.setReadOnlyProperties(true);
msg.setRedeliveryCounter(md.getRedeliveryCounter());
msg.setConnection(ActiveMQConnection.this);
msg.setMemoryUsage(null);
md.setMessage(msg);
}
//调用会话ActiveMQSession自己的dispatch方法来处理这条消息
dispatcher.dispatch(md);
} else {
LOG.debug("{} no dispatcher for {} in {}", this, md, dispatchers);
}
return null;
}
//如果传入的是 ProducerAck,则调用的是下面这个方法,这里我 们仅仅关注 MessageDispatch
就行了
@Override
public Response processProducerAck(ProducerAck pa) throws Exception {
if (pa != null && pa.getProducerId() != null) {
ActiveMQMessageProducer producer = producers.get(pa.getProducerId());
if (producer != null) {
producer.onProducerAck(pa);
}
}
return null;
}
在现在这个场景中,我们只关注 processMessageDispatch 方法,在这个方法中,只是简单的去调用 ActiveMQSession 的 dispatch 方法来处理消息,
tips: command.visit, 这里使用了适配器模式,如果 command 是一个MessageDispatch,那么它就会调用 processMessageDispatch 方法,其他方
法他不会关心,代码如下:MessageDispatch.visit 。
@Override
public Response visit(CommandVisitor visitor) throws Exception {
return visitor.processMessageDispatch(this);
}
ActiveMQSession.dispatch(md)
executor 这个对象其实是一个成员对象 ActiveMQSessionExecutor,专门负 责来处理消息分发 。
public void dispatch(MessageDispatch messageDispatch) {
try {
executor.execute(messageDispatch);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
connection.onClientInternalException(e);
}
}
ActiveMQSessionExecutor.execute
这个方法的核心功能就是处理消息的分发。
void execute(MessageDispatch message) throws InterruptedException {
if (!startedOrWarnedThatNotStarted) {
ActiveMQConnection connection = session.connection;
long aboutUnstartedConnectionTimeout = connection.getWarnAboutUnstartedConnectionTimeout();
if (connection.isStarted() || aboutUnstartedConnectionTimeout < 0L) {
startedOrWarnedThatNotStarted = true;
} else {
long elapsedTime = System.currentTimeMillis() - connection.getTimeCreated();
// lets only warn when a significant amount of time has passed
// just in case its normal operation
if (elapsedTime > aboutUnstartedConnectionTimeout) {
LOG.warn("Received a message on a connection which is not yet started. Have you forgotten to call Connection.start()? Connection: " + connection
+ " Received: " + message);
startedOrWarnedThatNotStarted = true;
}
}
}
//如果会话不是异步分发且没有使用sessionpool分支,则调用dispatch发送消息
if (!session.isSessionAsyncDispatch() && !dispatchedBySessionPool) {
dispatch(message);
} else {
//将消息直接放入到队列里
messageQueue.enqueue(message);
wakeup();
}
}
默认是采用异步消息分发。所以,直接调用 messageQueue.enqueue,把消息放到队列中,并且调用 wakeup 方法 。
异步分发的流程
public void wakeup() {
//进一步验证
if (!dispatchedBySessionPool) {
//判断session是否为异步分发
if (session.isSessionAsyncDispatch()) {
try {
TaskRunner taskRunner = this.taskRunner;
if (taskRunner == null) {
synchronized (this) {
if (this.taskRunner == null) {
if (!isRunning()) {
// stop has been called
return;
}
//通过 TaskRunnerFactory 创建了一个任务运行类 taskRunner,这里把自己作为一
//个 task 传入到 createTaskRunner 中,说明当前
//的类一定是实现了 Task 接口的. 简单来说,就是通过线程池去执行一个任务,完 成
//异步调度,简单吧
this.taskRunner = session.connection.getSessionTaskRunner().createTaskRunner(this,
"ActiveMQ Session: " + session.getSessionId());
}
taskRunner = this.taskRunner;
}
}
taskRunner.wakeup();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else {
//同步分发
while (iterate()) {
}
}
}
}
所以,对于异步分发的方式,会调用 ActiveMQSessionExecutor 中的 iterate 方法,我们来看看这个方法的代码 。
iterate
这个方法里面做两个事
public boolean iterate() {
// Deliver any messages queued on the consumer to their listeners.
for (ActiveMQMessageConsumer consumer : this.session.consumers) {
if (consumer.iterate()) {
return true;
}
}
// No messages left queued on the listeners.. so now dispatch messages
// queued on the session
MessageDispatch message = messageQueue.dequeueNoWait();
if (message == null) {
return false;
} else {
dispatch(message);
return !messageQueue.isEmpty();
}
}
ActiveMQMessageConsumer.iterate
public boolean iterate() {
MessageListener listener = this.messageListener.get();
if (listener != null) {
MessageDispatch md = unconsumedMessages.dequeueNoWait();
if (md != null) {
dispatch(md);
return true;
}
}
return false;
}
同步分发的流程
同步分发的流程,直接调用 ActiveMQSessionExcutor 中的 dispatch 方法,代 码如下
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);
}
}
} else {
if (!unconsumedMessages.isRunning()) {
// delayed redelivery, ensure it can be re delivered
session.connection.rollbackDuplicate(this, md.getMessage());
}
if (md.getMessage() == null) {
// End of browse or pull request timeout.
unconsumedMessages.enqueue(md);
} else {
if (!consumeExpiredMessage(md)) {
unconsumedMessages.enqueue(md);
if (availableListener != null) {
availableListener.onMessageAvailable(this);
}
} else {
beforeMessageIsConsumed(md);
afterMessageIsConsumed(md, true);
// Pull consumer needs to check if pull timed out and send
// a new pull command if not.
if (info.getCurrentPrefetchSize() == 0) {
unconsumedMessages.enqueue(null);
}
}
}
}
} else {
// deal with duplicate delivery
ConsumerId consumerWithPendingTransaction;
if (redeliveryExpectedInCurrentTransaction(md, true)) {
LOG.debug("{} tracking transacted redelivery {}", getConsumerId(), md.getMessage());
if (transactedIndividualAck) {
immediateIndividualTransactedAck(md);
} else {
session.sendAck(new MessageAck(md, MessageAck.DELIVERED_ACK_TYPE, 1));
}
} else if ((consumerWithPendingTransaction = redeliveryPendingInCompetingTransaction(md)) != null) {
LOG.warn("{} delivering duplicate {}, pending transaction completion on {} will rollback", getConsumerId(), md.getMessage(), consumerWithPendingTransaction);
session.getConnection().rollbackDuplicate(this, md.getMessage());
dispatch(md);
} else {
LOG.warn("{} suppressing duplicate delivery on connection, poison acking: {}", getConsumerId(), md);
posionAck(md, "Suppressing duplicate delivery on connection, consumer " + getConsumerId());
}
}
}
}
if (++dispatchedCount % 1000 == 0) {
dispatchedCount = 0;
Thread.yield();
}
} catch (Exception e) {
session.connection.onClientInternalException(e);
}
}
到这里为止,消息如何接受以及他的处理方式的流程,我们已经搞清楚了,希望对大家理解 activeMQ 的核心机制有一定的帮助 。
消费端的 PrefetchSize
还记得我们在分析消费端的源码的时候,所讲到的 prefetchsize 吗?这个prefetchsize 是做什么的?我们接下来去研究一下
原理剖析
prefetchSize 的设置方法
在 createQueue 中添加 consumer.prefetchSize,就可以看到效果 Destination destination=session.createQueue("myQueue?consumer.prefetchSize=10");
既然有批量加载,那么一定有批量确认,这样才算是彻底的优化
optimizeAcknowledge
总结
消息的确认过程
ACK_MODE
通过前面的源码分析,基本上已经知道了消息的消费过程,以及消息的批量获 取和批量确认,那么接下来再了解下消息的确认过程。
消息确认有四种ACK_MODE,分别是:
虽然 Client 端指定了 ACK 模式,但是在 Client 与 broker 在交换 ACK 指令的时候,还需要告知 ACK_TYPE,ACK_TYPE 表示此确认指令的类型,不同的
ACK_TYPE 将传递着消息的状态,broker 可以根据不同的 ACK_TYPE 对消息进 行不同的操作。
ACK_TYPE
消息的重发机制原理
消息重发的情况
在正常情况下,有几中情况会导致消息重新发送 :
死信队列
ActiveMQ 中默认的死信队列是 ActiveMQ.DLQ,如果没有特别的配置,有毒的消息都会被发送到这个队列。默认情况下,如果持久消息过期以后,也会被 送到 DLQ 中。
死信队列配置策略
缺省所有队列的死信消息都被发送到同一个缺省死信队列,不便于管理,可以 通过 individualDeadLetterStrategy 或 sharedDeadLetterStrategy 策略来进 行修改 。
// “>”表示对所有队列生效,如果需要设置指定队列,则直接写队 列名称
//queuePrefix:设置死信队列前缀
//useQueueForQueueMessage 设置队列保存到死信。
自动丢弃过期消息
死信队列的再次消费
当定位到消息不能消费的原因后,就可以在解决掉这个问题之后,再次消费死 信队列中的消息。因为死信队列仍然是一个队列 。
ActiveMQ 静态网络配置
配置说明
修改 activeMQ 服务器的 activeMQ.xml, 增加如下配置
两个 Brokers 通过一个 static 的协议来进行网络连接。一个 Consumer 连接到BrokerB 的一个地址上,当 Producer 在 BrokerA 上以相同的地址发送消息
是,此时消息会被转移到 BrokerB 上,也就是说 BrokerA 会转发消息到BrokerB 上 。
消息回流
从 5.6 版本开始,在 destinationPolicy 上新增了一个选项replayWhenNoConsumers 属性,这个属性可以用来解决当 broker1 上有需要转发的消息但是没有消费者时,把消息回流到它原始的 broker。同时把enableAudit 设置为 false,为了防止消息回流后被当作重复消息而不被分发 通过如下配置,在 activeMQ.xml 中。 分别在两台服务器都配置。即可完成消 息回流处理 。
动态网络连接
配置
在三台机器上安装 activemq,通过三个实例组成集群。
修改配置
主从 复制协议
ActiveMQ 的优缺点
适用场景