ActiveMQ基礎與SpringBoot整合

ActiveMQ實現了JMS規範。

# ActiveMQ中相關概念術語

  1. Destination目的地
    消息將要發送的地方,包括:QueueTopic,它們都對Destination接口進行了實現
    1. PTP模式 - Queue
    2. 發佈訂閱模式 - Topic
      MessageProvider需要指定Destination才能發送消息,MessageConsumer需要指定Destination才能接收和消費消息。
  2. Producer消息生產者
    消息生產者,負責將消息發送到目的地Destination
  3. Consumer消息消費者
    消息消費者,負責從目的地Destination消費消息。
  4. Message消息本體
  5. ConnectionFactory連接工廠
    用於創建連接的工廠
  6. Connection連接
    用戶訪問ActiveMQ
  7. Session會話
    一次持久有效有狀態的訪問,由Connection創建,是具體操作消息的基礎支撐。
    JMS中定義了兩種消息模型:點對點(point to point, queue)發佈/訂閱(publish/subscribe,topic)。主要區別就是是能否重複消費

# JMS中Queue模式與Topic模式對比

Topic Queue
概要 Publish Subscribe messaging 發佈訂閱消息 Point-to-Point 點對點
有無狀態 topic數據默認不落地,是無狀態的。 Queue數據默認會在mq服務器上以文件形式保存,比如Active MQ一般保存在$AMQ_HOME\data\kr-store\data下面。也可以配置成DB存儲。
完整性保障 並不保證publisher發佈的每條數據,Subscriber都能接受到。 Queue保證每條數據都能被receiver接收。
消息是否會丟失 一般來說publisher發佈消息到某一個topic時,只有正在監聽該topic地址的sub能夠接收到消息;如果沒有sub在監聽,該topic就丟失了。 Sender發送消息到目標Queue,receiver可以異步接收這個Queue上的消息。Queue上的消息如果暫時沒有receiver來取,也不會丟失。
消息發佈接收策略 一對多的消息發佈接收策略,監聽同一個topic地址的多個sub都能收到publisher發送的消息。Sub接收完通知mq服務器 一對一的消息發佈接收策略,一個sender發送的消息,只能有一個receiver接收。receiver接收完後,通知mq服務器已接收,mq服務器對queue裏的消息採取刪除或其他操作。

1. PTP Queue不可重複消費

消息生產者生產消息發送到queue中,然後消息消費者從queue中取出並且消費消息。
消息被消費以後(消費者ack應答確認/事務模式),queue中不再有存儲,所以消息消費者不可能消費到已經被消費的消息。
Queue支持存在多個消費者,但是對一個消息而言,只會有一個消費者可以消費、其它的則不能消費此消息了。
當消費者不存在時,消息會一直保存,直到有消費消費

img

2. 發佈訂閱模式 Topic 可以重複消費

消息生產者(發佈)將消息發佈到Topic中,同時有多個消息消費者(訂閱該Topic)消費該消息。
和點對點方式不同,發佈到topic的消息會被所有訂閱者消費。
**當生產者發佈消息,不管是否有消費者。都不會保存消息。**如果生產者向隊列發送消息時,沒有消費者訂閱該隊列,則消息全部丟失。否則向所有訂閱了該Topic的消費者發送同樣的消息(即:消費者必須在線)

img

# 在SpringBoot中使用ActiveMQ

ActiveMQ管理地址: http://localhost:8161/admin/

  1. PTP模式

    • 依賴

          //jms-active
          compile 'org.springframework.boot:spring-boot-starter-activemq'
          //active連接池-1.5.13依賴
          compile 'org.apache.activemq:activemq-pool'
      
    • 配置信息

      spring:
        # activemq
        activemq:
          broker-url: failover:(tcp://localhost:61616,tcp://localhost:666)?randomize=false      # tcp://localhost:61616/故障轉移,默認情況下如果某個鏈接失效了,則從列表中隨機獲取一個,如果設置了randomize=false則是嚴格按照列表的先後順序的
          user: admin           # 用戶名
          password: admin       # 密碼
          in-memory: false      # 基於內存的activemq
          close-timeout: 15s     # 在考慮結束之前等待的時間
          pool:
            enabled: true                               # 啓動連接池(是否用Pooledconnectionfactory代替普通的ConnectionFactory)
            max-connections: 10                         # 最大鏈接數量
            idle-timeout: 60s                           # 空閒連接存活時間
            block-if-full: true                         # 當連接請求和池滿時是否阻塞。設置false會拋“JMSException異常”
            block-if-full-timeout: -1                   # 如果池仍然滿,則在拋出異常之前阻塞時間
            create-connection-on-startup: true          # 是否在啓動時創建連接。可以在啓動時用於加熱池
            maximum-active-session-per-connection: 500  # 每個連接的有效會話的最大數目。
            reconnect-on-exception: true                # 當發生"JMSException"時嘗試重新連接
        jms:
          pub-sub-domain: false                  # 默認情況下activemq提供的是queue模式,若要使用topic模式需要配置下面配置
      
    • 定義PTP模式下的Destination-Queue

      /**
       * @author futao
       * Created on 2019-06-04.
       */
      @AllArgsConstructor
      @Getter
      public enum ActiveMqQueueEnum {
          /**
           * springboot-test-queue=測試Queue
           */
          TEST_QUEUE("springboot-test-queue", "測試Queue");
        
          private String queueName;
          private String desc;
      
          public static final String testQueue = "springboot-test-queue";
      }
      
      /**
       * @author futao
       * Created on 2019-06-04.
       */
      @Configuration
      public class ActiveMqConfig {
      
         /**
           * The ActiveMQConnectionFactory creates ActiveMQ Connections.
           * The PooledConnectionFactory pools Connections.
           * If you only need to create one Connection and keep it around for a long time you don't need to pool.
           * If you tend to create many Connection instances over time then Pooling is better as connecting is a heavy operation and can be a performance bottleneck.
           * <p>
           * 可以在這裏統一設置JmsTemplate的一些配置,也可以在具體使用到JmsTemplate的時候單獨設置
           * JmsMessageTemplate是對JmsTemplate的進一步封裝
           * TODO 目前看起來不起作用
           *
           * @param factory
           * @return
           */
          //    @Primary
      //    @Bean
          public JmsTemplate jmsTemplate(PooledConnectionFactory factory) {
              JmsTemplate jmsTemplate = new JmsTemplate();
              //關閉事物
              jmsTemplate.setSessionTransacted(false);
              //TODO 在此設置無效
      //        jmsTemplate.setSessionAcknowledgeMode(ActiveMQSession.INDIVIDUAL_ACKNOWLEDGE);
              jmsTemplate.setConnectionFactory(factory);
              return jmsTemplate;
          }
        
          @Bean(name = ActiveMqQueueEnum.testQueue)
          public ActiveMQQueue activeTestQueue() {
              return new ActiveMQQueue(ActiveMqQueueEnum.TEST_QUEUE.getQueueName());
          }
        /**
           * 定義一個消息監聽器連接工廠,這裏定義的是點對點模式的監聽器連接工廠
           *
           * @param pooledConnectionFactory
           * @return
           */
          @Bean(name = "jmsQueueListener")
          public DefaultJmsListenerContainerFactory jmsQueueListenerContainerFactory(PooledConnectionFactory pooledConnectionFactory) {
              DefaultJmsListenerContainerFactory factory =
                      new DefaultJmsListenerContainerFactory();
              factory.setConnectionFactory(pooledConnectionFactory);
              factory.setSessionTransacted(false);
              factory.setSessionAcknowledgeMode(ActiveMQSession.INDIVIDUAL_ACKNOWLEDGE);
              return factory;
          }
      }  
      
    • 定義PTP模式下的生產者

      package com.futao.springbootdemo.foundation.mq.active.ptp;
      
      import lombok.extern.slf4j.Slf4j;
      import org.apache.activemq.command.ActiveMQQueue;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.beans.factory.annotation.Qualifier;
      import org.springframework.jms.core.JmsMessagingTemplate;
      import org.springframework.stereotype.Component;
      
      import javax.jms.JMSException;
      
      /**
       * PTP模式生產者
       *
       * @author futao
       * Created on 2019-06-06.
       */
      @Slf4j
      @Component
      public class PtpProducer {
        
          @Autowired
          private JmsMessagingTemplate jmsMessagingTemplate;
      
          /**
           * 目的地
           */
          @Qualifier("springboot-test-queue")
          @Autowired
          private ActiveMQQueue springBootTestQueue;
      
          public void send(String msg) {
              jmsMessagingTemplate.convertAndSend(springBootTestQueue, msg);
              try {
                  log.info("send to ActiveMQ-Queue[{}] success ,msg:[{}]", springBootTestQueue.getQueueName(), msg);
              } catch (JMSException e) {
                  e.printStackTrace();
              }
          }
      }
      
      /**
       * @author futao
       * Created on 2019-06-04.
       */
      @RequestMapping("/activemq")
      @RestController
      public class ActiveController {
        @Resource
          private PtpProducer ptpProducer;
      
          @PostMapping("/ptp/sender")
          public void ptpSender(@RequestParam String msg) {
              ptpProducer.send(msg);
          }
      }
      
    • 定義PTP模式下的消費者

      package com.futao.springbootdemo.foundation.mq.active.ptp;
      
      import com.futao.springbootdemo.foundation.mq.active.ActiveMqQueueEnum;
      import lombok.extern.slf4j.Slf4j;
      import org.apache.activemq.ActiveMQConnectionFactory;
      import org.apache.activemq.command.ActiveMQMessage;
      import org.apache.activemq.command.ActiveMQQueue;
      import org.junit.Test;
      import org.springframework.jms.annotation.JmsListener;
      import org.springframework.stereotype.Service;
      
      import javax.jms.*;
      
      /**
       * @author futao
       * Created on 2019-06-06.
       */
      @Slf4j
      @Service
      public class PtpConsumer {
      
          @JmsListener(destination = ActiveMqQueueEnum.testQueue, containerFactory = "jmsQueueListener")
          public void ptpConsumer(ActiveMQMessage message) throws JMSException {
              String text = ((TextMessage) message).getText();
              if ("節日快樂666".equalsIgnoreCase(text)) {
                  message.acknowledge();    //ack手動確認
              }
              log.info("receive message from activeMQ :[{}]", text);
          }
        /**
         * 手動創建ActiveMQConnectionFactory消費消息,生產消息也類似
         */
          @Test
          public void test() throws Exception {
              ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory("admin", "admin", "tcp://localhost:61616");
              Connection connection = connectionFactory.createConnection();
              Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);//開啓ack手動確認
              MessageConsumer consumer = session.createConsumer(new ActiveMQQueue(ActiveMqQueueEnum.TEST_QUEUE.getQueueName()));
              connection.start();
              consumer.setMessageListener(message -> {
                  try {
                      String text = ((TextMessage) message).getText();
                      System.out.println(("收到消息:{}" + text));
                      if ("節日快樂666".equalsIgnoreCase(text)) {
                          message.acknowledge();    //ack手動確認
                      }
                  } catch (JMSException e) {
                      e.printStackTrace();
                  }
              });
              Thread.sleep(999999999);
          }
      }
      
      

    image.png

    • 特點
      • 一條消息只會發送給其中某一個單獨的消費者
        image.png

      • 未被確認的消息將再次發送給其他消費
        image.png

  2. 發佈訂閱模式

    • 發佈訂閱模式需要將spring.jms.pub-sub-domain=true,其他配置不需要修改

    • 定義發佈訂閱模式下的Destination - Topic

      /**
       * @author futao
       * Created on 2019-06-04.
       */
      @Configuration
      public class ActiveMqConfig {
        /**
           * ActiveMQ topic的定義
           */
          public static class TopicDefinition {
              public static final String activeTestTopic = "active-test-topic";
              public static final String activeProdTopic = "active-prod-topic";
          }
      
          /**
           * 定義一個名爲BeanName爲activeTestTopic的Topic:active-test-topic
           *
           * @return
           */
          @Bean(name = "activeTestTopic")
          public ActiveMQTopic activeMQTestTopic() {
              return new ActiveMQTopic(TopicDefinition.activeTestTopic);
          }
      
          /**
           * 定義一個名爲BeanName爲activeProdTopic的Topic:active-prod-topic
           *
           * @return
           */
          @Bean(name = "activeProdTopic")
          public ActiveMQTopic activeMQProdTopic() {
              return new ActiveMQTopic(TopicDefinition.activeProdTopic);
          }
      }
          @PostMapping("/ps/sender")
          public void pushTest(@RequestParam String msg) {
              activeMqProducer.send(msg);
          }
      
    • 發佈訂閱模式下的消費者定義

      package com.futao.springbootdemo.foundation.mq.active.topic;
      
      import com.futao.springbootdemo.foundation.mq.active.ActiveMqConfig;
      import lombok.extern.slf4j.Slf4j;
      import org.apache.activemq.command.ActiveMQMessage;
      import org.springframework.jms.annotation.JmsListener;
      import org.springframework.stereotype.Service;
      
      import javax.jms.JMSException;
      import javax.jms.TextMessage;
      
      /**
       * 訂閱的隊列是PTP模式還是Topic模式,與這邊的定義無關。取決於配置
       * # 開啓topic模式
       * spring:
       * jms:
       * pub-sub-domain: true
       *
       * @author futao
       * Created on 2019-06-04.
       */
      @Slf4j
      @Service
      public class ActiveMqConsumer {
      
          /**
           * 訂閱testTopic  -1
           *
           * @param mqMessage
           * @throws JMSException
           */
          @JmsListener(destination = ActiveMqConfig.TopicDefinition.activeTestTopic)
          public void testTopicConsumer1(ActiveMQMessage mqMessage) throws JMSException {
              String text = ((TextMessage) mqMessage.getMessage()).getText();
              log.info("testTopicConsumer1接收到activeMq-activeTestTopic消息:[{}]", text);
          }
      
          /**
           * 訂閱testTopic  -2
           *
           * @param mqMessage
           * @throws JMSException
           */
          @JmsListener(destination = ActiveMqConfig.TopicDefinition.activeTestTopic)
          public void testTopicConsumer2(ActiveMQMessage mqMessage) throws JMSException {
              String text = ((TextMessage) mqMessage.getMessage()).getText();
              log.info("testTopicConsumer2接收到activeMq-activeTestTopic消息:[{}]", text);
          }
      
          /**
           * 訂閱prodTopic  -1
           *
           * @param mqMessage
           * @throws JMSException
           */
          @JmsListener(destination = ActiveMqConfig.TopicDefinition.activeProdTopic)
          public void prodTopicConsumer1(ActiveMQMessage mqMessage) throws JMSException {
              String text = ((TextMessage) mqMessage.getMessage()).getText();
              log.info("prodTopicConsumer1接收到activeMq-activeProdTopic消息:[{}]", text);
          }
      
          /**
           * 訂閱 prodTopic  -2
           *
           * @param mqMessage
           * @throws JMSException
           */
          @JmsListener(destination = ActiveMqConfig.TopicDefinition.activeProdTopic)
          public void prodTopicConsumer2(ActiveMQMessage mqMessage) throws JMSException {
              String text = ((TextMessage) mqMessage.getMessage()).getText();
              log.info("prodTopicConsumer2接收到activeMq-activeProdTopic消息:[{}]", text);
          }
      }
      
    • 結果展示

      **發送到Topic的消息被所有訂閱了該Topic的消費者接收

image.png

image.png

# 參考資料

SpringBoot與ActiveMQ整合實現手動ACK(事務模式與ack應答模式)

# TODO:

  • 如何保證消費者將消息發送到ActiveMQ的過程中消息不丟失
  • ActiveMQ的集羣與主從
  • 消息的持久化
  • 事務
  • PTP模式下消費者多久沒ACK後ActiveMQ會認爲該條消息消費失敗呢?(是不是有個消費超時時間設置)。還是隻能等到該消費者下線。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章