学习和使用RabbitMQ
Centos7安装RabbitMQ
安装链接参考:https://www.cnblogs.com/lipg/p/14291863.html
需要注意的是erlang的版本,具体版本之间的依赖关系请查看官方网址:https://www.rabbitmq.com/which-erlang.html
RabbitMQ介绍
简介
RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而群集和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库
RabbitMQ 特点
1、可靠性(Reliability)
RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认、事务等。
2、灵活的路由(Flexible Routing)
在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。
3、消息集群(Clustering)
多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。
4、高可用(Highly Available Queues)
队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
5、多种协议(Multi-protocol)
RabbitMQ 支持多种消息队列协议,比如 STOMP、MQTT 等等。
6、多语言客户端(Many Clients)
RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby 等等。
7、管理界面(Management UI)
RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker 的许多方面。
8、跟踪机制(Tracing)
如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。
9、插件机制(Plugin System)
RabbitMQ 提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。
RabbitMQ 中的一些基本概念
1、Message
消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
2、Publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
3、Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
4、Binding
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
5、Queue
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
6、Connection
网络连接,比如一个TCP连接。
7、Channel
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
8、Consumer
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
9、Virtual Host
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。
10、Broker
表示消息队列服务器实体。
Exchange类型
Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键。
direct
fanout
它会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。
topic
前面讲到 direct 类型的交换器路由规则是完全匹配 BindingKey RoutingKey ,但是这种严
格的匹配方式在很多情况下不能满足实际业务的需求。 ωpic 类型的交换器在匹配规则上进行了
扩展,它与 direct 类型的交换器相似,也是将消息路由到 BindingKey RoutingKey 相匹配的队
列中,但这里的匹配规则有些不同,它约定:
- RoutingKey 为一个点号.分隔的字符串(被点号.分隔开的每段独立的字符串称为一个单词,如“com.rabbi q.client”;
- BindingKey RoutingKey 样也是点号.分隔的字符串;
- BindingKey 中可以存在两种特殊字符串*和#,用于做模糊匹配,其中#用于匹配 个单词,*用于匹配多规格单词(可以是零个)。
headers
headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中
headers 属性进行匹配。在绑定队列和交换器时制定一组键值对 当发送消息到交换器时,
RabbitM 会获取到该消息的 headers (也是一个键值对的形式) ,对比其中的键值对是否完全
匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由
到该队列 headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。
SpringBoot整合RabbitMQ
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>rabbitmqdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rabbitmqdemo</name>
<description>rabbitmqdemo</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yml配置
server:
port: 8081
spring:
rabbitmq:
virtual-host: /
host: 127.0.0.1
port: 5672
username: guest
password: guest
publisher-confirm-type: correlated
listener:
simple:
acknowledge-mode: auto
# 重试机制
retry:
enabled: true #是否开启消费者重试
max-attempts: 3 #最大重试次数
initial-interval: 2000ms #重试间隔时间(单位毫秒)
# max-interval: 1200000ms #重试最大时间间隔(单位毫秒)
# multiplier: 2 #间隔时间乘子,间隔时间*乘子=下一次的间隔时间,最大不能超过设置的最大间隔时间
测试
直连交换机:Direct exchange
RabbitmqConfig.java
/**
* 直连direct模式
*/
public static final String DIRECT_QUEUE1 = "direct_queue1";
public static final String DIRECT_QUEUE2 = "direct_queue2";
public static final String DIRECT_QUEUE3 = "direct_queue3";
public static final String DIRECT_ROUTING_KEY1 = "direct_routing_key1";
public static final String DIRECT_ROUTING_KEY2 = "direct_routing_key2";
public static final String DIRECT_EXCHANGE = "direct_exchange";
@Bean
public Queue directQueue1() {
return new Queue(DIRECT_QUEUE1, true);
}
@Bean
public Queue directQueue2() {
return new Queue(DIRECT_QUEUE2, false);
}
@Bean
public Queue directQueue3() {
return new Queue(DIRECT_QUEUE3, true);
}
/**
* 创建直连交换机,参数为交换机的名称
*/
@Bean
public DirectExchange directExchange() {
return new DirectExchange(DIRECT_EXCHANGE);
}
/**
* 将三个队列都与该直连交换机绑定起来
*/
@Bean
public Binding bindingDirectExchange1() {
return BindingBuilder.bind(directQueue1()).to(directExchange()).with(DIRECT_ROUTING_KEY1);
}
@Bean
public Binding bindingDirectExchange2() {
return BindingBuilder.bind(directQueue2()).to(directExchange()).with(DIRECT_ROUTING_KEY1);
}
@Bean
public Binding bindingDirectExchange3() {
return BindingBuilder.bind(directQueue3()).to(directExchange()).with(DIRECT_ROUTING_KEY2);
}
MqSender
/**
* 给直连交换机发送消息
*/
public void sendDirectExchange(String message) {
//第一个参数指将消息发送到该名称的交换机,第二个参数为对应的routing_key,第三个参数为发送的具体消息
amqpTemplate.convertAndSend(RabbitmqConfig.DIRECT_EXCHANGE, RabbitmqConfig.DIRECT_ROUTING_KEY1, message);
amqpTemplate.convertAndSend(RabbitmqConfig.DIRECT_EXCHANGE, RabbitmqConfig.DIRECT_ROUTING_KEY2, message);
}
MqReceive
/**
* 直连交换机队列的消费者
*/
@RabbitListener(queues = RabbitmqConfig.DIRECT_QUEUE1)
public void directReceive1(String message) {
log.info("直连交换机队列1读取到消息:[{}]", message);
// throw new RuntimeException("直连交换机队列1读消息发生异常");
}
@RabbitListener(queues = RabbitmqConfig.DIRECT_QUEUE2)
public void directReceive2(String message) {
log.info("直连交换机队列2读取到消息:[{}]", message);
}
@RabbitListener(queues = RabbitmqConfig.DIRECT_QUEUE3)
public void directReceive3(String message) {
log.info("直连交换机队列3读取到消息:[{}]", message);
}
MqSendController
/**
* 发送直连消息
*
* @return
*/
@GetMapping("/direct")
public String directSend(@RequestParam String message) {
mqSender.sendDirectExchange(message);
return "success";
}
测试结果
请求地址:http://localhost:8081/api/mq/send/direct?message=test-direct
日志打印:
扇形交换机:Fanoutexchange
RabbitmqConfig.java
/**
* 扇形交换机:Fanout exchange
*/
public static final String FANOUT_QUEUE1 = "fanout_queue1";
public static final String FANOUT_QUEUE2 = "fanout_queue2";
public static final String FANOUT_QUEUE3 = "fanout_queue3";
public static final String FANOUT_EXCHANGE = "fanout_exchange";
@Bean
public Queue fanoutQueue1() {
return new Queue(FANOUT_QUEUE1, true);
}
@Bean
public Queue fanoutQueue2() {
return new Queue(FANOUT_QUEUE2, false);
}
@Bean
public Queue fanoutQueue3() {
return new Queue(FANOUT_QUEUE3, true);
}
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange(FANOUT_EXCHANGE);
}
@Bean
public Binding bindingFanoutExchange1() {
return BindingBuilder.bind(fanoutQueue1()).to(fanoutExchange());
}
@Bean
public Binding bindingFanoutExchange2() {
return BindingBuilder.bind(fanoutQueue2()).to(fanoutExchange());
}
@Bean
public Binding bindingFanoutExchange3() {
return BindingBuilder.bind(fanoutQueue3()).to(fanoutExchange());
}
MqSender
/**
* 给扇形交换机发送消息
*/
public void sendFanoutExchange(String message) {
amqpTemplate.convertSendAndReceive(RabbitmqConfig.FANOUT_EXCHANGE, "", message);
}
MqReceive
/**
* 扇形交换机队列的消费者
*/
@RabbitListener(queues = RabbitmqConfig.FANOUT_QUEUE1)
public void fanoutReceive1(String message) {
log.info("扇形交换机队列1读取到消息:[{}]", message);
}
@RabbitListener(queues = RabbitmqConfig.FANOUT_QUEUE2)
public void fanoutReceive2(String message) {
log.info("扇形交换机队列2读取到消息:[{}]", message);
}
@RabbitListener(queues = RabbitmqConfig.FANOUT_QUEUE3)
public void fanoutReceive3(String message) {
log.info("扇形交换机队列3读取到消息:[{}]", message);
}
MqSendController
/**
* 发送扇形消息
*
* @return
*/
@GetMapping("/fanout")
public String fanoutSend(@RequestParam String message) {
mqSender.sendFanoutExchange(message);
return "success";
}
测试结果
请求地址:http://localhost:8081/api/mq/send/fanout?message=test-fanout
日志打印:
主题交换机:Topic exchange
RabbitmqConfig.java
/**
* 主题交换机:Topic exchange 模式
*/
public static final String TOPIC_QUEUE1 = "topic.human";
public static final String TOPIC_QUEUE2 = "topic.human.man";
public static final String TOPIC_ROUTING_KEY1 = "topic.human";
public static final String TOPIC_ROUTING_KEY2 = "topic.#";
public static final String TOPIC_EXCHANGE = "topic_exchange";
@Bean
public Queue topicQueue1() {
return new Queue(TOPIC_QUEUE1, true);
}
@Bean
public Queue topicQueue2() {
return new Queue(TOPIC_QUEUE2, true);
}
@Bean
public TopicExchange topicExchange() {
return new TopicExchange(TOPIC_EXCHANGE);
}
/**
* 绑定topic交换机
*/
@Bean
public Binding bindingTopicExchange1() {
return BindingBuilder.bind(topicQueue1()).to(topicExchange()).with(TOPIC_ROUTING_KEY1);
}
@Bean
public Binding bindingTopicExchange2() {
return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with(TOPIC_ROUTING_KEY2);
}
MqSender
/**
* 给主题交换机发送消息
*/
public void sendTopicExchange1(String message) {
//第一个参数指将消息发送到该名称的交换机,第二个参数为对应的routing_key,第三个参数为发送的具体消息
amqpTemplate.convertAndSend(RabbitmqConfig.TOPIC_EXCHANGE, "topic.human", message);
}
/**
* 给主题交换机发送消息
*/
public void sendTopicExchange2(String message) {
//第一个参数指将消息发送到该名称的交换机,第二个参数为对应的routing_key,第三个参数为发送的具体消息
amqpTemplate.convertAndSend(RabbitmqConfig.TOPIC_EXCHANGE, "topic.human.man", message);
}
/**
* 给主题交换机发送消息
*/
public void sendTopicExchange3(String message) {
//第一个参数指将消息发送到该名称的交换机,第二个参数为对应的routing_key,第三个参数为发送的具体消息
amqpTemplate.convertAndSend(RabbitmqConfig.TOPIC_EXCHANGE, "topic.human.woman", message);
}
MqReceive
/**
* 主题交换机队列的消费者
*/
@RabbitListener(queues = RabbitmqConfig.TOPIC_QUEUE1)
public void topicReceive1(String message) {
log.info("主题交换机队列1读取到消息:[{}]", message);
}
@RabbitListener(queues = RabbitmqConfig.TOPIC_QUEUE2)
public void topicReceive2(String message) {
log.info("主题交换机队列2读取到消息:[{}]", message);
}
MqSendController
/**
* 发送主题消息
*
* @return
*/
@GetMapping("/topic1")
public String topicSend1(@RequestParam String message) {
mqSender.sendTopicExchange1(message);
return "success";
}
/**
* 发送主题消息
*
* @return
*/
@GetMapping("/topic2")
public String topicSend2(@RequestParam String message) {
mqSender.sendTopicExchange2(message);
return "success";
}
/**
* 发送主题消息
*
* @return
*/
@GetMapping("/topic3")
public String topicSend3(@RequestParam String message) {
mqSender.sendTopicExchange3(message);
return "success";
}
测试结果
请求地址:http://localhost:8081/api/mq/send/topic1?message=topic1
日志打印:
请求地址:http://localhost:8081/api/mq/send/topic2?message=topic2
日志打印:
请求地址:http://localhost:8081/api/mq/send/topic3?message=topic3
日志打印:
首部交换机:Headers exchange
几乎不用了,忽略
手动应答
application.yml
server:
port: 8081
spring:
rabbitmq:
virtual-host: /
host: 127.0.0.1
port: 5672
username: guest
password: guest
publisher-confirm-type: correlated
listener:
simple:
#none--默认都成功 auto--自动应答 manual--手动应答
acknowledge-mode: manual
# 重试机制
# retry:
# enabled: true #是否开启消费者重试
# max-attempts: 3 #最大重试次数
# initial-interval: 2000ms #重试间隔时间(单位毫秒)
# max-interval: 1200000ms #重试最大时间间隔(单位毫秒)
# multiplier: 2 #间隔时间乘子,间隔时间*乘子=下一次的间隔时间,最大不能超过设置的最大间隔时间
RabbitmqConfig
/**
* 测试confirm 机制,专门创建了一个队列
*
* @return
*/
@Bean
public Queue queueConfirm() {
return QueueBuilder
.durable("queue_confirm")
.build();
}
MqSender
/**
* 测试 confirm机制
*/
public void sendConfirm() {
template.convertAndSend("queue_confirm", "confirm--test");
// template.setConfirmCallback(confirmCallback);
}
MqReceive
先不进行任何应答
@RabbitListener(queues = "queue_confirm")
public void receiveMsg(String dto, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
System.out.println("接收到的queue_confirm消息为:" + dto);
System.out.println("channel为:" + channel.toString());
System.out.println("tag为:" + tag);
System.out.println("手动应答开始");
// try {
// String str = null;
// str.equals("aaa");
// channel.basicAck(tag, true);
// } catch (Exception e) {
// channel.basicNack(tag, false, true);
// }
}
效果:监听到了消息,但是没有应答
成功应答
@RabbitListener(queues = "queue_confirm")
public void receiveMsg(String dto, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
System.out.println("接收到的queue_confirm消息为:" + dto);
System.out.println("channel为:" + channel.toString());
System.out.println("tag为:" + tag);
System.out.println("手动应答开始");
channel.basicAck(tag, true);
// try {
// String str = null;
// str.equals("aaa");
// channel.basicAck(tag, true);
// } catch (Exception e) {
// channel.basicNack(tag, false, true);
// }
}
效果:重启服务之后,刚刚上一步没有应答的消息会被再次投送,然后进行消费和应答
失败应答
@RabbitListener(queues = "queue_confirm")
public void receiveMsg(String dto, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
System.out.println("接收到的queue_confirm消息为:" + dto);
System.out.println("channel为:" + channel.toString());
System.out.println("tag为:" + tag);
System.out.println("手动应答开始");
// channel.basicAck(tag, true);
try {
String str = null;
str.equals("aaa");
channel.basicAck(tag, true);
} catch (Exception e) {
//第三个参数(requeue)表示是否重新放回队列
channel.basicNack(tag, false, true);
}
}
效果:requeue为true时,应答失败会把消息返回队列,如果没有重试限制等会一直处于消费–》退回–》消费的循环中。如果requeue为false并且没有后续处理,则会丢弃这个消息。
死信队列
给上一步的手动应答加上死信队列用于处理requeue为false的情况
/**
* 死信队列
*
* @return
*/
public static final String DEAD_QUEUE = "dead_queue";
public static final String DEAD_Exchange = "dead_exchange";
public static final String DEAD_ROUTING_KEY = "dead_key";
/**
* 测试confirm 机制,专门创建了一个队列
*
* @return
*/
@Bean
public Queue queueConfirm() {
return QueueBuilder
.durable("queue_confirm")
.deadLetterExchange(DEAD_Exchange)
.deadLetterRoutingKey(DEAD_ROUTING_KEY)
.build();
}
/**
* 死信队列和交换机
*/
@Bean
public Queue deadQueue() {
return new Queue(DEAD_QUEUE, true);
}
@Bean
public DirectExchange deadExchange() {
return new DirectExchange(DEAD_Exchange);
}
/**
* 死信交换器与死信队列绑定
*/
@Bean
public Binding deadBinding() {
return BindingBuilder.bind(deadQueue()).to(deadExchange()).with(DEAD_ROUTING_KEY);
}
**注意:**给一个队列配上死信队列之后需要将这个队列删掉,重新创建才可以,不然服务启动就会报错
重新自动创建队列之后,通过管理页面可以看到会有一些额外的标志来表示这个队列有设置死信队列
其他代码:
/**
* 死信队列监听
*/
@RabbitListener(queues = RabbitmqConfig.DEAD_QUEUE)
public void deadReceive(String message) {
log.info("死信读取到消息:[{}],并入库", message);
}
死信队列的消息并没有进行应答,所以数量为1
可用于小型项目的思路
1、首先启动RabbitMQ服务,能集群尽量集群
2、交换器、队列设置为持久化,重要信息也设置为持久化
3、为重要队列设置死信队列(可以每个队列设置一个死信队列)
4、应答模式根据实际情况,考虑是否需要设置为手动
5、设置重试机制,比如重试三次,每两分钟重试一次,三次均不成功则进入死信队列
6、死信队列不做任何其他操作,仅打印日志以及信息入库,待其他补漏措施处理,有条件的进行邮件、短信等提醒
最后
篇幅和底层知识储备有限,关于RabbitMQ的介绍和使用先到这里,想了解更多关于RabbitMQ底层的知识可以通过数据和其他博客甚至源码去了解。
关于上面写的一些RabbitMQ用于一些小项目生产环境的设计和思路也欢迎大家评论探讨:
1、如何保证消息的稳定投送?(有事务和confirm机制,但是目前还没用过)
2、如何保证消息消费失败之后不丢失
3、如何保证消息不堆积、不会OOM
参考列表:
Centos安装RabbitMQ
消息队列之 RabbitMQ
书籍:RabbitMQ实战指南