Java進(jìn)階 消息中間件架構(gòu)體系 — ActiveMQ研究

一、前言

MQ全稱(chēng)為Message Queue,即消息隊(duì)列,它是一種應(yīng)用程序之間的通信方法,消息隊(duì)列在分布式系統(tǒng)開(kāi)發(fā)中應(yīng)用非常廣泛。開(kāi)發(fā)中消息隊(duì)列通常有如下應(yīng)用場(chǎng)景:

1、任務(wù)異步處理。將不需要同步處理的并且耗時(shí)長(zhǎng)的操作由消息隊(duì)列通知消息接收方進(jìn)行異步處理。提高了應(yīng)用程序的響應(yīng)時(shí)間。
2、應(yīng)用程序解耦合MQ相當(dāng)于一個(gè)中介,生產(chǎn)方通過(guò)MQ與消費(fèi)方交互,它將應(yīng)用程序進(jìn)行解耦合。市場(chǎng)上還有哪些消息隊(duì)列?ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ、Redis。我們主要介紹主流的消息中間件,了解每個(gè)MQ的優(yōu)缺點(diǎn),能知曉什么樣的場(chǎng)景下選用合適的MQ。

二、ActiveMQ

2.1介紹

ActiveMQ 是完全基于 JMS 規(guī)范實(shí)現(xiàn)的一個(gè)消息中間件產(chǎn)品。 是 Apache 開(kāi)源基金會(huì)研發(fā)的消息中間件。ActiveMQ主要應(yīng)用在分布式系統(tǒng)架構(gòu)中,幫助構(gòu)建高可用、 高性能、可伸縮的企業(yè)級(jí)面向消息服務(wù)的系統(tǒng)。

2.2什么是JMS

Java 消息服務(wù)(Java Message Service)是 java 平臺(tái)中關(guān)于面向消息中間件的 API,用于在兩個(gè)應(yīng)用程序之間,或者分布式系統(tǒng)中發(fā)送消息,進(jìn)行異步通信。JMS 是一個(gè)與具體平臺(tái)無(wú)關(guān)的 API ,絕大多數(shù) MOM(Message Oriented Middleware)(面向消息中間件)提供商都對(duì) JMS 提供了支持。例如ActiveMQ就是其中一個(gè)實(shí)現(xiàn)。

2.3什么是MOOM

MOM 是面向消息的中間件,使用消息傳送提供者來(lái)協(xié)調(diào)消息傳送操作。MOM 需要提供 API 和管理工具。客戶(hù)端使用 api 調(diào)用,把消息發(fā)送到由提供者管理的目的地。在發(fā)送消息之后,客戶(hù)端會(huì)繼續(xù)執(zhí)行其他工作,并且在接收方收到這個(gè)消息確認(rèn)之前,提供者一直保留該消息。

2.4 JMS 規(guī)范

我們已經(jīng)知道了 JMS 規(guī)范的目的是為了使得 Java 應(yīng)用程序能夠訪問(wèn)現(xiàn)有 MOM (消息中間件)系統(tǒng),形成一套統(tǒng)一的標(biāo)準(zhǔn)規(guī)范,解決不同消息中間件之間的協(xié)作問(wèn)題。在創(chuàng)建 JMS 規(guī)范時(shí),設(shè)計(jì)者希望能夠結(jié)合現(xiàn)有的消息傳送的精髓,比如說(shuō)

不同的消息傳送模式或域,例如點(diǎn)對(duì)點(diǎn)消息傳送和發(fā)布訂閱消息傳送
提供于接收同步和異步消息的工具
對(duì)可靠消息傳送的支持
常見(jiàn)消息格式,例如流、文本和字節(jié)

2.5JMS對(duì)象模型

1)連接工廠。連接工廠(ConnectionFactory)是由管理員創(chuàng)建,并綁定到JNDI樹(shù)中。客戶(hù)端使用JNDI查找連接工廠,然后利用連接工廠創(chuàng)建一個(gè)JMS連接。

2)JMS連接。JMS連接(Connection)表示JMS客戶(hù)端和服務(wù)器端之間的一個(gè)活動(dòng)的連接,是由客戶(hù)端通過(guò)調(diào)用連接工廠的方法建立的。

3)JMS會(huì)話。JMS會(huì)話(Session)表示JMS客戶(hù)與JMS服務(wù)器之間的會(huì)話狀態(tài)。JMS會(huì)話建立在JMS連接上,表示客戶(hù)與服務(wù)器之間的一個(gè)會(huì)話線程。

4)JMS目的。JMS目的(Destination),又稱(chēng)為消息隊(duì)列,是實(shí)際的消息源。

5)JMS生產(chǎn)者和消費(fèi)者。生產(chǎn)者(Message Producer)和消費(fèi)者(Message Consumer)對(duì)象由Session對(duì)象創(chuàng)建,用于發(fā)送和接收消息。

6)JMS消息通常有兩種類(lèi)型:

① 點(diǎn)對(duì)點(diǎn)(Point-to-Point)。在點(diǎn)對(duì)點(diǎn)的消息系統(tǒng)中,消息分發(fā)給一個(gè)單獨(dú)的使用者。點(diǎn)對(duì)點(diǎn)消息往往與隊(duì)列(javax.jms.Queue)相關(guān)聯(lián)。
② 發(fā)布/訂閱(Publish/Subscribe)。發(fā)布/訂閱消息系統(tǒng)支持一個(gè)事件驅(qū)動(dòng)模型,消息生產(chǎn)者和消費(fèi)者都參與消息的傳遞。生產(chǎn)者發(fā)布事件,而使用者訂閱感興趣的事件,并使用事件。該類(lèi)型消息一般與特定的主題(javax.jms.Topic)關(guān)聯(lián)。

在這里插入圖片描述

2.6安裝ActiveMQ

windows安裝

下載地址:http://activemq.apache.org/activemq-5150-release.html

下載完成后解壓進(jìn)入bin目錄 運(yùn)行 activemq.bat。

如果你遇到如下問(wèn)題,5672端口被占用

在這里插入圖片描述

可以去修改activemq的conf目錄下的activemq.xml,把a(bǔ)mqp的端口改為其他的,這里改成了5673
在這里插入圖片描述

再次啟動(dòng):
在這里插入圖片描述

訪問(wèn)地址:http://127.0.0.1:8161/admin/進(jìn)入后臺(tái)頁(yè)面 初始賬號(hào)密碼 admin admin
在這里插入圖片描述

Docker安裝ActiveMQ

docker run -d --name activemq -p 61616:61616 -p 8161:8161 webcenter/activemq

2.7ActiveMQ快速入門(mén)

Springboot集成ActiveMQ

導(dǎo)入依賴(lài)

    <dependencies>
        <!--Springboot-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.3.0.RELEASE</version>
        </dependency>
        <!--ActiveMq-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-activemq</artifactId>
            <version>1.5.0.RELEASE</version>
        </dependency>
        <!--消息隊(duì)列連接池-->
        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>activemq-pool</artifactId>
            <version>5.15.0</version>
        </dependency>
    </dependencies>

配置MQ

server:
  port: 8080

spring:
  activemq:
    broker-url: tcp://127.0.0.1:61616
    user: admin
    password: admin
    close-timeout: 15s   # 在考慮結(jié)束之前等待的時(shí)間
    in-memory: true      # 默認(rèn)代理URL是否應(yīng)該在內(nèi)存中。如果指定了顯式代理,則忽略此值。
    non-blocking-redelivery: false  # 是否在回滾回滾消息之前停止消息傳遞。這意味著當(dāng)啟用此命令時(shí),消息順序不會(huì)被保留。
    send-timeout: 0     # 等待消息發(fā)送響應(yīng)的時(shí)間。設(shè)置為0等待永遠(yuǎn)。
    queue-name: active.queue
    topic-name: active.topic.name.model
    #  packages:
    #    trust-all: true #不配置此項(xiàng),會(huì)報(bào)錯(cuò)
    pool:
      enabled: true
      max-connections: 10   #連接池最大連接數(shù)
      idle-timeout: 30000   #空閑的連接過(guò)期時(shí)間,默認(rèn)為30秒
    # jms:
    #   pub-sub-domain: true  #默認(rèn)情況下activemq提供的是queue模式,若要使用topic模式需要配置下面配置

# 是否信任所有包
#spring.activemq.packages.trust-all=
# 要信任的特定包的逗號(hào)分隔列表(當(dāng)不信任所有包時(shí))
#spring.activemq.packages.trusted=
# 當(dāng)連接請(qǐng)求和池滿(mǎn)時(shí)是否阻塞。設(shè)置false會(huì)拋“JMSException異常”。
#spring.activemq.pool.block-if-full=true
# 如果池仍然滿(mǎn),則在拋出異常前阻塞時(shí)間。
#spring.activemq.pool.block-if-full-timeout=-1ms
# 是否在啟動(dòng)時(shí)創(chuàng)建連接。可以在啟動(dòng)時(shí)用于加熱池。
#spring.activemq.pool.create-connection-on-startup=true
# 是否用Pooledconnectionfactory代替普通的ConnectionFactory。
#spring.activemq.pool.enabled=false
# 連接過(guò)期超時(shí)。
#spring.activemq.pool.expiry-timeout=0ms
# 連接空閑超時(shí)
#spring.activemq.pool.idle-timeout=30s
# 連接池最大連接數(shù)
#spring.activemq.pool.max-connections=1
# 每個(gè)連接的有效會(huì)話的最大數(shù)目。
#spring.activemq.pool.maximum-active-session-per-connection=500
# 當(dāng)有"JMSException"時(shí)嘗試重新連接
#spring.activemq.pool.reconnect-on-exception=true
# 在空閑連接清除線程之間運(yùn)行的時(shí)間。當(dāng)為負(fù)數(shù)時(shí),沒(méi)有空閑連接驅(qū)逐線程運(yùn)行。
#spring.activemq.pool.time-between-expiration-check=-1ms
# 是否只使用一個(gè)MessageProducer
#spring.activemq.pool.use-anonymous-producers=true

編寫(xiě)配置類(lèi)

/**
 * @author 原
 * @date 2020/12/16
 * @since 1.0
 **/
@Configuration
public class BeanConfig {
    @Value("${spring.activemq.broker-url}")
    private String brokerUrl;

    @Value("${spring.activemq.user}")
    private String username;

    @Value("${spring.activemq.topic-name}")
    private String password;

    @Value("${spring.activemq.queue-name}")
    private String queueName;

    @Value("${spring.activemq.topic-name}")
    private String topicName;

    @Bean(name = "queue")
    public Queue queue() {
        return new ActiveMQQueue(queueName);
    }

    @Bean(name = "topic")
    public Topic topic() {
        return new ActiveMQTopic(topicName);
    }

    @Bean
    public ConnectionFactory connectionFactory(){
        return new ActiveMQConnectionFactory(username, password, brokerUrl);
    }

    @Bean
    public JmsMessagingTemplate jmsMessageTemplate(){
        return new JmsMessagingTemplate(connectionFactory());
    }

    /**
     * 在Queue模式中,對(duì)消息的監(jiān)聽(tīng)需要對(duì)containerFactory進(jìn)行配置
     * @param connectionFactory
     * @return
     */
    @Bean("queueListener")
    public JmsListenerContainerFactory<?> queueJmsListenerContainerFactory(ConnectionFactory connectionFactory){
        SimpleJmsListenerContainerFactory factory = new SimpleJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setPubSubDomain(false);
        return factory;
    }

    /**
     * 在Topic模式中,對(duì)消息的監(jiān)聽(tīng)需要對(duì)containerFactory進(jìn)行配置
     * @param connectionFactory
     * @return
     */
    @Bean("topicListener")
    public JmsListenerContainerFactory<?> topicJmsListenerContainerFactory(ConnectionFactory connectionFactory){
        SimpleJmsListenerContainerFactory factory = new SimpleJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setPubSubDomain(true);
        return factory;
    }
}

編寫(xiě)啟動(dòng)類(lèi)

/**
 * @author 原
 * @date 2020/12/8
 * @since 1.0
 **/
@SpringBootApplication
@EnableJms //開(kāi)啟JMS支持
public class DemoApplication {

    @Autowired
    private JmsMessagingTemplate jmsMessagingTemplate;

    @Autowired
    private Queue queue;

    @Autowired
    private Topic topic;

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    /**
     * 應(yīng)用啟動(dòng)后,會(huì)執(zhí)行該方法
     * 會(huì)分別向queue和topic發(fā)送一條消息
     */
    @PostConstruct
    public void sendMsg(){
        jmsMessagingTemplate.convertAndSend(queue,"queue-test");
        jmsMessagingTemplate.convertAndSend(topic,"topic-test");
    }
}

查看activemq后臺(tái)


在這里插入圖片描述

active.queue 為隊(duì)列的名稱(chēng)
Number Of Pending Messages 等待消費(fèi)的消息數(shù)量 3是因?yàn)槲易约喊l(fā)了3次
Messages Enqueued 已經(jīng)進(jìn)入隊(duì)列的消息數(shù)量

在這里插入圖片描述

因?yàn)闆](méi)有消費(fèi)者,消息一直沒(méi)有被消費(fèi)。下面我們編寫(xiě)消費(fèi)者代碼。

/**
 * @author 原
 * @date 2020/12/16
 * @since 1.0
 **/
@Component
public class QueueConsumerListener {

    @JmsListener(destination = "${spring.activemq.queue-name}",containerFactory = "queueListener")
    public void getQueue(String message){
        System.out.println("接受queue:"+message);
    }

    @JmsListener(destination = "${spring.activemq.topic-name}",containerFactory = "topicListener")
    public void getTopic(String message){
        System.out.println("接受topic:"+message);
    }
}

在后臺(tái)發(fā)送一條消息


在這里插入圖片描述

控制臺(tái)打印


在這里插入圖片描述

發(fā)送topic消息
在這里插入圖片描述

控制臺(tái)打印:


在這里插入圖片描述

但是發(fā)現(xiàn)一個(gè)問(wèn)題是,之前在沒(méi)有消費(fèi)的時(shí)候,有3條queue和一條topic,但是當(dāng)我啟動(dòng)消費(fèi)者時(shí),queue的3條消息被消費(fèi)了,topic確沒(méi)有。這是因?yàn)椋?

topic模式有普通訂閱和持久化訂閱

普通訂閱:在消費(fèi)者啟動(dòng)之前發(fā)送過(guò)來(lái)的消息,消費(fèi)者啟動(dòng)之后不會(huì)去消費(fèi);

持久化訂閱: 在消費(fèi)者啟動(dòng)之前發(fā)送過(guò)來(lái)的消息,消費(fèi)者啟動(dòng)之后會(huì)去消費(fèi);

2.8ActiveMQ原理分析

消息同步發(fā)送與異步發(fā)送

ActiveMQ支持同步、異步兩種發(fā)送模式將消息發(fā)送到broker上。
同步發(fā)送過(guò)程中,發(fā)送者發(fā)送一條消息會(huì)阻塞直到broker反饋一個(gè)確認(rèn)消息,表示消息已經(jīng)被broker處理。
這個(gè)機(jī)制提供了消息的安全性保障,但是由于是阻塞的操作,會(huì)影響到客戶(hù)端消息發(fā)送的性能異步發(fā)送的過(guò)程中,發(fā)送者不需要等待broker提供反饋,所以性能相對(duì)較高。但是可能會(huì)出現(xiàn)消息丟失的情況。
所以使用異步發(fā)送的前提是在某些情況下允許出現(xiàn)數(shù)據(jù)丟失的情況。
默認(rèn)情況下,非持久化消息是異步發(fā)送的,持久化消息并且是在非事務(wù)模式下是同步發(fā)送的。
但是在開(kāi)啟事務(wù)的情況下,消息都是異步發(fā)送。由于異步發(fā)送的效率會(huì)比同步發(fā)送性能更高。所以在發(fā)送持久化消息的時(shí)候,盡量去開(kāi)啟事務(wù)會(huì)話。

消息發(fā)送原理

在這里插入圖片描述

ProducerWindowSize的含義:

producer每發(fā)送一個(gè)消息,統(tǒng)計(jì)一下發(fā)送的字節(jié)數(shù),當(dāng)字節(jié)數(shù)達(dá)到ProducerWindowSize值時(shí),需要等待broker的確認(rèn),才能繼續(xù)發(fā)送。

代碼在:

ActiveMQSession的1957行主要用來(lái)約束在異步發(fā)送時(shí)producer端允許積壓的(尚未ACK)的消息的大小,且只對(duì)異步發(fā)送有意義。每次發(fā)送消息之后,都將會(huì)導(dǎo)致memoryUsage大小增加(+message.size),當(dāng)broker返回producerAck時(shí),memoryUsage尺寸減少(producerAck.size,此size表示先前發(fā)送消息的大小)。

可以通過(guò)如下2種方式設(shè)置:

? 在brokerUrl中設(shè)置: "tcp://localhost:61616?jms.producerWindowSize=1048576",這種設(shè)置將會(huì)對(duì)所有的
producer生效。
? 在destinationUri中設(shè)置: "test-queue?producer.windowSize=1048576",此參數(shù)只會(huì)對(duì)使用此Destination實(shí)例
的producer失效,將會(huì)覆蓋brokerUrl中的producerWindowSize值。

注意:此值越大,意味著消耗Client端的內(nèi)存就越大。

源碼分析

ActiveMQMessageProducer.send(...)方法

public void send(Destination destination, Message message, int deliveryMode, int priority, long timeToLive, AsyncCallback onComplete) throws JMSException {
        checkClosed();//檢查session連接,若已關(guān)閉直接拋出異常
        if (destination == null) {//校驗(yàn)發(fā)送消息的目的地是否為空,也就是必須制定queue或者topic信息
            if (info.getDestination() == null) {
                throw new UnsupportedOperationException("A destination must be specified.");
            }
            throw new InvalidDestinationException("Don't understand null destinations");
        }

        //這里做的是封裝Destination
        ActiveMQDestination dest;
        if (destination.equals(info.getDestination())) {
            dest = (ActiveMQDestination)destination;
        } else if (info.getDestination() == null) {
            dest = ActiveMQDestination.transform(destination);
        } else {
            throw new UnsupportedOperationException("This producer can only send messages to: " + this.info.getDestination().getPhysicalName());
        }
        if (dest == null) {
            throw new JMSException("No destination specified");
        }
        //封裝Message
        if (transformer != null) {
            Message transformedMessage = transformer.producerTransform(session, this, message);
            if (transformedMessage != null) {
                message = transformedMessage;
            }
        }
        //如果設(shè)置了producerWindow,則需要校驗(yàn)producerWindow大小
        if (producerWindow != null) {
            try {
                producerWindow.waitForSpace();
            } catch (InterruptedException e) {
                throw new JMSException("Send aborted due to thread interrupt.");
            }
        }
        //發(fā)送消息
        this.session.send(this, dest, message, deliveryMode, priority, timeToLive, producerWindow, sendTimeout, onComplete);
        //做統(tǒng)計(jì)的
        stats.onMessage();
    }

ActiveMQSession的send方法

protected void send(ActiveMQMessageProducer producer, ActiveMQDestination destination, Message message, int deliveryMode, int priority, long timeToLive,
                        MemoryUsage producerWindow, int sendTimeout, AsyncCallback onComplete) throws JMSException {
        //校驗(yàn)連接  
        checkClosed();
        //校驗(yàn)發(fā)送目標(biāo)
        if (destination.isTemporary() && connection.isDeleted(destination)) {
            throw new InvalidDestinationException("Cannot publish to a deleted Destination: " + destination);
        }
        //互斥鎖,如果一個(gè)session的多個(gè)producer發(fā)送消息到這里,會(huì)保證消息發(fā)送的有序性
        synchronized (sendMutex) {
            // tell the Broker we are about to start a new transaction
            doStartTransaction();
            TransactionId txid = transactionContext.getTransactionId();
            long sequenceNumber = producer.getMessageSequence();

            //Set the "JMS" header fields on the original message, see 1.1 spec section 3.4.11
            message.setJMSDeliveryMode(deliveryMode);//設(shè)置是否持久化
            long expiration = 0L;
            if (!producer.getDisableMessageTimestamp()) {
                long timeStamp = System.currentTimeMillis();
                message.setJMSTimestamp(timeStamp);
                if (timeToLive > 0) {
                    expiration = timeToLive + timeStamp;
                }
            }
            message.setJMSExpiration(expiration);//消息過(guò)期時(shí)間
            message.setJMSPriority(priority);//消息優(yōu)先級(jí)
            message.setJMSRedelivered(false);//是否重復(fù)發(fā)送

            // transform to our own message format here 統(tǒng)一封裝
            ActiveMQMessage msg = ActiveMQMessageTransformation.transformMessage(message, connection);
            msg.setDestination(destination);
            //設(shè)置消息ID
            msg.setMessageId(new MessageId(producer.getProducerInfo().getProducerId(), sequenceNumber));

            // Set the message id.
            if (msg != message) {//如果消息是經(jīng)過(guò)轉(zhuǎn)化的,則更新原來(lái)的消息id和目的地
                message.setJMSMessageID(msg.getMessageId().toString());
                // Make sure the JMS destination is set on the foreign messages too.
                message.setJMSDestination(destination);
            }
            //clear the brokerPath in case we are re-sending this message
            msg.setBrokerPath(null);

            msg.setTransactionId(txid);
            if (connection.isCopyMessageOnSend()) {
                msg = (ActiveMQMessage)msg.copy();
            }
            msg.setConnection(connection);
            msg.onSend();//把消息屬性和消息體都設(shè)置為只讀,防止被修改
            msg.setProducerId(msg.getMessageId().getProducerId());
            if (LOG.isTraceEnabled()) {
                LOG.trace(getSessionId() + " sending message: " + msg);
            }
            //如果onComplete沒(méi)有設(shè)置,且發(fā)送超時(shí)時(shí)間小于0,且消息不需要反饋,且連接器不是同步發(fā)送模式,且消息非持久化或者連接器是異步發(fā)送模式
            //或者存在事務(wù)id的情況下,走異步發(fā)送,否則走同步發(fā)送
            if (onComplete==null && sendTimeout <= 0 && !msg.isResponseRequired() && !connection.isAlwaysSyncSend() && (!msg.isPersistent() || connection.isUseAsyncSend() || txid != null)) {
                this.connection.asyncSendPacket(msg);
                if (producerWindow != null) {
                    // Since we defer lots of the marshaling till we hit the
                    // wire, this might not
                    // provide and accurate size. We may change over to doing
                    // more aggressive marshaling,
                    // to get more accurate sizes.. this is more important once
                    // users start using producer window
                    // flow control.
                    int size = msg.getSize();//異步發(fā)送的情況下,需要設(shè)置producerWindow的大小
                    producerWindow.increaseUsage(size);
                }
            } else {
                if (sendTimeout > 0 && onComplete==null) {
                    this.connection.syncSendPacket(msg,sendTimeout);//帶超時(shí)時(shí)間的同步發(fā)送//帶回調(diào)的同步發(fā)送
                }else {
                    this.connection.syncSendPacket(msg, onComplete);//帶回調(diào)的同步發(fā)送
                }
            }

        }
    }

看下異步發(fā)送的代碼ActiveMQConnection. asyncSendPacket()

 /**
     * send a Packet through the Connection - for internal use only
     *
     * @param command
     * @throws JMSException
     */
    public void asyncSendPacket(Command command) throws JMSException {
        if (isClosed()) {
            throw new ConnectionClosedException();
        } else {
            doAsyncSendPacket(command);
        }
    }

    private void doAsyncSendPacket(Command command) throws JMSException {
        try {
            this.transport.oneway(command);
        } catch (IOException e) {
            throw JMSExceptionSupport.create(e);
        }
    }

再看看transport是個(gè)什么東西?在哪里實(shí)例化的?按照以前看源碼的慣例來(lái)看,它肯定不是一個(gè)單純的對(duì)象。按照以往我看源碼的經(jīng)驗(yàn)來(lái)看,一定是在創(chuàng)建連接的過(guò)程中初始化的。所以我們定位到代碼

//從connection=connectionFactory.createConnection();這行代碼作為入口,一直跟蹤ActiveMQConnectionFactory. createActiveMQConnection這個(gè)方法中。代碼如下

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);
//省略后面的代碼
}
//這個(gè)方法就是實(shí)例化Transport的 1.構(gòu)建Broker的URL 2.根據(jù)這個(gè)URL去創(chuàng)建一個(gè)鏈接TransportFactory.connect 默認(rèn)使用的TCP連接
 protected Transport createTransport() throws JMSException {
        try {
            URI connectBrokerUL = brokerURL;
            String scheme = brokerURL.getScheme();
            if (scheme == null) {
                throw new IOException("Transport not scheme specified: [" + brokerURL + "]");
            }
            if (scheme.equals("auto")) {
                connectBrokerUL = new URI(brokerURL.toString().replace("auto", "tcp"));
            } else if (scheme.equals("auto+ssl")) {
                connectBrokerUL = new URI(brokerURL.toString().replace("auto+ssl", "ssl"));
            } else if (scheme.equals("auto+nio")) {
                connectBrokerUL = new URI(brokerURL.toString().replace("auto+nio", "nio"));
            } else if (scheme.equals("auto+nio+ssl")) {
                connectBrokerUL = new URI(brokerURL.toString().replace("auto+nio+ssl", "nio+ssl"));
            }

            return TransportFactory.connect(connectBrokerUL);//里面的代碼繼續(xù)往下看
        } catch (Exception e) {
            throw JMSExceptionSupport.create("Could not create Transport. Reason: " + e, e);
        }
    }    

TransportFactory. findTransportFactory**

1.從TRANSPORT_FACTORYS這個(gè)Map集合中,根據(jù)scheme去獲得一個(gè)TransportFactory指定的實(shí)例對(duì)象
2.如果Map集合中不存在,則通過(guò)TRANSPORT_FACTORY_FINDER去找一個(gè)并且構(gòu)建實(shí)例
? 這個(gè)地方又有點(diǎn)類(lèi)似于我們之前所學(xué)過(guò)的SPI的思想吧?他會(huì)從METAINF/services/org/apache/activemq/transport/ 這個(gè)路徑下,根據(jù)URI組裝的scheme去找到匹配class對(duì)象并且
實(shí)例化,所以根據(jù)tcp為key去對(duì)應(yīng)的路徑下可以找到T cpT ransportFactory

 //TransportFactory.connect(connectBrokerUL)
    public static Transport connect(URI location) throws Exception {
        TransportFactory tf = findTransportFactory(location);
        return tf.doConnect(location);
    }
    
    //findTransportFactory(location)
        public static TransportFactory findTransportFactory(URI location) throws IOException {
        String scheme = location.getScheme();
        if (scheme == null) {
            throw new IOException("Transport not scheme specified: [" + location + "]");
        }
        TransportFactory tf = TRANSPORT_FACTORYS.get(scheme);
        if (tf == null) {
            // Try to load if from a META-INF property.
            try {
                tf = (TransportFactory)TRANSPORT_FACTORY_FINDER.newInstance(scheme);
                TRANSPORT_FACTORYS.put(scheme, tf);
            } catch (Throwable e) {
                throw IOExceptionSupport.create("Transport scheme NOT recognized: [" + scheme + "]", e);
            }
        }
        return tf;
    }

調(diào)用TransportFactory.doConnect去構(gòu)建一個(gè)連接

    public Transport doConnect(URI location) throws Exception {
        try {
            Map<String, String> options = new HashMap<String, String>(URISupport.parseParameters(location));
            if( !options.containsKey("wireFormat.host") ) {
                options.put("wireFormat.host", location.getHost());
            }
            WireFormat wf = createWireFormat(options);
            Transport transport = createTransport(location, wf);
            Transport rc = configure(transport, wf, options);
            //remove auto
            IntrospectionSupport.extractProperties(options, "auto.");

            if (!options.isEmpty()) {
                throw new IllegalArgumentException("Invalid connect parameters: " + options);
            }
            return rc;
        } catch (URISyntaxException e) {
            throw IOExceptionSupport.create(e);
        }
    }

configure

    public Transport configure(Transport transport, WireFormat wf, Map options) throws Exception {
        //組裝一個(gè)復(fù)合的transport,這里會(huì)包裝兩層,一個(gè)是IactivityMonitor.另一個(gè)是WireFormatNegotiator
        transport = compositeConfigure(transport, wf, options);

        transport = new MutexTransport(transport);//再做一層包裝,MutexTransport
        transport = new ResponseCorrelator(transport);//包裝ResponseCorrelator

        return transport;
    }

到目前為止,這個(gè)transport實(shí)際上就是一個(gè)調(diào)用鏈了,他的鏈結(jié)構(gòu)為
ResponseCorrelator(MutexT ransport(WireFormatNegotiator(IactivityMonitor(T cpT ransport()))
每一層包裝表示什么意思呢?
ResponseCorrelator 用于實(shí)現(xiàn)異步請(qǐng)求。
MutexT ransport 實(shí)現(xiàn)寫(xiě)鎖,表示同一時(shí)間只允許發(fā)送一個(gè)請(qǐng)求
WireFormatNegotiator 實(shí)現(xiàn)了客戶(hù)端連接broker的時(shí)候先發(fā)送數(shù)據(jù)解析相關(guān)的協(xié)議信息,比如解析版本號(hào),是否
使用緩存等
InactivityMonitor 用于實(shí)現(xiàn)連接成功成功后的心跳檢查機(jī)制,客戶(hù)端每10s發(fā)送一次心跳信息。服務(wù)端每30s讀取
一次心跳信息。

同步發(fā)送和異步發(fā)送的區(qū)別

public Object request(Object command, int timeout) throws IOException {
FutureResponse response = asyncRequest(command, null);
return response.getResult(timeout); // 從future方法阻塞等待返回
}

持久化消息與非持久化消息的存儲(chǔ)消息

正常情況下,非持久化消息是存儲(chǔ)在內(nèi)存中的,持久化消息是存儲(chǔ)在文件中的。能夠存儲(chǔ)的最大消息數(shù)據(jù)在
${ActiveMQ_HOME}/conf/activemq.xml文件中的systemUsage節(jié)點(diǎn)
SystemUsage配置設(shè)置了一些系統(tǒng)內(nèi)存和硬盤(pán)容量

<systemUsage>
<systemUsage>
<memoryUsage>
//該子標(biāo)記設(shè)置整個(gè)ActiveMQ節(jié)點(diǎn)的“可用內(nèi)存限制”。這個(gè)值不能超過(guò)ActiveMQ本身設(shè)置的最大內(nèi)存大小。其中的
percentOfJvmHeap屬性表示百分比。占用70%的堆內(nèi)存
<memoryUsage percentOfJvmHeap="70" />
</memoryUsage>
<storeUsage>
//該標(biāo)記設(shè)置整個(gè)ActiveMQ節(jié)點(diǎn),用于存儲(chǔ)“持久化消息”的“可用磁盤(pán)空間”。該子標(biāo)記的limit屬性必須要進(jìn)行設(shè)置
<storeUsage limit="100 gb"/>
</storeUsage>
<tempUsage>
//一旦ActiveMQ服務(wù)節(jié)點(diǎn)存儲(chǔ)的消息達(dá)到了memoryUsage的限制,非持久化消息就會(huì)被轉(zhuǎn)儲(chǔ)到 temp store區(qū)域,雖然
我們說(shuō)過(guò)非持久化消息不進(jìn)行持久化存儲(chǔ),但是ActiveMQ為了防止“數(shù)據(jù)洪峰”出現(xiàn)時(shí)非持久化消息大量堆積致使內(nèi)存耗
盡的情況出現(xiàn),還是會(huì)將非持久化消息寫(xiě)入到磁盤(pán)的臨時(shí)區(qū)域——temp store。這個(gè)子標(biāo)記就是為了設(shè)置這個(gè)temp
store區(qū)域的“可用磁盤(pán)空間限制”
<tempUsage limit="50 gb"/>
</tempUsage>
</systemUsage>
</systemUsage>

從上面的配置我們需要get到一個(gè)結(jié)論,當(dāng)非持久化消息堆積到一定程度的時(shí)候,也就是內(nèi)存超過(guò)指定的設(shè)置閥值時(shí),ActiveMQ會(huì)將內(nèi)存中的非持久化消息寫(xiě)入到臨時(shí)文件,以便騰出內(nèi)存。但是它和持久化消息的區(qū)別是,重啟之后,持久化消息會(huì)從文件中恢復(fù),非持久化的臨時(shí)文件會(huì)直接刪除

消息的持久化策略分析

消息持久性對(duì)于可靠消息傳遞來(lái)說(shuō)是一種比較好的方法,即時(shí)發(fā)送者和接受者不是同時(shí)在線或者消息中心在發(fā)送者發(fā)送消息后宕機(jī)了,在消息中心重啟后仍然可以將消息發(fā)送出去。消息持久性的原理很簡(jiǎn)單,就是在發(fā)送消息出去后,消息中心首先將消息存儲(chǔ)在本地文件、內(nèi)存或者遠(yuǎn)程數(shù)據(jù)庫(kù),然后把消息發(fā)送給接受者,發(fā)送成功后再把消息從存儲(chǔ)中刪除,失敗則繼續(xù)嘗試。接下來(lái)我們來(lái)了解一下消息在broker上的持久化存儲(chǔ)實(shí)現(xiàn)方式

持久化存儲(chǔ)支持類(lèi)型

ActiveMQ支持多種不同的持久化方式,主要有以下幾種,不過(guò),無(wú)論使用哪種持久化方式,消息的存儲(chǔ)邏輯都是一致的。

? KahaDB存儲(chǔ)(默認(rèn)存儲(chǔ)方式)
? JDBC存儲(chǔ)
? Memory存儲(chǔ)
? LevelDB存儲(chǔ)
? JDBC With ActiveMQ Journal

KahaDB存儲(chǔ)

KahaDB是目前默認(rèn)的存儲(chǔ)方式,可用于任何場(chǎng)景,提高了性能和恢復(fù)能力。消息存儲(chǔ)使用一個(gè)事務(wù)日志和僅僅用一個(gè)索引文件來(lái)存儲(chǔ)它所有的地址。
KahaDB是一個(gè)專(zhuān)門(mén)針對(duì)消息持久化的解決方案,它對(duì)典型的消息使用模式進(jìn)行了優(yōu)化。在Kaha中,數(shù)據(jù)被追加到data logs中。當(dāng)不再需要log文件中的數(shù)據(jù)的時(shí)候,log文件會(huì)被丟棄。

配置方式

<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>

KahaDB的存儲(chǔ)原理
在data/kahadb這個(gè)目錄下,會(huì)生成四個(gè)文件

? db.data 它是消息的索引文件,本質(zhì)上是B-Tree(B樹(shù)),使用B-Tree作為索引指向db-.log里面存儲(chǔ)的消息
? db.redo 用來(lái)進(jìn)行消息恢復(fù)
? db-.log 存儲(chǔ)消息內(nèi)容。新的數(shù)據(jù)以APPEND的方式追加到日志文件末尾。屬于順序?qū)懭耄虼讼⒋鎯?chǔ)是比較
快的。默認(rèn)是32M,達(dá)到閥值會(huì)自動(dòng)遞增
? lock文件 鎖,表示當(dāng)前獲得kahadb讀寫(xiě)權(quán)限的broker

JDBC存儲(chǔ)

使用JDBC持久化方式,數(shù)據(jù)庫(kù)會(huì)創(chuàng)建3個(gè)表:activemq_msgs,activemq_acks和activemq_lock。
ACTIVEMQ_MSGS 消息表,queue和topic都存在這個(gè)表中
ACTIVEMQ_ACKS 存儲(chǔ)持久訂閱的信息和最后一個(gè)持久訂閱接收的消息ID
ACTIVEMQ_LOCKS 鎖表,用來(lái)確保某一時(shí)刻,只能有一個(gè)ActiveMQ broker實(shí)例來(lái)訪問(wèn)數(shù)據(jù)庫(kù)

JDBC存儲(chǔ)配置

<persistenceAdapter>
<jdbcPersistenceAdapter dataSource="# MySQL-DS " createTablesOnStartup="true" />
</persistenceAdapter>

dataSource指定持久化數(shù)據(jù)庫(kù)的bean,createT ablesOnStartup是否在啟動(dòng)的時(shí)候創(chuàng)建數(shù)據(jù)表,默認(rèn)值是true,這樣每次啟動(dòng)都會(huì)去創(chuàng)建數(shù)據(jù)表了,一般是第一次啟動(dòng)的時(shí)候設(shè)置為true,之后改成false
Mysql持久化Bean配置

<bean id="Mysql-DS" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.11.156:3306/activemq?
relaxAutoCommit=true"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>

LevelDB存儲(chǔ)

LevelDB持久化性能高于KahaDB,雖然目前默認(rèn)的持久化方式仍然是KahaDB。并且,在ActiveMQ 5.9版本提供
了基于LevelDB和Zookeeper的數(shù)據(jù)復(fù)制方式,用于Master-slave方式的首選數(shù)據(jù)復(fù)制方案。
不過(guò),據(jù)ActiveMQ官網(wǎng)對(duì)LevelDB的表述:LevelDB官方建議使用以及不再支持,推薦使用的是KahaDB

<persistenceAdapter>
<levelDBdirectory="activemq-data"/>
</persistenceAdapter>

Memory 消息存儲(chǔ)

基于內(nèi)存的消息存儲(chǔ),內(nèi)存消息存儲(chǔ)主要是存儲(chǔ)所有的持久化的消息在內(nèi)存中。persistent=”false”,表示不設(shè)置持久化存儲(chǔ),直接存儲(chǔ)到內(nèi)存中

<beans>
<broker brokerName="test-broker" persistent="false"
xmlns="http://activemq.apache.org/schema/core">
<transportConnectors>
<transportConnector uri="tcp://localhost:61635"/>
</transportConnectors> </broker>
</beans>

JDBC Message store with ActiveMQ Journal

這種方式克服了JDBC Store的不足,JDBC每次消息過(guò)來(lái),都需要去寫(xiě)庫(kù)和讀庫(kù)。
ActiveMQ Journal,使用高速緩存寫(xiě)入技術(shù),大大提高了性能。
當(dāng)消費(fèi)者的消費(fèi)速度能夠及時(shí)跟上生產(chǎn)者消息的生產(chǎn)速度時(shí),journal文件能夠大大減少需要寫(xiě)入到DB中的消息。
舉個(gè)例子,生產(chǎn)者生產(chǎn)了1000條消息,這1000條消息會(huì)保存到j(luò)ournal文件,如果消費(fèi)者的消費(fèi)速度很快的情況
下,在journal文件還沒(méi)有同步到DB之前,消費(fèi)者已經(jīng)消費(fèi)了90%的以上的消息,那么這個(gè)時(shí)候只需要同步剩余的
10%的消息到DB。
如果消費(fèi)者的消費(fèi)速度很慢,這個(gè)時(shí)候journal文件可以使消息以批量方式寫(xiě)到DB。
? 將原來(lái)的標(biāo)簽注釋掉
? 添加如下標(biāo)簽

<persistenceFactory>
<journalPersistenceAdapterFactory  dataSource="#Mysql-DS" dataDirectory="activemqdata"/>
</persistenceFactory>

? 在服務(wù)端循環(huán)發(fā)送消息。可以看到數(shù)據(jù)是延遲同步到數(shù)據(jù)庫(kù)的

消費(fèi)端消費(fèi)東西的原理

我們知道有兩種方法可以接收消息,一種是使用同步阻塞的MessageConsumer#receive方法。另一種是使用消息監(jiān)聽(tīng)器MessageListener。這里需要注意的是,在同一個(gè)session下,這兩者不能同時(shí)工作,也就是說(shuō)不能針對(duì)不同消息采用不同的接收方式。否則會(huì)拋出異常。
至于為什么這么做,最大的原因還是在事務(wù)性會(huì)話中,兩種消費(fèi)模式的事務(wù)不好管控

消費(fèi)流程圖


在這里插入圖片描述

ActiveMQMessageConsumer.receive消費(fèi)端同步接收消息的源碼入口

public Message receive() throws JMSException {
    checkClosed();
    checkMessageListener();  //檢查receive和MessageListener是否同時(shí)配置在當(dāng)前的會(huì)話中,同步消費(fèi)不需要設(shè)置MessageListener 否則會(huì)報(bào)錯(cuò)
    sendPullCommand(0); //如果PrefetchSizeSize為0并且unconsumerMessage為空,則發(fā)起pull命令
    MessageDispatch md = dequeue(-1); //從unconsumerMessage出隊(duì)列獲取消息
    if (md == null) {
        return null;
    }
    beforeMessageIsConsumed(md);
    afterMessageIsConsumed(md, false); //發(fā)送ack給到broker
    return createActiveMQMessage(md);//獲取消息并返回
}

sendPullCommand

發(fā)送pull命令從broker上獲取消息,前提是prefetchSize=0并且unconsumedMessages為空。
unconsumedMessage表示未消費(fèi)的消息,這里面預(yù)讀取的消息大小為prefetchSize的值

protected void sendPullCommand(long timeout) throws JMSException {
    clearDeliveredList();
    if (info.getCurrentPrefetchSize() == 0 && unconsumedMessages.isEmpty()) {
        MessagePull messagePull = new MessagePull();
        messagePull.configure(info);
        messagePull.setTimeout(timeout);
        session.asyncSendPacket(messagePull); //向服務(wù)端異步發(fā)送messagePull指令
    }
}

clearDeliveredList

在上面的sendPullCommand方法中,會(huì)先調(diào)用clearDeliveredList方法,主要用來(lái)清理已經(jīng)分發(fā)的消息鏈表deliveredMessages
deliveredMessages,存儲(chǔ)分發(fā)給消費(fèi)者但還未應(yīng)答的消息鏈表

? 如果session是事務(wù)的,則會(huì)遍歷deliveredMessage中的消息放入到previouslyDeliveredMessage中來(lái)做重發(fā)
? 如果session是非事務(wù)的,根據(jù)ACK的模式來(lái)選擇不同的應(yīng)答操作

    // async (on next call) clear or track delivered as they may be flagged as duplicates if they arrive again
    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());
                            }
                            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.clear();
                            pendingAck = null;
                        }
                    }
                    clearDeliveredList = false;
                }
            }
        }
    }

dequeue

從unconsumedMessage中取出一個(gè)消息,在創(chuàng)建一個(gè)消費(fèi)者時(shí),就會(huì)為這個(gè)消費(fèi)者創(chuàng)建一個(gè)未消費(fèi)的消息道,
這個(gè)通道分為兩種,一種是簡(jiǎn)單優(yōu)先級(jí)隊(duì)列分發(fā)通道SimplePriorityMessageDispatchChannel ;另一種是先進(jìn)先
出的分發(fā)通道FifoMessageDispatchChannel.
至于為什么要存在這樣一個(gè)消息分發(fā)通道,大家可以想象一下,如果消費(fèi)者每次去消費(fèi)完一個(gè)消息以后再broker拿一個(gè)消息,效率是比較低的。所以通過(guò)這樣的設(shè)計(jì)可以允許session能夠一次性將多條消息分發(fā)給一個(gè)消費(fèi)者。
默認(rèn)情況下對(duì)于queue來(lái)說(shuō),prefetchSize的值是1000

beforeMessageIsConsumed

? 這里面主要是做消息消費(fèi)之前的一些準(zhǔn)備工作,如果ACK類(lèi)型不是DUPS_OK_ACKNOWLEDGE或者隊(duì)列模式(簡(jiǎn)單來(lái)說(shuō)就是除了T opic和DupAck這兩種情況),所有的消息先放到deliveredMessages鏈表的開(kāi)頭。并且如果當(dāng)前是事務(wù)類(lèi)型的會(huì)話,則判斷transactedIndividualAck,如果為true,表示單條消息直接返回ack。
? 否則,調(diào)用ackLater,批量應(yīng)答, client端在消費(fèi)消息后暫且不發(fā)送ACK,而是把它緩存下來(lái)(pendingACK),等到這些消息的條數(shù)達(dá)到一定閥值時(shí),只需要通過(guò)一個(gè)ACK指令把它們?nèi)看_認(rèn);這比對(duì)每條消息都逐個(gè)確認(rèn),在性能上要提高很多

    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 {
                    ackLater(md, MessageAck.DELIVERED_ACK_TYPE);
                }
            }
        }
    }

afterMessageIsConsumed

這個(gè)方法的主要作用是執(zhí)行應(yīng)答操作,這里面做以下幾個(gè)操作

? 如果消息過(guò)期,則返回消息過(guò)期的ack
? 如果是事務(wù)類(lèi)型的會(huì)話,則不做任何處理
? 如果是AUTOACK或者(DUPS_OK_ACK且是隊(duì)列),并且是優(yōu)化ack操作,則走批量確認(rèn)ack
? 如果是DUPS_OK_ACK,則走ackLater邏輯
? 如果是CLIENT_ACK,則執(zhí)行ackLater

private void afterMessageIsConsumed(MessageDispatch md, boolean messageExpired) throws JMSException {
        if (unconsumedMessages.isClosed()) {
            return;
        }
        if (messageExpired) {
            acknowledge(md, MessageAck.EXPIRED_ACK_TYPE);
            stats.getExpiredMessageCount().increment();
        } else {
            stats.onMessage();
            if (session.getTransacted()) {
                // Do nothing.
            } else if (isAutoAcknowledgeEach()) {
                if (deliveryingAcknowledgements.compareAndSet(false, true)) {
                    synchronized (deliveredMessages) {
                        if (!deliveredMessages.isEmpty()) {
                            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()) {
                ackLater(md, MessageAck.STANDARD_ACK_TYPE);
            } else if (session.isClientAcknowledge()||session.isIndividualAcknowledge()) {
                boolean messageUnackedByConsumer = false;
                synchronized (deliveredMessages) {
                    messageUnackedByConsumer = deliveredMessages.contains(md);
                }
                if (messageUnackedByConsumer) {
                    ackLater(md, MessageAck.DELIVERED_ACK_TYPE);
                }
            }
            else {
                throw new IllegalStateException("Invalid session state.");
            }
        }
    }

2.9ActiveMQ的優(yōu)缺點(diǎn)

ActiveMQ 采用消息推送方式,所以最適合的場(chǎng)景是默認(rèn)消息都可在短時(shí)間內(nèi)被消費(fèi)。數(shù)據(jù)量越大,查找和消費(fèi)消息就越慢,消息積壓程度與消息速度成反比。

缺點(diǎn)

1.吞吐量低。由于 ActiveMQ 需要建立索引,導(dǎo)致吞吐量下降。這是無(wú)法克服的缺點(diǎn),只要使用完全符合 JMS 規(guī)范的消息中間件,就要接受這個(gè)級(jí)別的TPS。
2.無(wú)分片功能。這是一個(gè)功能缺失,JMS 并沒(méi)有規(guī)定消息中間件的集群、分片機(jī)制。而由于 ActiveMQ 是偉企業(yè)級(jí)開(kāi)發(fā)設(shè)計(jì)的消息中間件,初衷并不是為了處理海量消息和高并發(fā)請(qǐng)求。如果一臺(tái)服務(wù)器不能承受更多消息,則需要橫向拆分。ActiveMQ 官方不提供分片機(jī)制,需要自己實(shí)現(xiàn)。

適用場(chǎng)景

對(duì) TPS 要求比較低的系統(tǒng),可以使用 ActiveMQ 來(lái)實(shí)現(xiàn),一方面比較簡(jiǎn)單,能夠快速上手開(kāi)發(fā),另一方面可控性也比較好,還有比較好的監(jiān)控機(jī)制和界面

不適用的場(chǎng)景

消息量巨大的場(chǎng)景。ActiveMQ 不支持消息自動(dòng)分片機(jī)制,如果消息量巨大,導(dǎo)致一臺(tái)服務(wù)器不能處理全部消息,就需要自己開(kāi)發(fā)消息分片功能。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,772評(píng)論 3 422
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,947評(píng)論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,201評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,960評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,350評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,549評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,104評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,914評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,089評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,340評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,753評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,007評(píng)論 1 289
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,834評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,106評(píng)論 2 375

推薦閱讀更多精彩內(nèi)容