MQ初識--Activemq

    目前互聯網分佈式架構的系統基本上離不開消息中間件,也就是此篇博文要講的,那我們就一起來認識一下這位朋友吧。

講到MQ不得不提交異步解耦這個概念,就拿電商下訂單舉例(一般訂單都涉及到短信、物流、郵件等各個流程),下面以圖片爲例子說明。

應用介紹

應用解耦

  1. 不用與線程池綁定,不用寫短信線程任務、物流線程任務等
  2. 即使物流系統掛了,需要幾分鐘修復,在這幾分鐘消息物流系統待處理的消息都被緩存在MQ裏,並不會影響用戶下單,當系統恢復後,補充存儲在MQ裏的訂單信息即可,終端客戶察覺不到物流系統故障,提升體驗。

 流量消峯

        淘寶雙十一大部分流量系統會在期間猛增,這個時候如果沒有緩衝機制難以爲繼,可以通過MQ把請求暫存起來,分散到較長的一段時間內處理,這也就是我們雙十一爲什麼到了零點,下訂單不能一次到位的感覺,但事實上進行流量消峯並不是因爲能力不夠,而是出於經濟原則,比如有的業務系統流量最高峯也不會超過10000QPS,平時只有1000QPS,這種情況下就可以用個普通的服務器(支持1000QPS即可),然後加個消息隊列作爲高峯期緩衝,無需大筆資金購買服務器。

消息分發

       在大數據時代,數據是不斷產生的,各個分析團隊、算法團隊都需要依賴這些數據進行工作,這個時候有個持久化的消息隊列就顯得尤爲重要。數據的產生方面只需要將各自的消息寫入到消息隊列即可,數據的使用方根據各自需求訂閱各自感興趣的數據,不同的團隊所訂閱的消息是可以重複的也可以不重複,互不干擾,也不必和數據生產方關聯。

       各個子系統將日誌數據不停的寫入到消息隊列,不同的數據處理系統有各自的Offset,互不影響。甚至各個團隊處理完的數據也可以寫入到消息隊列,作爲數據的產生方,供給其它團隊使用,避免重複計算。

保證最終一致性

 

 

動態擴容

 

 

消息中間件概念

1、什麼是Message-Queue消息中間件

  • 利用高效可靠地消息機制進行與平臺無關的 數據交流
  • 基於數據通信來實現分佈式系統的集成
  • 通過提供消息傳遞和消息排隊模型,它可以在分佈式環境下擴展進程間的通信
  • 本質上就是接受請求-->保存數據-->發送數據

2、消息中間件的應用場景

  • 跨系統數據傳遞、高併發流量消峯、數據異步處理、系統解耦等

3、常用的消息中間件

  • ActiceMq、RabbitMq、Kafka、RocketMq

協議

       協議是計算機通信共同遵從的一組約定,是對數據格式和計算機之間交換數據時必須遵守的規則。(http重協議,使用輕協議), 消息中間件協議:OpenWire、AMQP、MQTT、Kafka、OpenMessage

  1.  AMQP(Advanced Message Queuing Protocol)協議:高級消息隊列協議。特性:事務支持、持久化支持出生在金融行業,在可靠消息處理上具備天然優勢。使用此協議的MQ:ActiceMq 、RabbitMq
  2.  MQTT(Message Queue Telemetry Transport)協議:消息隊列遙測傳輸,是IBM開發的一個即時通訊的通訊協議,物聯網架構系統中中亞的組成部分。特性:輕量、結構簡單、傳輸快沒有事務支持、沒有持久化相關設計,應用場景:適用於計算能力有限、低帶寬、網路不穩定的場景。使用此協議的MQ:ActiceMq 、RabbitMq
  3.  OpenMessage是最近一兩年有阿里發起的,與雅虎、滴滴出行、Streamlio等公司共同參與創立的分佈式消息中間件協議、流處理應用的開發標準,是國內首個在全球範圍內發起的分佈式消息領域國際標準。特性:結構簡單、解析快、有事務支持、有持久化設計。使用此協議的MQ:Apache RocketMq
  4.  Kafka協議:是基於TCP二進制協議,消息內部是通過長度來分隔,由一些基本數據類型組成。特性:結構簡單,解析快無事務支持有持久化設計。使用此協議的MQ:Kafka

備註:持久化簡單來說就是將數據存入磁盤,而不是存在內存中隨着服務的生命週期一致同生同死,而是是數據永久保存稱之爲持久化。

ActiveMQ安裝(得先安裝JDK1.8以上版本)

1、下載後解壓文件

       mkdir /usr/activemq 並將文件解壓至此處文件夾 tar -zxvf apache-activemq-5.15.9-bin.tar.gz -C /usr/activemq  解壓後目錄如下(可以發現基本上和tomcat一模一樣,其中data是存放日誌文件,webapps是存放管理控制檯程序目錄 ,jar包爲activemq全量包。)

2、創建軟鏈接並啓動    

      爲方便寫配置項,創建軟鏈接    ln -s /usr/activemq/apache-activemq-5.15.9 /usr/activemq/latest  (和快捷方式一樣,操作同一個文件)  

      啓動:

  1. 進入bin目錄     cd /usr/activemq/latest/bin   
  2. 後臺守護進程啓動 (稍等會會啓動進程)  ./activemq start   備註:前臺啓動   ./activemq console

   2、檢測是否啓動成功

      方法一:訪問管理控制檯:http://ip:8161/admin ActiveMQ的管理頁面默認開啓了身份校驗: 賬號:admin 密碼:admin 

      方法二:在啓動Console 或 日誌文件(data/activemq.log)中看到日誌輸出 

      方法三:或用jps命令查看 6193爲啓動的Pid

[root@zhujiname bin]# jps

6193 activemq.jar

6350 Jps

備註:輸入網址,因爲我這裏是虛擬機所以以實際ip爲準(可能會被防火牆攔截,記得關閉防火牆:service iptables stop,查看防火牆狀態service iptables status)。


           
       

ActiveMQ使用

使用下載好的jar包 (在ActiveMQ目錄有activemq-all-5.15.9jar包)或者使用maven依賴下載

<dependency>    
    <groupId>org.apache.activemq</groupId>    
    <artifactId>activemq-all</artifactId>    
    <version>5.15.9</version> 
</dependency>

如果是springboot那麼請添加一下依賴

<dependency>    
    <groupId>org.springframework.boot</groupId>    
    <artifactId>spring-boot-starter-activemq</artifactId> 
</dependency>

普通web項目運行activemq

package activemq;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;
import java.util.Date;

/**
 * @author Heian
 * @time 19/08/14 12:22
 * @description: 消息生產者
 */
public class Producer {

    public static void main(String[] args) {
        //啓動生產者
        ProducerThread producerThread = new ProducerThread ("tcp://192.168.32.130:61616","queue1");
        producerThread.start ();
    }

    static class ProducerThread extends Thread{
        String brokerUrl;
        String destinationUrl;

        public ProducerThread(String brokerUrl,String destinationUrl){
            this.brokerUrl = brokerUrl;
            this.destinationUrl = destinationUrl;
        }

        @Override
        public void run() {

            ActiveMQConnectionFactory connectionFactory;
            Connection connection;
            Session session;

            try {
                //1、創建連接工廠
                connectionFactory = new ActiveMQConnectionFactory (brokerUrl);
                //2、創建連接
                connection = connectionFactory.createConnection ();
                connection.start ();//一定要記得start
                //3、創建會話(可以創建一個或者多個session,我這裏默認一個)
                // 第一個參數:是否開啓事務。true:開啓事務,第二個參數忽略。第二個參數:當第一個參數爲false時,纔有意義。消息的應答模式。1、自動應答2、手動應答。一般是自動應答。
                session = connection.createSession (false, Session.AUTO_ACKNOWLEDGE);
                //4、創建消息發送目標 (Topic 或者 Queue  都實現了Destination)
                Destination destination = session.createQueue (destinationUrl);
                //5、用目的地創建消息生產者
                MessageProducer producer = session.createProducer (destination);
                producer.setDeliveryMode (DeliveryMode.PERSISTENT);//設置遞送模式(持久化/不持久化)
                //6、創建一條文本消息
                String str = "hello activemq:from" + Thread.currentThread ().getName () + ",時間:" + new Date (System.currentTimeMillis ());
                TextMessage message = session.createTextMessage (str);
                //7、通過producer推送消息
                producer.send (message);
                //8、清理 關閉連接
                session.close ();
                connection.close ();

            }catch (JMSException e){
                e.printStackTrace ();
            }
            super.run ();
        }
    }

}
package activemq;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

/**
 * @author Heian
 * @time 19/08/14 20:13
 * @description: 消息消費者
 */
public class Consumer {


    public static void main(String[] args) {
        new ConsumerThread("tcp://192.168.32.130:61616","queue1").start ();
        new ConsumerThread("tcp://192.168.32.130:61616","queue1").start ();
    }

    static class ConsumerThread extends Thread{

        String brokerUrl;//ip+端口
        String destinationUrl;//目標隊列

        public ConsumerThread(String brokerUrl,String destinationUrl){
            this.brokerUrl = brokerUrl;
            this.destinationUrl = destinationUrl;
        }

        @Override
        public void run() {
            ActiveMQConnectionFactory connectionFactory;
            Connection connection;
            Session session;
            MessageConsumer messageConsumer;//消息消費者
            try {
                //1、創建連接工廠
                connectionFactory = new ActiveMQConnectionFactory (brokerUrl);
                //2、創建連接
                connection = connectionFactory.createConnection ();
                connection.start ();//一定要啓動
                //3、創建會話(可以創建一個或者多個session,我這裏默認一個)類似於selector 可以同時監聽多個連接
                // 第一個參數:是否開啓事務。true:開啓事務,第二個參數忽略。第二個參數:當第一個參數爲false時,纔有意義。消息的應答模式。1、自動應答2、手動應答。一般是自動應答。
                session = connection.createSession (false, Session.AUTO_ACKNOWLEDGE);
                //4、創建消息消費目標 (Topic 或者 Queue  都實現了Destination)
                Destination destination = session.createQueue (destinationUrl);
                //5、創建消息消費者
                messageConsumer = session.createConsumer (destination);
                //session.createDurableSubscriber ("topic",destinationUrl);持久訂閱
                //6、接收消息,沒有消息就持續等待  阻塞等待
                Message receiveMsg = messageConsumer.receive ();
                if (receiveMsg  instanceof TextMessage ){
                    System.out.println ("收到消息:" + ((TextMessage) receiveMsg).getText ());
                }else {
                    System.out.println (receiveMsg);
                }
                //8、清理 關閉連接
                messageConsumer.close ();
                connection.close ();
                session.close ();
            } catch (JMSException e) {
                e.printStackTrace ();
            }


        }
    }

}

可以看出其創建過程類似於我們的jdbc的創建,可參考下面的流程圖:

現在分別啓動一個生產和兩個消費者:此時消費者已經消費了一個消息,但還有一個消費者處於阻塞狀態,可查看前臺頁面

如果生產者創建的topic,消費者也訂閱了(發佈訂閱),僞代碼如下:那麼生生產者發一條並且兩個消費者訂閱了,那麼這兩個消費者都會收到消息。

引出問題?

       如何是先運行生產者,在運行消費者,也就是生產者生產了1條數據,兩個消費者處於等待,那麼這兩個消費者會收到消息嗎?答案是並不會,就像你訂閱報紙一樣,你開始都沒訂閱人家報紙,人家爲什麼還給你送報紙,對吧。如果想要收到,就必須的持久訂閱。

備註:在發佈訂閱模式下,訂閱者只會收到發佈者的訂閱以後的消息;在持久訂閱模式下(在MQ登記一個有名的訂閱),當前消費者消費了一段時間後可能掛了,此時發佈者會將沒傳遞到你的消息存起來,當你復活起來後,繼續給你推送未收到的消息。

//生產者 
public static void main(String[] args) { 
        ProducerThread producerThread = new ProducerThread("tcp://192.168.32.130:61616","topic1");                      
        producerThread.start ();
    }
//消費者
public static void main(String[] args) {
        new ConsumerThread("tcp://192.168.32.130:61616","topic1").start ();
        new ConsumerThread("tcp://192.168.32.130:61616","topic1").start ();
    }

SpringBoot項目運行activemq

yaml配置:

spring:
  activemq:
    broker-url: tcp://192.168.32.130:61616
    #user:admin
    #password:admin

配置類:第一配置:連接mq實例配置Bean      第二個配置:對象序列化轉成json類型配置Bean

package com.example.customers.activemq.day1;

import javax.jms.ConnectionFactory;

import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.support.converter.MappingJackson2MessageConverter;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.converter.MessageType;

@Configuration
public class JmsConfiguration {

	//定義 連接mq實例的工廠類  如果是多個mq實例,則要建立連接工廠,單機測試的話可有可無
	@Bean
	public DefaultJmsListenerContainerFactory myFactory(ConnectionFactory connectionFactory,
			DefaultJmsListenerContainerFactoryConfigurer configurer) {
		System.out.println ("----------創建工廠連接監聽器--------------");
		DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
		configurer.configure(factory, connectionFactory);
		return factory;
	}

	@Bean // 序列化對象在將其轉成json形式
	public MessageConverter jacksonJmsMessageConverter() {
		MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
		converter.setTargetType(MessageType.TEXT);
		converter.setTypeIdPropertyName("_type");
		return converter;
	}

}

生產者:

package com.example.customers.activemq.day1;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jms.core.JmsTemplate;

import javax.annotation.PostConstruct;

/**
 * @author Heian
 * @time 19/08/15 21:47
 * @description: 生產者發送對象出去
 */
@SpringBootApplication
public class Producer {

    @Autowired
    private JmsTemplate jmsTemplate;

    @PostConstruct
    public void sendMessage() {
        // 發送對象出去
        System.out.println("-----發送短信中-----");
        jmsTemplate.convertAndSend("destinationName", new Emails ("[email protected]", "hello 1"));
        jmsTemplate.convertAndSend("destinationName", new Emails ("[email protected]", "hello 2"));
    }

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

}

消費者:

package com.example.customers.activemq.day1;

import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

/**
 * @author Heian
 * @time 19/08/15 21:51
 * @description: 消費者接受對象
 */
@Component
public class Consumer {

    //會啓動線程池創建監聽線程不斷的輪詢指定目標地址是否有消息   必須指定連接工廠myFactory,否則無法收到消息
    @JmsListener(destination = "destinationName",containerFactory = "myFactory")
    public void receive(Emails email) {
        System.out.println(Thread.currentThread().getName() + "接受到的消息" + email);
    }

}

傳輸對象實體類       

package com.example.customers.activemq.day1;

/**
 * @author Heian
 * @time 19/08/15 21:36
 * @description: 測試activemq
 */
public class Emails {

    private String emailsUrl;
    private String emailsContent;

    public Emails() {
    }

    public Emails(String emailsUrl, String emailsContent) {
        this.emailsUrl = emailsUrl;
        this.emailsContent = emailsContent;
    }

    public String getEmailsUrl() {
        return emailsUrl;
    }

    public void setEmailsUrl(String emailsUrl) {
        this.emailsUrl = emailsUrl;
    }

    public String getEmailsContent() {
        return emailsContent;
    }

    public void setEmailsContent(String emailsContent) {
        this.emailsContent = emailsContent;
    }

    @Override
    public String toString() {
        return "Emails{" +
                "emailsUrl='" + emailsUrl + '\'' +
                ", emailsContent='" + emailsContent + '\'' +
                '}';
    }
}

此時啓動生產者,會掃描路徑同時消費者也會啓動,即生產者生產了兩條信息,消費者自然也會消費兩條,類比發佈訂閱。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章