Java進階專題(十九) 訊息中介軟體架構體系(1)-- ActiveMQ研究

有夢想的老王發表於2020-12-18

前言

MQ全稱為Message Queue,即訊息佇列,它是一種應用程式之間的通訊方法,訊息佇列在分散式系統開
發中應用非常廣泛。開發中訊息佇列通常有如下應用場景:1、任務非同步處理。將不需要同步處理的並且耗時長的操作由訊息佇列通知訊息接收方進行非同步處理。提高了應用程式的響應時間。2、應用程式解耦合MQ相當於一箇中介,生產方通過MQ與消費方互動,它將應用程式進行解耦合。市場上還有哪些訊息佇列?ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ、Redis。我們主要介紹主流的訊息中介軟體,瞭解每個MQ的優缺點,能知曉什麼樣的場景下選用合適的MQ。

ActiveMQ

介紹

ActiveMQ 是完全基於 JMS 規範實現的一個訊息中介軟體產品。 是 Apache 開源基金會研發的訊息中介軟體。ActiveMQ主要應用在分散式系統架構中,幫助構建高可用、 高效能、可伸縮的企業級面向訊息服務的系統。

什麼是JMS

Java 訊息服務(Java Message Service)是 java 平臺中關於面向訊息中介軟體的 API,用於在兩個應用程式之間,或者分散式系統中傳送訊息,進行非同步通訊。JMS 是一個與具體平臺無關的 API ,絕大多數 MOM(Message Oriented Middleware)(面向訊息中介軟體)提供商都對 JMS 提供了支援。例如ActiveMQ就是其中一個實現。

什麼是MOM

MOM 是面向訊息的中介軟體,使用訊息傳送提供者來協調訊息傳送操作。MOM 需要提供 API 和管理工具。客戶端使用 api 呼叫,把訊息傳送到由提供者管理的目的地。在傳送訊息之後,客戶端會繼續執行其他工作,並且在接收方收到這個訊息確認之前,提供者一直保留該訊息。

JMS規範

我們已經知道了 JMS 規範的目的是為了使得 Java 應用程式能夠訪問現有 MOM (訊息中介軟體)系統,形成一套統一的標準規範,解決不同訊息中介軟體之間的協作問題。在建立 JMS 規範時,設計者希望能夠結合現有的訊息傳送的精髓,比如說

  1. 不同的訊息傳送模式或域,例如點對點訊息傳送和釋出訂閱訊息傳送
  2. 提供於接收同步和非同步訊息的工具
  3. 對可靠訊息傳送的支援
  4. 常見訊息格式,例如流、文字和位元組

JMS物件模型

1)連線工廠。連線工廠(ConnectionFactory)是由管理員建立,並繫結到JNDI樹中。客戶端使用JNDI查詢連線工廠,然後利用連線工廠建立一個JMS連線。

2)JMS連線。JMS連線(Connection)表示JMS客戶端和伺服器端之間的一個活動的連線,是由客戶端通過呼叫連線工廠的方法建立的。

3)JMS會話。JMS會話(Session)表示JMS客戶與JMS伺服器之間的會話狀態。JMS會話建立在JMS連線上,表示客戶與伺服器之間的一個會話執行緒。

4)JMS目的。JMS目的(Destination),又稱為訊息佇列,是實際的訊息源。

5)JMS生產者和消費者。生產者(Message Producer)和消費者(Message Consumer)物件由Session物件建立,用於傳送和接收訊息。

6)JMS訊息通常有兩種型別:

① 點對點(Point-to-Point)。在點對點的訊息系統中,訊息分發給一個單獨的使用者。點對點訊息往往與佇列(javax.jms.Queue)相關聯。

② 釋出/訂閱(Publish/Subscribe)。釋出/訂閱訊息系統支援一個事件驅動模型,訊息生產者和消費者都參與訊息的傳遞。生產者釋出事件,而使用者訂閱感興趣的事件,並使用事件。該型別訊息一般與特定的主題(javax.jms.Topic)關聯。

安裝ActiveMQ

windows安裝

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

下載完成後解壓進入bin目錄 執行 activemq.bat。

如果你遇到如下問題,5672埠被佔用

可以去修改activemq的conf目錄下的activemq.xml,把amqp的埠改為其他的,這裡改成了5673

再次啟動:

訪問地址:http://127.0.0.1:8161/admin/進入後臺頁面 初始賬號密碼 admin admin

Docker安裝ActiveMQ

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

ActiveMQ快速入門

Springboot整合ActiveMQ

匯入依賴

    <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>
        <!--訊息佇列連線池-->
        <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   # 在考慮結束之前等待的時間
    in-memory: true      # 預設代理URL是否應該在記憶體中。如果指定了顯式代理,則忽略此值。
    non-blocking-redelivery: false  # 是否在回滾回滾訊息之前停止訊息傳遞。這意味著當啟用此命令時,訊息順序不會被保留。
    send-timeout: 0     # 等待訊息傳送響應的時間。設定為0等待永遠。
    queue-name: active.queue
    topic-name: active.topic.name.model
    #  packages:
    #    trust-all: true #不配置此項,會報錯
    pool:
      enabled: true
      max-connections: 10   #連線池最大連線數
      idle-timeout: 30000   #空閒的連線過期時間,預設為30秒
    # jms:
    #   pub-sub-domain: true  #預設情況下activemq提供的是queue模式,若要使用topic模式需要配置下面配置

# 是否信任所有包
#spring.activemq.packages.trust-all=
# 要信任的特定包的逗號分隔列表(當不信任所有包時)
#spring.activemq.packages.trusted=
# 當連線請求和池滿時是否阻塞。設定false會拋“JMSException異常”。
#spring.activemq.pool.block-if-full=true
# 如果池仍然滿,則在丟擲異常前阻塞時間。
#spring.activemq.pool.block-if-full-timeout=-1ms
# 是否在啟動時建立連線。可以在啟動時用於加熱池。
#spring.activemq.pool.create-connection-on-startup=true
# 是否用Pooledconnectionfactory代替普通的ConnectionFactory。
#spring.activemq.pool.enabled=false
# 連線過期超時。
#spring.activemq.pool.expiry-timeout=0ms
# 連線空閒超時
#spring.activemq.pool.idle-timeout=30s
# 連線池最大連線數
#spring.activemq.pool.max-connections=1
# 每個連線的有效會話的最大數目。
#spring.activemq.pool.maximum-active-session-per-connection=500
# 當有"JMSException"時嘗試重新連線
#spring.activemq.pool.reconnect-on-exception=true
# 在空閒連線清除執行緒之間執行的時間。當為負數時,沒有空閒連線驅逐執行緒執行。
#spring.activemq.pool.time-between-expiration-check=-1ms
# 是否只使用一個MessageProducer
#spring.activemq.pool.use-anonymous-producers=true

編寫配置類

/**
 * @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模式中,對訊息的監聽需要對containerFactory進行配置
     * @param connectionFactory
     * @return
     */
    @Bean("queueListener")
    public JmsListenerContainerFactory<?> queueJmsListenerContainerFactory(ConnectionFactory connectionFactory){
        SimpleJmsListenerContainerFactory factory = new SimpleJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setPubSubDomain(false);
        return factory;
    }

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

編寫啟動類

/**
 * @author 原
 * @date 2020/12/8
 * @since 1.0
 **/
@SpringBootApplication
@EnableJms //開啟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);
    }

    /**
     * 應用啟動後,會執行該方法
     * 會分別向queue和topic傳送一條訊息
     */
    @PostConstruct
    public void sendMsg(){
        jmsMessagingTemplate.convertAndSend(queue,"queue-test");
        jmsMessagingTemplate.convertAndSend(topic,"topic-test");
    }
}

檢視activemq後臺

active.queue 為佇列的名稱

Number Of Pending Messages 等待消費的訊息數量 3是因為我自己發了3次

Messages Enqueued 已經進入佇列的訊息數量

因為沒有消費者,訊息一直沒有被消費。下面我們編寫消費者程式碼。

/**
 * @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);
    }
}

在後臺傳送一條訊息

控制檯列印

傳送topic訊息

控制檯列印:

但是發現一個問題是,之前在沒有消費的時候,有3條queue和一條topic,但是當我啟動消費者時,queue的3條訊息被消費了,topic確沒有。這是因為:

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

普通訂閱:在消費者啟動之前傳送過來的訊息,消費者啟動之後不會去消費;

持久化訂閱: 在消費者啟動之前傳送過來的訊息,消費者啟動之後會去消費;

ActiveMQ原理分析

訊息同步傳送與非同步傳送

ActiveMQ支援同步、非同步兩種傳送模式將訊息傳送到broker上。
同步傳送過程中,傳送者傳送一條訊息會阻塞直到broker反饋一個確認訊息,表示訊息已經被broker處理。這個機
制提供了訊息的安全性保障,但是由於是阻塞的操作,會影響到客戶端訊息傳送的效能
非同步傳送的過程中,傳送者不需要等待broker提供反饋,所以效能相對較高。但是可能會出現訊息丟失的情況。所
以使用非同步傳送的前提是在某些情況下允許出現資料丟失的情況。
預設情況下,非持久化訊息是非同步傳送的,持久化訊息並且是在非事務模式下是同步傳送的。
但是在開啟事務的情況下,訊息都是非同步傳送。由於非同步傳送的效率會比同步傳送效能更高。所以在傳送持久化消
息的時候,儘量去開啟事務會話。

訊息傳送原理

ProducerWindowSize的含義

producer每傳送一個訊息,統計一下傳送的位元組數,當位元組數達到ProducerWindowSize值時,需要等待broker的確認,才能繼續傳送。

程式碼在:ActiveMQSession的1957行
主要用來約束在非同步傳送時producer端允許積壓的(尚未ACK)的訊息的大小,且只對非同步傳送有意義。每次傳送訊息之後,都將會導致memoryUsage大小增加(+message.size),當broker返回producerAck時,memoryUsage尺寸減少(producerAck.size,此size表示先前傳送訊息的大小)。

可以通過如下2種方式設定:
Ø 在brokerUrl中設定: "tcp://localhost:61616?jms.producerWindowSize=1048576",這種設定將會對所有的
producer生效。
Ø 在destinationUri中設定: "test-queue?producer.windowSize=1048576",此引數只會對使用此Destination例項
的producer失效,將會覆蓋brokerUrl中的producerWindowSize值。

注意:此值越大,意味著消耗Client端的記憶體就越大。

原始碼分析

ActiveMQMessageProducer.send(...)方法

public void send(Destination destination, Message message, int deliveryMode, int priority, long timeToLive, AsyncCallback onComplete) throws JMSException {
        checkClosed();//檢查session連線,若已關閉直接丟擲異常
        if (destination == null) {//校驗傳送訊息的目的地是否為空,也就是必須制定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;
            }
        }
		//如果設定了producerWindow,則需要校驗producerWindow大小
        if (producerWindow != null) {
            try {
                producerWindow.waitForSpace();
            } catch (InterruptedException e) {
                throw new JMSException("Send aborted due to thread interrupt.");
            }
        }
		//傳送訊息
        this.session.send(this, dest, message, deliveryMode, priority, timeToLive, producerWindow, sendTimeout, onComplete);
		//做統計的
        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 {
		//校驗連線	
        checkClosed();
    	//校驗傳送目標
        if (destination.isTemporary() && connection.isDeleted(destination)) {
            throw new InvalidDestinationException("Cannot publish to a deleted Destination: " + destination);
        }
    	//互斥鎖,如果一個session的多個producer傳送訊息到這裡,會保證訊息傳送的有序性
        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);//設定是否持久化
            long expiration = 0L;
            if (!producer.getDisableMessageTimestamp()) {
                long timeStamp = System.currentTimeMillis();
                message.setJMSTimestamp(timeStamp);
                if (timeToLive > 0) {
                    expiration = timeToLive + timeStamp;
                }
            }
            message.setJMSExpiration(expiration);//訊息過期時間
            message.setJMSPriority(priority);//訊息優先順序
            message.setJMSRedelivered(false);//是否重複傳送

            // transform to our own message format here 統一封裝
            ActiveMQMessage msg = ActiveMQMessageTransformation.transformMessage(message, connection);
            msg.setDestination(destination);
            //設定訊息ID
            msg.setMessageId(new MessageId(producer.getProducerInfo().getProducerId(), sequenceNumber));

            // Set the message id.
            if (msg != message) {//如果訊息是經過轉化的,則更新原來的訊息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();//把訊息屬性和訊息體都設定為只讀,防止被修改
            msg.setProducerId(msg.getMessageId().getProducerId());
            if (LOG.isTraceEnabled()) {
                LOG.trace(getSessionId() + " sending message: " + msg);
            }
            //如果onComplete沒有設定,且傳送超時時間小於0,且訊息不需要反饋,且聯結器不是同步傳送模式,且訊息非持久化或者聯結器是非同步傳送模式
			//或者存在事務id的情況下,走非同步傳送,否則走同步傳送
            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();//非同步傳送的情況下,需要設定producerWindow的大小
                    producerWindow.increaseUsage(size);
                }
            } else {
                if (sendTimeout > 0 && onComplete==null) {
                    this.connection.syncSendPacket(msg,sendTimeout);//帶超時時間的同步傳送//帶回撥的同步傳送
                }else {
                    this.connection.syncSendPacket(msg, onComplete);//帶回撥的同步傳送
                }
            }

        }
    }

看下非同步傳送的程式碼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是個什麼東西?在哪裡例項化的?按照以前看原始碼的慣例來看,它肯定不是一個單純的物件。按照以往我看原始碼的經驗來看,一定是在建立連線的過程中初始化的。所以我們定位到程式碼

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

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);
//省略後面的程式碼
}
//這個方法就是例項化Transport的 1.構建Broker的URL 2.根據這個URL去建立一個連結TransportFactory.connect 預設使用的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);//裡面的程式碼繼續往下看
        } catch (Exception e) {
            throw JMSExceptionSupport.create("Could not create Transport. Reason: " + e, e);
        }
    }    

TransportFactory. findTransportFactory

  1. 從TRANSPORT_FACTORYS這個Map集合中,根據scheme去獲得一個TransportFactory指定的例項物件
  2. 如果Map集合中不存在,則通過TRANSPORT_FACTORY_FINDER去找一個並且構建例項
    Ø 這個地方又有點類似於我們之前所學過的SPI的思想吧?他會從METAINF/services/org/apache/activemq/transport/ 這個路徑下,根據URI組裝的scheme去找到匹配class物件並且
    例項化,所以根據tcp為key去對應的路徑下可以找到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;
    }

呼叫TransportFactory.doConnect去構建一個連線


    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 {
        //組裝一個複合的transport,這裡會包裝兩層,一個是IactivityMonitor.另一個是WireFormatNegotiator
        transport = compositeConfigure(transport, wf, options);

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

        return transport;
    }

到目前為止,這個transport實際上就是一個呼叫鏈了,他的鏈結構為
ResponseCorrelator(MutexT ransport(WireFormatNegotiator(IactivityMonitor(T cpT ransport()))
每一層包裝表示什麼意思呢?
ResponseCorrelator 用於實現非同步請求。
MutexT ransport 實現寫鎖,表示同一時間只允許傳送一個請求
WireFormatNegotiator 實現了客戶端連線broker的時候先傳送資料解析相關的協議資訊,比如解析版本號,是否
使用快取等
InactivityMonitor 用於實現連線成功成功後的心跳檢查機制,客戶端每10s傳送一次心跳資訊。服務端每30s讀取
一次心跳資訊。

同步傳送和非同步傳送的區別

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

持久化訊息和非持久化訊息的儲存原理

正常情況下,非持久化訊息是儲存在記憶體中的,持久化訊息是儲存在檔案中的。能夠儲存的最大訊息資料在
${ActiveMQ_HOME}/conf/activemq.xml檔案中的systemUsage節點
SystemUsage配置設定了一些系統記憶體和硬碟容量

<systemUsage>
<systemUsage>
<memoryUsage>
//該子標記設定整個ActiveMQ節點的“可用記憶體限制”。這個值不能超過ActiveMQ本身設定的最大記憶體大小。其中的
percentOfJvmHeap屬性表示百分比。佔用70%的堆記憶體
<memoryUsage percentOfJvmHeap="70" />
</memoryUsage>
<storeUsage>
//該標記設定整個ActiveMQ節點,用於儲存“持久化訊息”的“可用磁碟空間”。該子標記的limit屬性必須要進行設定
<storeUsage limit="100 gb"/>
</storeUsage>
<tempUsage>
//一旦ActiveMQ服務節點儲存的訊息達到了memoryUsage的限制,非持久化訊息就會被轉儲到 temp store區域,雖然
我們說過非持久化訊息不進行持久化儲存,但是ActiveMQ為了防止“資料洪峰”出現時非持久化訊息大量堆積致使記憶體耗
盡的情況出現,還是會將非持久化訊息寫入到磁碟的臨時區域——temp store。這個子標記就是為了設定這個temp
store區域的“可用磁碟空間限制”
<tempUsage limit="50 gb"/>
</tempUsage>
</systemUsage>
</systemUsage>

從上面的配置我們需要get到一個結論,當非持久化訊息堆積到一定程度的時候,也就是記憶體超過指定的設定閥值時,ActiveMQ會將記憶體中的非持久化訊息寫入到臨時檔案,以便騰出記憶體。但是它和持久化訊息的區別是,重啟之後,持久化訊息會從檔案中恢復,非持久化的臨時檔案會直接刪除

訊息的持久化策略分析

訊息永續性對於可靠訊息傳遞來說是一種比較好的方法,即時傳送者和接受者不是同時線上或者訊息中心在傳送者傳送訊息後當機了,在訊息中心重啟後仍然可以將訊息傳送出去。訊息永續性的原理很簡單,就是在傳送訊息出去後,訊息中心首先將訊息儲存在本地檔案、記憶體或者遠端資料庫,然後把訊息傳送給接受者,傳送成功後再把訊息從儲存中刪除,失敗則繼續嘗試。接下來我們來了解一下訊息在broker上的持久化儲存實現方式

持久化儲存支援型別

ActiveMQ支援多種不同的持久化方式,主要有以下幾種,不過,無論使用哪種持久化方式,訊息的儲存邏輯都是一致的。
Ø KahaDB儲存(預設儲存方式)

Ø JDBC儲存

Ø Memory儲存

Ø LevelDB儲存

Ø JDBC With ActiveMQ Journal

KahaDB儲存
KahaDB是目前預設的儲存方式,可用於任何場景,提高了效能和恢復能力。訊息儲存使用一個事務日誌和僅僅用一個索引檔案來儲存它所有的地址。
KahaDB是一個專門針對訊息持久化的解決方案,它對典型的訊息使用模式進行了優化。在Kaha中,資料被追加到data logs中。當不再需要log檔案中的資料的時候,log檔案會被丟棄。

配置方式

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

KahaDB的儲存原理
在data/kahadb這個目錄下,會生成四個檔案
Ø db.data 它是訊息的索引檔案,本質上是B-Tree(B樹),使用B-Tree作為索引指向db-.log裡面儲存的訊息
Ø db.redo 用來進行訊息恢復
Ø db-
.log 儲存訊息內容。新的資料以APPEND的方式追加到日誌檔案末尾。屬於順序寫入,因此訊息儲存是比較
快的。預設是32M,達到閥值會自動遞增
Ø lock檔案 鎖,表示當前獲得kahadb讀寫許可權的broker

JDBC儲存
使用JDBC持久化方式,資料庫會建立3個表:activemq_msgs,activemq_acks和activemq_lock。
ACTIVEMQ_MSGS 訊息表,queue和topic都存在這個表中
ACTIVEMQ_ACKS 儲存持久訂閱的資訊和最後一個持久訂閱接收的訊息ID
ACTIVEMQ_LOCKS 鎖表,用來確保某一時刻,只能有一個ActiveMQ broker例項來訪問資料庫
JDBC儲存配置

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

dataSource指定持久化資料庫的bean,createT ablesOnStartup是否在啟動的時候建立資料表,預設值是true,這
樣每次啟動都會去建立資料表了,一般是第一次啟動的時候設定為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儲存

LevelDB持久化效能高於KahaDB,雖然目前預設的持久化方式仍然是KahaDB。並且,在ActiveMQ 5.9版本提供
了基於LevelDB和Zookeeper的資料複製方式,用於Master-slave方式的首選資料複製方案。
不過,據ActiveMQ官網對LevelDB的表述:LevelDB官方建議使用以及不再支援,推薦使用的是KahaDB

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

Memory 訊息儲存
基於記憶體的訊息儲存,記憶體訊息儲存主要是儲存所有的持久化的訊息在記憶體中。persistent=”false”,表示不設定持
久化儲存,直接儲存到記憶體中

<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每次訊息過來,都需要去寫庫和讀庫。
ActiveMQ Journal,使用快取記憶體寫入技術,大大提高了效能。
當消費者的消費速度能夠及時跟上生產者訊息的生產速度時,journal檔案能夠大大減少需要寫入到DB中的訊息。
舉個例子,生產者生產了1000條訊息,這1000條訊息會儲存到journal檔案,如果消費者的消費速度很快的情況
下,在journal檔案還沒有同步到DB之前,消費者已經消費了90%的以上的訊息,那麼這個時候只需要同步剩餘的
10%的訊息到DB。
如果消費者的消費速度很慢,這個時候journal檔案可以使訊息以批量方式寫到DB。
Ø 將原來的標籤註釋掉
Ø 新增如下標籤

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

Ø 在服務端迴圈傳送訊息。可以看到資料是延遲同步到資料庫的

消費端消費訊息的原理

我們知道有兩種方法可以接收訊息,一種是使用同步阻塞的MessageConsumer#receive方法。另一種是使用訊息監聽器MessageListener。這裡需要注意的是,在同一個session下,這兩者不能同時工作,也就是說不能針對不同訊息採用不同的接收方式。否則會丟擲異常。
至於為什麼這麼做,最大的原因還是在事務性會話中,兩種消費模式的事務不好管控

消費流程圖

ActiveMQMessageConsumer.receive消費端同步接收訊息的原始碼入口

public Message receive() throws JMSException {
    checkClosed();
    checkMessageListener();  //檢查receive和MessageListener是否同時配置在當前的會話中,同步消費不需要設定MessageListener 否則會報錯
    sendPullCommand(0); //如果PrefetchSizeSize為0並且unconsumerMessage為空,則發起pull命令
    MessageDispatch md = dequeue(-1); //從unconsumerMessage出佇列獲取訊息
    if (md == null) {
        return null;
    }
    beforeMessageIsConsumed(md);
    afterMessageIsConsumed(md, false); //傳送ack給到broker
    return createActiveMQMessage(md);//獲取訊息並返回
}

sendPullCommand
傳送pull命令從broker上獲取訊息,前提是prefetchSize=0並且unconsumedMessages為空。
unconsumedMessage表示未消費的訊息,這裡面預讀取的訊息大小為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); //向服務端非同步傳送messagePull指令
    }
}

clearDeliveredList

在上面的sendPullCommand方法中,會先呼叫clearDeliveredList方法,主要用來清理已經分發的訊息連結串列
deliveredMessages
deliveredMessages,儲存分發給消費者但還未應答的訊息連結串列
Ø 如果session是事務的,則會遍歷deliveredMessage中的訊息放入到previouslyDeliveredMessage中來做重發
Ø 如果session是非事務的,根據ACK的模式來選擇不同的應答操作

    // 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中取出一個訊息,在建立一個消費者時,就會為這個消費者建立一個未消費的訊息道,
這個通道分為兩種,一種是簡單優先順序佇列分發通道SimplePriorityMessageDispatchChannel ;另一種是先進先
出的分發通道FifoMessageDispatchChannel.
至於為什麼要存在這樣一個訊息分發通道,大家可以想象一下,如果消費者每次去消費完一個訊息以後再broker拿一個訊息,效率是比較低的。所以通過這樣的設計可以允許session能夠一次性將多條訊息分發給一個消費者。
預設情況下對於queue來說,prefetchSize的值是1000

beforeMessageIsConsumed

​ 這裡面主要是做訊息消費之前的一些準備工作,如果ACK型別不是DUPS_OK_ACKNOWLEDGE或者佇列模式(簡單來說就是除了T opic和DupAck這兩種情況),所有的訊息先放到deliveredMessages連結串列的開頭。並且如果當前是事務型別的會話,則判斷transactedIndividualAck,如果為true,表示單條訊息直接返回ack。
​ 否則,呼叫ackLater,批量應答, client端在消費訊息後暫且不傳送ACK,而是把它快取下來(pendingACK),等到這些訊息的條數達到一定閥值時,只需要通過一個ACK指令把它們全部確認;這比對每條訊息都逐個確認,在效能上要提高很多

    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

這個方法的主要作用是執行應答操作,這裡面做以下幾個操作
Ø 如果訊息過期,則返回訊息過期的ack
Ø 如果是事務型別的會話,則不做任何處理
Ø 如果是AUTOACK或者(DUPS_OK_ACK且是佇列),並且是優化ack操作,則走批量確認ack
Ø 如果是DUPS_OK_ACK,則走ackLater邏輯
Ø 如果是CLIENT_ACK,則執行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.");
            }
        }
    }

ActiveMQ的優缺點

ActiveMQ 採用訊息推送方式,所以最適合的場景是預設訊息都可在短時間內被消費。資料量越大,查詢和消費訊息就越慢,訊息積壓程度與訊息速度成反比。

缺點

1.吞吐量低。由於 ActiveMQ 需要建立索引,導致吞吐量下降。這是無法克服的缺點,只要使用完全符合 JMS 規範的訊息中介軟體,就要接受這個級別的TPS。
2.無分片功能。這是一個功能缺失,JMS 並沒有規定訊息中介軟體的叢集、分片機制。而由於 ActiveMQ 是偉企業級開發設計的訊息中介軟體,初衷並不是為了處理海量訊息和高併發請求。如果一臺伺服器不能承受更多訊息,則需要橫向拆分。ActiveMQ 官方不提供分片機制,需要自己實現。

適用場景

對 TPS 要求比較低的系統,可以使用 ActiveMQ 來實現,一方面比較簡單,能夠快速上手開發,另一方面可控性也比較好,還有比較好的監控機制和介面

不適用的場景

訊息量巨大的場景。ActiveMQ 不支援訊息自動分片機制,如果訊息量巨大,導致一臺伺服器不能處理全部訊息,就需要自己開發訊息分片功能。

相關文章