文章目錄
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類中定義的方法選擇使用。