参考文档:https://www.kuangstudy.com/zl/rabbitmq#1366240777285423106
(一)、安装
使用docker-compose安装
version: "3"
services:
rabbitmq:
image: rabbitmq:3-management
ports:
- 5672:5672
- 15672:15672
volumes:
- ./rabbitmq:/var/lib/rabbitmq
environment:
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=admin
1、拉取 RabbitMQ 镜像
我这边选择的版本是 rabbitmq:3.12-management在终端中执行以下命令以拉取 rabbitmq:3.12-management根据自己使用过的版本:
镜像尽量选择 带-management后缀的,因为这个是自带Web监控页面,同3.12版本MQ有两个
docker pull rabbitmq:3.12-management
docker pull rabbitmq:3.12 这个是不带Web管理页面的,是需要自己手动安装插件
docker pull rabbitmq:3.12-management
2、创建并运行容器
使用以下命令创建一个新的 rabbitmq容器并将其启动:
docker run --name myrabbit -p 5672:5672 -p 15672:15672 -d rabbitmq:3.12-management
–name 是 容器别名,将 宿主机 5672端口映射到 容器内5672,and 端口15672端口映射到 容器内15672 端口,访问宿主机端口的时候会映射到对应容器端口, -d 表示后台运行。
3、RabbitMQ 常用端口以及作用
-
5672端口:AMQP(Advanced Message Queuing Protocol)协议的默认端口,用于客户端与RabbitMQ服务器之间的通信。
-
15672端口:RabbitMQ的管理界面,默认使用HTTP协议,用于监控和管理RabbitMQ服务器。
-
4369端口:Erlang分布式节点通信端口,用于RabbitMQ节点之间的通信。
-
25672端口:Erlang分布式节点通信端口,用于集群中的内部通信。
-
5671端口:安全的AMQP端口,使用TLS/SSL进行加密通信。
4、访问 管理页面测试,是否启动成功
http://ip:15672/
RabbitMQ默认的登录账号和密码如下:
- 用户名:guest
- 密码: guest
主页:
-
connections:无论生产者还是消费者,都需要与RabbitMQ建立连接后才可以完成消息的生产和消费,在这里可以查看连接情况
-
channels:通道,建立连接后,会形成通道,消息的投递获取依赖通道。
-
Exchanges:交换机,用来实现消息的路由
-
Queues:队列,即消息队列,消息存放在队列中,等待消费,消费后被移除队列。
(二)、简单测试
1、在publisher中发送消息 在consumer接收消息
一、创建一个maven项目 共两个子模块 publisher 和 consumer
二、在父工程中添加依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
三、添加yml配置 (消费者和生产者都需要添加)
#mq配置
spring:
rabbitmq:
username: admin #用户名
password: admin #密码
host: ip #主机ip
port: 5672 # 端口
virtual-host: /host #虚拟主机名称
四、在publisher模块中 创建test测试简单的发送消息
@SpringBootTest
public class TestSendMessages {
@Resource
private RabbitTemplate rabbitTemplate;
@Test
public void testSend(){
String queueName = "zzr.queue";
String message = "hello RabbitMQ ! 又发送一条了";
rabbitTemplate.convertAndSend(queueName,message);
}
}
五、在consumer模块中 创建接收消息类 使用注解@RabbitListener接收
@Component
public class ConsumerConfig {
@RabbitListener(queues = {"zzr.queue"})
public void listener(String msg){
System.out.println("接收消息:【" + msg + "】");
}
}
- 启动项目 会接收到由publisher模块发送的消息
2、测试、多个消费者绑定一个队列 共同消费队列中的消息
-
在可视化界面中 创建队列work.queue
-
在publisher模块中 添加代码
-
@Test public void testWorkQueueSend() throws InterruptedException { String queueName = "work.queue"; for (int i = 1; i <= 50; i++) { String message = "hello RabbitMQ ! 发送第" + i +"条消息!"; rabbitTemplate.convertAndSend(queueName,message); Thread.sleep(200); } }
-
在consumer模块中 新增两个方法消费work.queue 队列中的消息
-
/** * 测试两个消费者接受同一个队列的消息 * @param msg 接受的消息 */ @RabbitListener(queues = {"work.queue"}) public void workListener1(String msg){ System.out.println("消费者1 接收消息:【" + msg + "】"); } @RabbitListener(queues = {"work.queue"}) public void workListener2(String msg){ System.out.println("消费者2 接收消息:【" + msg + "】"); }
- 得到的结果为 消费者1 与 **消费者2 ** 消费的数量都为25个
- 如果一个队列绑定了一个消费者 生产者发送了一条消息 这个消息只会被消费一次 并且消费的逻辑为 轮循
(三)交换机
- 在RabbitMQ中,生产者发送信息不会直接将消息投递到队列中,而是将消息投递到交换机中,再由交换机转发到具体的队列中,队列再将消息以推送或者拉取方式给消费进行消费
一、交换机(Exchange)的类型
1.直连交换机:Direct Exchange
-
直连交换机是最简单的交换机类型,它将消息的路由键与绑定键进行精确匹配,当我们的路由键和绑定键一致的时候,将消息发送到与之完全匹配的队列。
-
像上图所描述的我们的路由键是orange就会进入对应的绑定键的orange队列中
-
如果是两个相同的绑定键则都会进入,同时推送到Q1和Q2队列中
****注意:****直连交换机只能根据绑定键进行路由,无法实现更复杂的路由逻辑。这意味着在需要进行高级路由或消息过滤的情况下,直连交换机可能无法满足需求。如果我们需要一个消息发送到多个队列中需要在交换机上绑定多个路由键,也是非常的麻烦
代码实现:
-
创建两个队列queues (direct.zzr1,direct.zzr2)
-
创建交换机Exchange (direct_zzr )类型选择direct
-
绑定消息队列 direct.zzr1 设置key为red 再次绑定direct.zzr1 设置key为yellow
-
绑定消息队列 direct.zzr2 设置key为red 再次绑定direct.zzr2 设置key为blue
-
在consumer模块中 创建两个方法 监听消息
-
@RabbitListener(queues = {"direct.zzr1"}) public void workDirect1(String msg){ System.out.println("消费者direct.zzr1 接收消息:【" + msg + "】"); } @RabbitListener(queues = {"direct.zzr2"}) public void workDirect2(String msg){ System.out.println("消费者direct.zzr2 接收消息:【" + msg + "】"); }
-
在publisher模块中 模拟发送消息携带不同的key
-
//当key为 red时 消费者1和消费者2都能接收到消息 @Test public void testDirectSend1() { String fanoutName = "direct_zzr"; rabbitTemplate.convertAndSend(fanoutName,"red","hello,everyOne"); } //当key为 blue时 只有消费者2能接收到消息 @Test public void testDirectSend2() { String fanoutName = "direct_zzr"; rabbitTemplate.convertAndSend(fanoutName,"blue","hello,everyOne"); } //当key为 yellow时 只有消费者1能接收到消息 @Test public void testDirectSend3() { String fanoutName = "direct_zzr"; rabbitTemplate.convertAndSend(fanoutName,"yellow","hello,everyOne"); } //最后依次执行的结果为: testDirectSend1() 消费者fanout.zzr1 接收消息:【hello,everyOne】 消费者fanout.zzr2 接收消息:【hello,everyOne】 testDirectSend2() 消费者fanout.zzr2 接收消息:【hello,everyOne】 testDirectSend3() 消费者fanout.zzr1 接收消息:【hello,everyOne】
2.主题交换机:Topic Exchange
- 主题交换机基于模式匹配的方式将消息路由到队列。它使用通配符来进行匹配,支持通配符符号 “" 和 “#”。其中 "” 表示匹配一个单词,“#” 表示匹配一个或多个单词。
主题交换机就像是升级版的直连交换机一样,通过绑定键进行访问,但是不同的地方是主题交换机有两个特殊字符一个是*号另一个是#,通过这两个符合可以设定不同的条件,只有符合条件才会发送到对应的队列中。
为了方便大家理解我写几个案例:
- RoutingKey:aa.orange.bb ====>Q1
- RoutingKey:aa.orange.rabbit====>Q1,Q2
- RoutingKey:aa.bb.rabbit====>Q2
- RoutingKey:lazy.aa====>Q2
- RoutingKey:lazy.aa.rabbit====>Q2
- RoutingKey:lazy.orange.aa====>Q1,Q2
- RoutingKey:lazy.orange.rabbit====>Q1,Q2
知识拓展:
-
当一个队列的绑定键是#,它将会接收所有的消息,而不再考虑所接收消息的路由键
-
当一个队列的绑定键没有用到#和*时,它就像直连交换机一样工作
代码展示:
-
创建两个队列:topic.zzr1 topic.zzr2
-
创建交换机: topic_zzr
-
绑定 topic.zzr1 key为 #.rabbitmq
-
绑定 topic.zzr2 key为 java.#
-
在consumer模块中 创建两个方法 监听消息
-
@RabbitListener(queues = {"topic.zzr1"}) public void workTopic1(String msg){ System.out.println("消费者topic.zzr1 接收消息:【" + msg + "】"); } @RabbitListener(queues = {"topic.zzr2"}) public void workTopic2(String msg){ System.out.println("消费者topic.zzr2 接收消息:【" + msg + "】"); }
-
在publisher模块中 模拟发送消息携带不同的key
-
// 发送消息 以java开头的 那么topic.zzr2可以接收 @Test public void testTopicSend2() { String fanoutName = "topic_zzr"; rabbitTemplate.convertAndSend(fanoutName,"java.news","hello,everyOne"); } //执行结果:消费者topic.zzr2 接收消息:【hello,everyOne】 // 发送消息 以rebbitmq结尾的key可以接收 @Test public void testTopicSend3() { String fanoutName = "topic_zzr"; rabbitTemplate.convertAndSend(fanoutName,"news.rabbitmq","hello,everyOne"); } //执行结果:消费者topic.zzr1 接收消息:【hello,everyOne】
3.扇形交换机:Fanout Exchange
- 扇形交换机将消息广播到所有与之绑定的队列。无论消息的路由键是什么,扇形交换机都会将消息发送到所有绑定的队列中。这种类型的交换机常用于实现发布-订阅模式,将消息广播给多个消费者。
4.首部交换机:Headers exchange
- 首部交换机和扇形交换机一样都不要路由键,首交换机根据消息Headers的属性进行匹配和路由。在消息发送时,可以指定一组键值对作为消息的头部属性,交换机会根据这些属性进行匹配。首部交换机提供了更灵活的匹配方式,但相对复杂度较高,通常使用较少。
注意:Hash结构中要求携带一个键“x-match”,这个键的Value可以是any或者all,这代表消息携带的Hash是仅匹配一个(any)还是需要全部匹配(all)
- all:在发布消息时携带的所有Entry必须和绑定在队列上的所有Entry完全匹配
- any:只要发布消息时携带的有一对键值对headers满足队列定义的多个参数arguments的其中一个就能匹配上,注意这里是键值对的完全匹配,只要匹配到键,值却是不一样的
5.默认交换机:Default Exchange
- 默认交换机是一个预定义的无名交换机,它会自动将消息发送到与之路由键名称相同的队列中。当生产者没有显式地指定交换机时,消息会被发送到默认交换机中。
- **注意:**默认交换机有一个特殊属性是默认交换机会自动将新建队列绑定到自己身上,并且绑定的路由键名称与队列名称一致。 也就是说当创建一个新的队列时,如果未显式地指定要绑定的交换机,那么该队列将自动绑定到默认交换机上。
6.死信交换机:Dead Letter Exchange
- 死信交换机用于处理无法被消费者正确处理的消息。当消息在队列中变成死信(例如超过重试次数或队列已满),它将被发送到死信交换机,并根据死信交换机的绑定规则路由到指定的死信队列中进行进一步处理。
通常是以下三种情况:
- 消息被拒绝,并且设置 requeue 参数为 false
- 消息过期(默认情況下 Rabbit 中的消息不过期,但是可以设置队列的过期时间和消息的过期时间以达到消息过期的效果)
- 队列达到最大长度(一般当设置了最大队列长度或大小并达到最大值时)
- 满足以上的任意一种就会变成死信消息被我们的死信交换机接收到并发送给队列
案例讲解
生产者生产一条1分钟后超时的订单消息到正常交换机exchange-a中,消息匹配到队列queue-a,但一分钟后仍未消费。 消息会被投递到死信交换机dlxy-exchange中,并发送到死信队列中, 死信队列dlx-queue的消费者拿到消息后,根据消息去查询订单的状态,如果仍然是未支付状态,将订单状态更新为超时状态。