Spring Boot與消息隊列


1. 消息隊列

1.1 引入

消息隊列已經逐漸成爲企業IT系統內部通信的核心手段。它具有低耦合、可靠投遞、廣播、流量控制、最終一致性等一系列功能,成爲異步RPC的主要手段之一。當今市面上有很多主流的消息中間件,如老牌的ActiveMQ、RabbitMQ,炙手可熱的Kafka,阿里巴巴自主開發RocketMQ等。

1.2 應用場景

  • 異步通信
  • 應用解耦
  • 流量削峯
    在這裏插入圖片描述

1.3 核心概念

消息服務中兩個重要概念:消息代理(message broker)和目的地(destination)。當消息發送者發送消息以後,將由消息代理接管,消息代理保證消息傳遞到指定目的地。消息隊列主要有兩種形式的目的地:

  • 隊列(queue):針對於點對點消息通信(point-to-point)
  • **主題(**topic):針對於發佈(publish)/訂閱(subscribe)消息通信

其他的重要概念有:

  • 點對點式:消息發送者發送消息,消息代理將其放入一個隊列中,消息接收者從隊列中獲取消息內容,消息讀取後被移出隊列

    消息只有唯一的發送者和接受者,但並不是說只能有一個接收者。即消息有可能被多個接收者接收,但只能有一個用戶使用到消息。

  • 發佈訂閱式:發送者(發佈者)發送消息到主題,多個接收者(訂閱者)監聽(訂閱)這個主題,那麼就會在消息到達時同時收到消息

  • JMS(Java Message Service):Java消息服務,它是基於JVM消息代理的規範,ActiveMQ、HornetMQ都是JMS的實現

  • AMQP(Advanced Message Queuing Protocol):高級消息隊列協議,也是一個消息代理的規範,兼容JMS。RabbitMQ是AMQP的實現

1.4 JMS VS AMQP

JMS AMQP
定義 Java API 網絡線級協議
跨語言
跨平臺
Model Peer-2-Peer、Pub/sub direct exchange、fanout exchange、topic change、headers exchange、system exchange
支持消息類型 多種消息類型:TextMessage、MapMessage、BytesMessage、StreamMessage、ObjectMessag、eMessage (只有消息頭和屬性) byte[],當實際應用時,有複雜的消息,可以將消息序列化後發送。
綜合評價 JMS 定義了JAVA API層面的標準;在java體系中,多個client均可以通過JMS進行交互,不需要應用修改代碼,但是其對跨平臺的支持較差 AMQP定義了wire-level層的協議標準;天然具有跨平臺、跨語言特性

2. RabbitMQ

2.1 核心概念

RabbitMQ是一個由erlang開發的AMQP(Advanved Message Queue Protocol)的開源實現。它的核心概念有:

  • Message:消息,消息是不具名的,它由消息頭和消息體組成。消息體是不透明的,而消息頭則由一系列的可選屬性組成,這些屬性包括routing-key(路由鍵)、priority(相對於其他消息的優先權)、delivery-mode(指出該消息可能需要持久性存儲)等。

  • Publisher:消息的生產者,也是一個向交換器發佈消息的客戶端應用程序

  • Exchange:交換器,用來接收生產者發送的消息並將這些消息路由給服務器中的隊列。Exchange有4種類型:

    • direct(默認)
    • fanout
    • topic
    • headers

    不同類型的Exchange轉發消息的策略有所區別

  • Queue:消息隊列,用來保存消息直到發送給消費者。它是消息的容器,也是消息的終點。一個消息可投入一個或多個隊列。消息一直在隊列裏面,等待消費者連接到這個隊列將其取走。

  • Binding:綁定,用於消息隊列和交換器之間的關聯。一個綁定就是基於路由鍵將交換器和消息隊列連接起來的路由規則,所以可以將交換器理解成一個由綁定構成的路由表。Exchange 和Queue的綁定可以是多對多的關係。

  • Connection:網絡連接,比如一個TCP連接

  • Channel:信道,多路複用連接中的一條獨立的雙向數據流通道。信道是建立在真實的TCP連接內的虛擬連接,AMQP 命令都是通過信道發出去的,不管是發佈消息、訂閱隊列還是接收消息,這些動作都是通過信道完成。因爲對於操作系統來說建立和銷燬TCP 都是非常昂貴的開銷,所以引入了信道的概念,以複用一條TCP 連接

  • Consumer:消息的消費者,表示一個從消息隊列中取得消息的客戶端應用程序

  • Virtual Host:虛擬主機,表示一批交換器、消息隊列和相關對象。虛擬主機是共享相同的身份認證和加密環境的獨立服務器域。每個vhost 本質上就是一個mini 版的RabbitMQ 服務器,擁有自己的隊列、交換器、綁定和權限機制。vhost 是AMQP 概念的基礎,必須在連接時指定,RabbitMQ 默認的vhost 是/

  • Broker:表示消息隊列服務器實體

在這裏插入圖片描述

2.2 運行機制

AMQP 中增加了Exchange和Binding的角色。生產者把消息發佈到Exchange 上,消息最終到達隊列並被消費者接收,而Binding 決定交換器的消息應該發送到那個隊列。

2.3 Exchange

Exchange共有direct、fanout、topic和headers四種類型,下面重點看一下前三種:

2.3.1 direct

在這裏插入圖片描述

direct是一種完全匹配且單播的模式,當消息中的路由鍵和Binding中的binding key完全一致時,交換器纔將消息發到對應的消息隊列中。

2.3.2 fanout

在這裏插入圖片描述

fanout是一種廣播的模式,它並處理路由鍵,只是簡單的將隊列綁定到交換器上,每個發到fanout類型的交換器的消息都會被髮到所有綁定的隊列上。因此,fanout類型轉發速度是最快的。

2.3.3 topic

在這裏插入圖片描述

topic交換器通過模式匹配分配消息的路由鍵屬性,將路由鍵和某個模式進行匹配。此時隊列需要綁定到一個模式上。它將路由鍵和綁定鍵的字符切分爲單詞,這些單詞之間用點隔開。它同樣會識別兩個通配符:

  • #:匹配0個或是多個單詞
  • *:匹配一個單詞

2.4 雲服務器安裝

2.4.1. docker安裝rabbitmq
  • 查詢rabbitmq相關鏡像文件

    [root@izbp15ffbqqbe97j9dcf5dz ~]# docker search rabbitmq   
    
  • 拉取鏡像,這裏選擇帶管理平臺的*-management版本

    [root@izbp15ffbqqbe97j9dcf5dz ~]# docker pull rabbitmq:3.8.5-management
    Trying to pull repository docker.io/library/rabbitmq ... 
    3.8.5-management: Pulling from docker.io/library/rabbitmq
    d7c3167c320d: Pull complete 
    131f805ec7fd: Pull complete 
    322ed380e680: Pull complete 
    6ac240b13098: Pull complete 
    58ab633708c7: Pull complete 
    4ef7b4c52e3f: Pull complete 
    0bcc8241708b: Pull complete 
    4bbf89f47f34: Pull complete 
    2dcee968b577: Pull complete 
    b7f34b3a6ae9: Pull complete 
    57f11a70a594: Pull complete 
    6a2415c21710: Pull complete 
    Digest: sha256:4739d28990182895e27e726414801408c85db5c7af285eb689f2b67cd45c1b29
    Status: Downloaded newer image for docker.io/rabbitmq:3.8.5-management
    [root@izbp15ffbqqbe97j9dcf5dz ~]# docker pull rabbitmq:3.8.5-management
    
  • 查看docker中的鏡像列表

    [root@izbp15ffbqqbe97j9dcf5dz ~]# docker images;
    REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
    docker.io/rabbitmq   3.8.5-management    95bc78c8d15d        3 days ago          187 MB
    docker.io/redis      latest              235592615444        10 days ago         104 MB
    docker.io/mysql      latest              be0dbf01a0f3        11 days ago         541 MB
    
  • 運行rabbitmq並進行端口映射

    [root@izbp15ffbqqbe97j9dcf5dz ~]# docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq 95bc78c8d15d
    
  • 查看是否運行成功

    [root@izbp15ffbqqbe97j9dcf5dz ~]# docker ps
    CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                                                                        NAMES
    afc7a1ab33ce        95bc78c8d15d        "docker-entrypoint..."   3 seconds ago       Up 3 seconds        4369/tcp, 5671/tcp, 0.0.0.0:5672->5672/tcp, 15671/tcp, 25672/tcp, 0.0.0.0:15672->15672/tcp   rabbitmq
    
2.4.2 安全規則配置

登錄雲服務器,選擇左側的安全組中的配置規則,手動添加訪問規則
在這裏插入圖片描述

2.4.3 登錄

使用ip地址:端口號的方式在瀏覽器中登錄rabbitmq,如果看到如下界面,表示rabbitmq安裝成功
在這裏插入圖片描述

使用默認的用戶名guest和密碼guest登錄,進行管理界面,然後就可以使用啦~
在這裏插入圖片描述

2.4.4. 簡單實驗
  • 登錄RabbitMQ控制檯,添加三種Exchange
    在這裏插入圖片描述

  • 添加4個隊列
    在這裏插入圖片描述

  • Exchange綁定隊列
    在這裏插入圖片描述
    在這裏插入圖片描述
    在這裏插入圖片描述

  • 發送消息
    在這裏插入圖片描述

  • 查看隊列,接收消息
    在這裏插入圖片描述

  • 查看具體接受到的信息
    在這裏插入圖片描述

3. Spring Boot整合RabbitMQ

3.1 環境搭建

Spring Boot中整合RabbitMQ首先需要導入依賴:

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

Spring Boot中提供了和RabbitMQ相關的自動配置類RabbitAutoConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Import(RabbitAnnotationDrivenConfiguration.class)
public class RabbitAutoConfiguration {

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnMissingBean(ConnectionFactory.class)
	protected static class RabbitConnectionFactoryCreator {

		@Bean
		public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties properties,
				ObjectProvider<ConnectionNameStrategy> connectionNameStrategy) throws Exception {
			PropertyMapper map = PropertyMapper.get();
			CachingConnectionFactory factory = new CachingConnectionFactory(
					getRabbitConnectionFactoryBean(properties).getObject());
			map.from(properties::determineAddresses).to(factory::setAddresses);
			map.from(properties::isPublisherReturns).to(factory::setPublisherReturns);
			map.from(properties::getPublisherConfirmType).whenNonNull().to(factory::setPublisherConfirmType);
			RabbitProperties.Cache.Channel channel = properties.getCache().getChannel();
			map.from(channel::getSize).whenNonNull().to(factory::setChannelCacheSize);
			map.from(channel::getCheckoutTimeout).whenNonNull().as(Duration::toMillis)
					.to(factory::setChannelCheckoutTimeout);
			RabbitProperties.Cache.Connection connection = properties.getCache().getConnection();
			map.from(connection::getMode).whenNonNull().to(factory::setCacheMode);
			map.from(connection::getSize).whenNonNull().to(factory::setConnectionCacheSize);
			map.from(connectionNameStrategy::getIfUnique).whenNonNull().to(factory::setConnectionNameStrategy);
			return factory;
		}

		private RabbitConnectionFactoryBean getRabbitConnectionFactoryBean(RabbitProperties properties)
				throws Exception {
			PropertyMapper map = PropertyMapper.get();
			RabbitConnectionFactoryBean factory = new RabbitConnectionFactoryBean();
			map.from(properties::determineHost).whenNonNull().to(factory::setHost);
			map.from(properties::determinePort).to(factory::setPort);
			map.from(properties::determineUsername).whenNonNull().to(factory::setUsername);
			map.from(properties::determinePassword).whenNonNull().to(factory::setPassword);
			map.from(properties::determineVirtualHost).whenNonNull().to(factory::setVirtualHost);
			map.from(properties::getRequestedHeartbeat).whenNonNull().asInt(Duration::getSeconds)
					.to(factory::setRequestedHeartbeat);
			map.from(properties::getRequestedChannelMax).to(factory::setRequestedChannelMax);
			RabbitProperties.Ssl ssl = properties.getSsl();
			if (ssl.determineEnabled()) {
				factory.setUseSSL(true);
				map.from(ssl::getAlgorithm).whenNonNull().to(factory::setSslAlgorithm);
				map.from(ssl::getKeyStoreType).to(factory::setKeyStoreType);
				map.from(ssl::getKeyStore).to(factory::setKeyStore);
				map.from(ssl::getKeyStorePassword).to(factory::setKeyStorePassphrase);
				map.from(ssl::getTrustStoreType).to(factory::setTrustStoreType);
				map.from(ssl::getTrustStore).to(factory::setTrustStore);
				map.from(ssl::getTrustStorePassword).to(factory::setTrustStorePassphrase);
				map.from(ssl::isValidateServerCertificate)
						.to((validate) -> factory.setSkipServerCertificateValidation(!validate));
				map.from(ssl::getVerifyHostname).to(factory::setEnableHostnameVerification);
			}
			map.from(properties::getConnectionTimeout).whenNonNull().asInt(Duration::toMillis)
					.to(factory::setConnectionTimeout);
			factory.afterPropertiesSet();
			return factory;
		}

	}

	@Configuration(proxyBeanMethods = false)
	@Import(RabbitConnectionFactoryCreator.class)
	protected static class RabbitTemplateConfiguration {

		@Bean
		@ConditionalOnMissingBean
		public RabbitTemplateConfigurer rabbitTemplateConfigurer(RabbitProperties properties,
				ObjectProvider<MessageConverter> messageConverter,
				ObjectProvider<RabbitRetryTemplateCustomizer> retryTemplateCustomizers) {
			RabbitTemplateConfigurer configurer = new RabbitTemplateConfigurer();
			configurer.setMessageConverter(messageConverter.getIfUnique());
			configurer
					.setRetryTemplateCustomizers(retryTemplateCustomizers.orderedStream().collect(Collectors.toList()));
			configurer.setRabbitProperties(properties);
			return configurer;
		}

		@Bean
		@ConditionalOnSingleCandidate(ConnectionFactory.class)
		@ConditionalOnMissingBean(RabbitOperations.class)
		public RabbitTemplate rabbitTemplate(RabbitTemplateConfigurer configurer, ConnectionFactory connectionFactory) {
			RabbitTemplate template = new RabbitTemplate();
			configurer.configure(template, connectionFactory);
			return template;
		}

		@Bean
		@ConditionalOnSingleCandidate(ConnectionFactory.class)
		@ConditionalOnProperty(prefix = "spring.rabbitmq", name = "dynamic", matchIfMissing = true)
		@ConditionalOnMissingBean
		public AmqpAdmin amqpAdmin(ConnectionFactory connectionFactory) {
			return new RabbitAdmin(connectionFactory);
		}

	}

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(RabbitMessagingTemplate.class)
	@ConditionalOnMissingBean(RabbitMessagingTemplate.class)
	@Import(RabbitTemplateConfiguration.class)
	protected static class MessagingTemplateConfiguration {

		@Bean
		@ConditionalOnSingleCandidate(RabbitTemplate.class)
		public RabbitMessagingTemplate rabbitMessagingTemplate(RabbitTemplate rabbitTemplate) {
			return new RabbitMessagingTemplate(rabbitTemplate);
		}

	}

}

RabbitAutoConfiguration中有和自動配置相關的連接工廠ConnectionFactory,並通過RabbitProperties 封裝了RabbitMQ的配置。因此,可以在application.properties中添加和RabbitMQ相關的配置

spring.rabbitmq.addresses=xxxx
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

此外,自動配置類還添加了如下的兩個組件:

  • RabbitTemplate :給RabbitMQ發送和接受消息
  • AmqpAdmin : RabbitMQ系統管理功能組件,它可以用於創建和刪除 Queue、Exchange、Binding

此外還提供了和RabbitMQ相關的註解:

  • @EnableRabbit:開啓RabbitMQ的註解支持
  • @RabbitListener :設置監聽器,監聽消息隊列的內容

3.2 實驗

RabbitAutoConfiguration提供了RabbitTemplate 和AmqpAdmin 兩個組件用於操作消息隊列,下面我們通過單元測試的方式來看一下如何使用。首先設置它們各自的對象,並通過@Autowired實現自動裝配

@Autowired
RabbitTemplate rabbitTemplate;
@Autowired
AmqpAdmin amqpAdmin;

測試方法

@Test
public void createExchange(){
    amqpAdmin.declareExchange(new DirectExchange("amqpadmin.exchange"));
    System.out.println("創建完成");

    amqpAdmin.declareQueue(new Queue("amqpadmin.queue", true));

    amqpAdmin.declareBinding(new Binding("amqpadmin.queue",
                                         Binding.DestinationType.QUEUE,
                                         "amqpadmin.exchange",
                                         "amqp.#",
                                         null));

}

使用amqpAdmin.declareExchange()可以添加一個交換器,方法的參數是如下所示的某一個Exchange接口實現類對象
在這裏插入圖片描述

DirectExchange()構造方法中需傳入交換器的name。
在這裏插入圖片描述

類似,declareQueue()用於聲明一個隊列
在這裏插入圖片描述

declareBinding()用於實現交換器和隊列之間的綁定。
在這裏插入圖片描述

rabbitTemplate中的convertAndSend(exchage,routeKey,object)可用於向指定的交換器中傳遞數據,並設置路由鍵。其中object默認當成消息體,只需要傳入要發送的對象,自動序列化發送給rabbitmq

@Test
public void testPutMessage() {
    Map<String,Object> map = new HashMap<>();
    map.put("msg","first message");
    map.put("data", Arrays.asList("hello world",123,true));
    //對象被默認序列化以後發送出去
    rabbitTemplate.convertAndSend("amqpadmin.exchange","amqp.#",new Book(1,"西遊記"));

}

執行單元測試,可以從消息隊列中獲取到傳入的message
在這裏插入圖片描述

另外receiveAndConvert()可用於從指定的隊列中獲取元素

@Test
public void receive(){
    Object o = rabbitTemplate.receiveAndConvert("amqpadmin.queue");
    System.out.println(o.getClass());
    System.out.println(o);
}

執行單元測試,控制檯輸出

class dyliang.domain.Book
dyliang.domain.Book@3d7fb838

其他更多的方法可以根據RabbitTemplate 和AmqpAdmin類中定義的方法選擇使用。

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