本小节来和小伙伴们分享一下 RabbitMQ 的七种消息传递形式。一起来看看。

大部分情况下,我们可能都是在 Spring Boot 或者 Spring Cloud 环境下使用 RabbitMQ,因此本文我也主要从这两个方面来和大家分享 RabbitMQ 的用法。

1. RabbitMQ 架构简介

一图胜千言,如下:

04-RabbitMQ七种消息收发方式 - 图1

这张图中涉及到如下一些概念:

  1. 生产者(Publisher):发布消息到 RabbitMQ 中的交换机(Exchange)上。
  2. 交换机(Exchange):和生产者建立连接并接收生产者的消息。
  3. 消费者(Consumer):监听 RabbitMQ 中的 Queue 中的消息。
  4. 队列(Queue):Exchange 将消息分发到指定的 Queue,Queue 和消费者进行交互。
  5. 路由(Routes):交换机转发消息到队列的规则。

2. 准备工作

首先创建一个空工程

04-RabbitMQ七种消息收发方式 - 图2

工程名叫做mq_demo01

04-RabbitMQ七种消息收发方式 - 图3

在这个mq_demo01工程中用Spring Initializr的方式创建一个SpringBoot的module

04-RabbitMQ七种消息收发方式 - 图4

创建一个消费者的模块,模块名叫做consumer

04-RabbitMQ七种消息收发方式 - 图5

选择依赖

04-RabbitMQ七种消息收发方式 - 图6

项目创建成功后,在 application.properties 中配置 RabbitMQ 的基本连接信息,如下:

  1. # 配置RabbitMQ的基本信息
  2. # RabbitMQ的地址
  3. spring.rabbitmq.host=localhost
  4. # RabbitMQ的端口号
  5. spring.rabbitmq.port=5672
  6. spring.rabbitmq.username=guest
  7. spring.rabbitmq.password=guest
  8. # 这个可以不用给,默认就是/
  9. spring.rabbitmq.virtual-hosts=/

然后在org.javaboy.consumer.config包下面创建一个消息队列

  1. @Configuration
  2. public class RabbitConfig {
  3. public static final String QUEUE_NAME = "javaboy_queue";
  4. @Bean
  5. Queue javaboyQueue() {
  6. //1. 第一个参数是队列的名字
  7. //2。第二个参数是持久化,是什么意思呢?将来的消息会从交换机进入到消息队列里面去,如果设置成true在消息队列里面会把消息存下来,这样子如果RabbitMQ异常重启了,那你之前那些没有被消费的消息依然还在,相当于它把这些消息持久化到硬盘中去。如果设置成false,就是代表不给它持久化,那么如果RabbitMQ异常重启了,那么那些消息就会丢失。
  8. //3. 该队列是否具有排他性,这个队列是由哪个connection创建的,将来就只能由这一个connection去操作它,去消费它。如果设置为true的话,那么这个消息队列就只能由创建它的connection才能访问,其他的connection都不能访问这个消息队列。但是如果试图在不同的连接中重新声明或者访问排他性队列的话,那么系统会报一个资源被锁定的错误。同时对于排他性队列而言,当连接断开的时候,这个队列就会被自动地删除。
  9. //4。如果该队列没有消费者,那么是否自动删除该队列
  10. return new Queue(QUEUE_NAME, true, false, false);
  11. }
  12. }

然后在org.javaboy.consumer.receiver包下面创建一个消息的消费者

  1. /**
  2. * 这是一个消息消费者
  3. */
  4. @Component
  5. public class MsgReceiver {
  6. /**
  7. * 通过 @RabbitListener 注解指定该方法监听的消息队列,该注解的参数就是消息队列的名字
  8. * @param msg
  9. */
  10. @RabbitListener(queues = RabbitConfig.QUEUE_NAME)
  11. public void handleMsg(String msg) {
  12. System.out.println("msg = " + msg);
  13. }
  14. }

启动应用,在RabbitMQ的web管理页面的Connections的选项卡中就可以看到已经有一个连接了

04-RabbitMQ七种消息收发方式 - 图7

在RabbitMQ的web管理页面的Channels的选项卡中就可以看到已经有一个通道了

04-RabbitMQ七种消息收发方式 - 图8

在RabbitMQ的web管理页面的Queues的选项卡中就可以看到我们自己定义的队列

04-RabbitMQ七种消息收发方式 - 图9

我们可以在RabbitMQ的web管理页面的Queues选项卡中的指定的消息队列中发布消息

04-RabbitMQ七种消息收发方式 - 图10

消息发布后,我们可以在Idea的控制台看到这条我们在RabbitMQ的web管理页面中发布的消息了

04-RabbitMQ七种消息收发方式 - 图11

3. 消息收发

在 RabbitMQ 中,所有的消息生产者提交的消息都会交由 Exchange 进行再分配,Exchange 会根据不同的策略将消息分发到不同的 Queue 中。

RabbitMQ 官网介绍了如下几种消息分发的形式:

04-RabbitMQ七种消息收发方式 - 图12

04-RabbitMQ七种消息收发方式 - 图13

04-RabbitMQ七种消息收发方式 - 图14

这里给出了七种,其中第七种是消息确认,消息确认这块松哥之前发过相关的文章,传送门:

所以这里我主要和大家介绍前六种消息收发方式。

3.1. Hello World

咦?这个咋没有交换机?这个其实是默认的交换机,我们需要提供一个生产者一个队列以及一个消费者,不需要我们自己提供交换机,会有一个默认的交换机提供给我们。消息传播图如下:

04-RabbitMQ七种消息收发方式 - 图15

这个时候使用的其实是默认的直连交换机(DirectExchange)

来看看代码实现:

再创建一个新的Empty Project,工程名叫做mq_demo02

04-RabbitMQ七种消息收发方式 - 图16

在这个mq_demo02工程中用Spring Initializr的方式创建两个SpringBoot的module

04-RabbitMQ七种消息收发方式 - 图17

创建一个消息生产者的模块,模块名叫做publisher

04-RabbitMQ七种消息收发方式 - 图18

选择依赖

04-RabbitMQ七种消息收发方式 - 图19

然后以同样的方法在mq_demo02工程中创建一个消息的消费者的模块,模块名叫做consumer,勾选的依赖同上

04-RabbitMQ七种消息收发方式 - 图20

在mq_demo02工程的publisher模块的application.properties配置文件中作如下配置

  1. spring.rabbitmq.host=localhost
  2. spring.rabbitmq.port=5672
  3. spring.rabbitmq.username=guest
  4. spring.rabbitmq.password=guest
  5. spring.rabbitmq.virtual-host=/

在mq_demo02工程的publisher模块的org.javaboy.publisher.config包下面创建消息队列

  1. @Configuration
  2. public class RabbitConfig {
  3. public static final String QUEUE_NAME = "hello_world_queue_name";
  4. @Bean
  5. Queue helloWorldQueue() {
  6. return new Queue(QUEUE_NAME, true, false, false);
  7. }
  8. }

在mq_demo02工程的publisher模块的src/test/java目录的org.javaboy.publisher包的PublisherApplicationTests类中发布消息到消息队列

  1. @SpringBootTest
  2. class PublisherApplicationTests {
  3. @Autowired
  4. RabbitTemplate rabbitTemplate;
  5. @Test
  6. void contextLoads() {
  7. rabbitTemplate.convertAndSend(RabbitConfig.QUEUE_NAME, "hello 江南一点雨");
  8. }
  9. }

在mq_demo02工程的consumer模块的application.properties配置文件中作如下配置

  1. spring.rabbitmq.host=localhost
  2. spring.rabbitmq.port=5672
  3. spring.rabbitmq.username=guest
  4. spring.rabbitmq.password=guest
  5. spring.rabbitmq.virtual-host=/

在mq_demo02工程的consumer模块的org.javaboy.publisher.config包下面创建消息队列

  1. @Configuration
  2. public class RabbitConfig {
  3. public static final String QUEUE_NAME = "hello_world_queue_name";
  4. @Bean
  5. Queue helloWorldQueue() {
  6. return new Queue(QUEUE_NAME, true, false, false);
  7. }
  8. }

在mq_demo02工程的consumer模块的org.javaboy.publisher.receiver包下面创建消息消费者

  1. @Component
  2. public class MsgReceiver {
  3. @RabbitListener(queues = RabbitConfig.QUEUE_NAME)
  4. public void handleMsg(String msg) {
  5. System.out.println("msg = " + msg);
  6. }
  7. }

先运行consumer模块的应用,然后再运行publisher模块的单元测试方法contextLoads(),最后可以看到consumer模块的控制台收到了publisher模块发布的消息。

04-RabbitMQ七种消息收发方式 - 图21

我们在RabbitMQ的web管理页面的Queues选项卡中也可以看到我们创建的消息队列

04-RabbitMQ七种消息收发方式 - 图22

我们发消息的时候,消费者可以不用在线,这其实就是一个很好的解耦。比如:我们这里可以停止consumer的应用,然后在publisher应用再次发送消息仍然可以成功。我们可以在RabbitMQ的web管理页面的Queues选项卡中看到我们的消息队列hello_world_queue_name中的Ready的值是1表示我们的消息队列hello_world_queue_name中有一条消息待消费。当消费者重新上线后又可以继续消费消息队列中待消费(Ready)的消息。

04-RabbitMQ七种消息收发方式 - 图23

上图中的Unacked表示消息队列中未确认的消息数量,因为把消息发送给消费者让消费者去消费,消费者消费完之后需要告诉RabbitMQ这条消息我已经消费了。如果消息已经发送给消费者,但是消费者没有告诉RabbitMQ这条消息已经消费了,那么这就属于Unacked的状态。Ready + Unacked = Total

3.2. Work queues

这种情况是这样的:

一个生产者,一个默认的交换机(DirectExchange),一个队列,两个消费者,如下图:

04-RabbitMQ七种消息收发方式 - 图24

一个队列对应了多个消费者,默认情况下,由队列对消息进行平均分配,消息会被分到不同的消费者手中。消费者可以配置各自的并发能力,进而提高消息的消费能力,也可以配置手动 ack(ack就是确认消息),来决定是否要消费某一条消息。

来看看代码实现:

创建mq_demo03工程,原封不动的复制mq_demo02工程的代码到mq_demo03工程中

在mq_demo03工程的consumer应用的MsgReceiver类中中再添加一个消费者

  1. @RabbitListener(queues = RabbitConfig.QUEUE_NAME)
  2. public void handleMsg2(String msg) {
  3. System.out.println("msg2 = " + msg);
  4. }

启动consumer应用,然后再在mq_demo03工程的publisher应用的单元测试类中的测试方法中循环发送20条消息

  1. @SpringBootTest
  2. class PublisherApplicationTests {
  3. @Autowired
  4. RabbitTemplate rabbitTemplate;
  5. @Test
  6. void contextLoads() {
  7. for (int i = 0; i < 20; i++) {
  8. rabbitTemplate.convertAndSend(RabbitConfig.QUEUE_NAME, "hello 江南一点雨:" + i);
  9. }
  10. }
  11. }

运行上面的单元测试方法。我们发现consumer应用的控制台中无规律的打印了这20条消息

04-RabbitMQ七种消息收发方式 - 图25

通过我们的RabbitMQ的web管理页面的Channels选项卡可以看到此时有两个通道,其实就是对应mq_demo03工程的consumer应用的两个消费者

04-RabbitMQ七种消息收发方式 - 图26

mq_demo03工程的consumer应用中给消费者作并发能力的配置

  1. /**
  2. * concurrency 指的是并发数量,即这个消费者将开启 20 个子线程去消费消息
  3. *
  4. * @param msg
  5. */
  6. @RabbitListener(queues = RabbitConfig.QUEUE_NAME, concurrency = "20")
  7. public void handleMsg2(Message message,Channel channel) throws IOException {
  8. System.out.println("msg2 = " + msg+"--->"+Thread.currentThread().getName());
  9. }

启动consumer应用,然后在RabbitMQ的web管理页面的Channels选项卡可以看到此时一共有21个通道

04-RabbitMQ七种消息收发方式 - 图27

此时我们运行mq_demo03工程的publisher应用中的测试方法中发送消息,可以看到mq_demo03工程的consumer应用的控制台,几乎全部都是msg2这个消费者消费的,因为msg2这个消费者的并发能力强,它开了20个子线程去消费消息

04-RabbitMQ七种消息收发方式 - 图28

我们在SpringBoot应用中会自动ack(ack就是确认消息),我们也可以在consumer应用的配置文件application.properties中设置成手动确认

  1. # 手动 ack,注意这是要在消费者工程中配置,而不是在生产者工程中配置
  2. spring.rabbitmq.listener.simple.acknowledge-mode=manual

重启mq_demo03工程的consumer应用,然后在mq_demo03工程的publisher应用的测试方法中发送消息,可以看到consumer应用的控制台打印了这20条消息,然后再次重启mq_demo03工程的consumer应用,我们会发现consumer应用的控制台又打印了这20条消息,但是此时我们并没有在publisher应用中发送消息啊,我们可以通过RabbitMQ的web管理页面的Queues选项卡查看hello_world_queue_name的消息队列的Unacked的值是20,说明此时我们有20条消息未确认。原因是因为未确认的消息RabbitMQ就会认为没有成功消费,等消费者下次连接上RabbitMQ又会去发给消费者,让消费者重新消费这些未确认的消息。

我们可以在代码中手动ack确认消息,例如在mq_demo03工程的consumer应用中修改消费者代码

  1. @Component
  2. public class MsgReceiver {
  3. @RabbitListener(queues = RabbitConfig.QUEUE_NAME)
  4. public void handleMsg(Message message, Channel channel) throws IOException {
  5. System.out.println("receive = " + message.getPayload());
  6. //确认消息,就是告诉 RabbitMQ,这条消息我已经消费成功了
  7. channel.basicAck(((Long) message.getHeaders().get(AmqpHeaders.DELIVERY_TAG)), false);
  8. }
  9. /**
  10. * concurrency 指的是并发数量,即这个消费者将开启 20 个子线程去消费消息
  11. *
  12. * @param msg
  13. */
  14. @RabbitListener(queues = RabbitConfig.QUEUE_NAME, concurrency = "20")
  15. public void handleMsg2(Message message,Channel channel) throws IOException {
  16. //拒绝消费该消息,第二个参数requeue 表示被拒绝的消息是否重新进入队列中,如果设置成true,那么RabbitMQ又会把它发给另外的消费者去消费
  17. channel.basicReject((Long) message.getHeaders().get(AmqpHeaders.DELIVERY_TAG),true);
  18. }
  19. }

重启mq_demo03工程的consumer应用,然后在consumer应用的控制台可以看到全部的消息都被handleMsg消费了。此时第二个消费者拒绝了所有消息,第一个消费者handleMsg消费了所有消息。

04-RabbitMQ七种消息收发方式 - 图29

3.3. Publish/Subscribe

再来看发布订阅模式,这种情况是这样:

一个生产者,多个消费者,每一个消费者都有自己的一个队列,生产者没有将消息直接发送到队列,而是发送到了交换机,每个队列绑定交换机,生产者发送的消息经过交换机,到达队列,实现一个消息被多个消费者获取的目的。需要注意的是,如果将消息发送到一个没有队列绑定的 Exchange上面,那么该消息将会丢失,这是因为在 RabbitMQ 中 Exchange 不具备存储消息的能力,只有队列具备存储消息的能力,如下图:

04-RabbitMQ七种消息收发方式 - 图30

这种情况下,我们有四种交换机可供选择,分别是:

  • Direct
  • Fanout
  • Topic
  • Header

我分别来给大家举一个简单例子看下。

3.3.1. Direct

Direct交换机的工作方式:消息发送到Direct交换机上面,然后交换机和队列之间会有一个routing key把它们关联起来,然后我们发送消息的时候,会让消息带一个routing key,消息到Direct交换机上,交换机会根据消息携带的routing key来找消息对应的队列然后再发送到对应的队列上去,消费者直接消费队列就行。

演示代码:

创建一个新的Empty Project,工程名叫做mq_demo04

04-RabbitMQ七种消息收发方式 - 图31

在这个mq_demo04工程中用Spring Initializr的方式创建两个SpringBoot的module

04-RabbitMQ七种消息收发方式 - 图32

先创建一个消息生产者的模块,模块名叫publisher

04-RabbitMQ七种消息收发方式 - 图33

选择依赖

04-RabbitMQ七种消息收发方式 - 图34

然后以同样的方法在mq_demo04工程中创建一个消息的消费者的模块,模块名叫做consumer,勾选的依赖同上

04-RabbitMQ七种消息收发方式 - 图35

在mq_demo04工程的publisher模块和consumer模块的application.properties配置文件中都作如下配置:

  1. spring.rabbitmq.host=localhost
  2. spring.rabbitmq.port=5672
  3. spring.rabbitmq.username=guest
  4. spring.rabbitmq.password=guest
  5. spring.rabbitmq.virtual-host=/

在mq_demo04工程的publisher模块的org.javaboy.publisher.config包下创建消息队列

  1. /**
  2. * Direct:这种路由策略,将消息队列绑定到 DirectExchange 上,当消息到达交换机的时候,消息会携带一个 routing_key,然后交换机会找到名为 routing_key 的队列,将消息路由过去
  3. */
  4. @Configuration
  5. public class RabbitConfig {
  6. public static final String DIRECT_QUEUE_NAME = "direct_queue_name";
  7. public static final String DIRECT_QUEUE_NAME2 = "direct_queue_name2";
  8. public static final String DIRECT_EXCHANGE_NAME = "direct_exchange_name";
  9. @Bean
  10. Queue directQueue1() {
  11. return new Queue(DIRECT_QUEUE_NAME, true, false, false);
  12. }
  13. @Bean
  14. Queue directQueue2() {
  15. return new Queue(DIRECT_QUEUE_NAME2, true, false, false);
  16. }
  17. /**
  18. * 定义一个直连交换机
  19. * @return
  20. */
  21. @Bean
  22. DirectExchange directExchange() {
  23. //1. 交换机的名称
  24. //2。交换机是否持久化:这里的持久化指的是交换机本身能否持久化,如果你将它设为false,将来RabbitMQ重启之后,这个交换机就没有了,如果设置为true,将来RabbitMQ重启之后,这个交换机就还在。
  25. //3. 如果没有与之绑定的队列,是否删除交换机
  26. return new DirectExchange(DIRECT_EXCHANGE_NAME, true, false);
  27. }
  28. /**
  29. * 这个定义是将交换机和队列绑定起来
  30. * @return
  31. */
  32. @Bean
  33. Binding directBinding1() {
  34. return BindingBuilder
  35. //设置绑定的队列
  36. .bind(directQueue1())
  37. //设置绑定的交换机
  38. .to(directExchange())
  39. //设置 routing_key
  40. .with(DIRECT_QUEUE_NAME);
  41. }
  42. @Bean
  43. Binding directBinding2() {
  44. return BindingBuilder
  45. .bind(directQueue2())
  46. .to(directExchange())
  47. .with(DIRECT_QUEUE_NAME2);
  48. }
  49. }

在mq_demo04工程的consumer模块的org.javaboy.consumer.config包下创建消息队列

  1. /**
  2. * Direct:这种路由策略,将消息队列绑定到 DirectExchange 上,当消息到达交换机的时候,消息会携带一个 routing_key,然后交换机会找到名为 routing_key 的队列,将消息路由过去
  3. */
  4. @Configuration
  5. public class RabbitConfig {
  6. public static final String DIRECT_QUEUE_NAME = "direct_queue_name";
  7. public static final String DIRECT_QUEUE_NAME2 = "direct_queue_name2";
  8. public static final String DIRECT_EXCHANGE_NAME = "direct_exchange_name";
  9. @Bean
  10. Queue directQueue1() {
  11. return new Queue(DIRECT_QUEUE_NAME, true, false, false);
  12. }
  13. @Bean
  14. Queue directQueue2() {
  15. return new Queue(DIRECT_QUEUE_NAME2, true, false, false);
  16. }
  17. /**
  18. * 定义一个直连交换机
  19. * @return
  20. */
  21. @Bean
  22. DirectExchange directExchange() {
  23. //1. 交换机的名称
  24. //2。交换机是否持久化:这里的持久化指的是交换机本身能否持久化,如果你将它设为false,将来RabbitMQ重启之后,这个交换机就没有了,如果设置为true,将来RabbitMQ重启之后,这个交换机就还在。
  25. //3. 如果没有与之绑定的队列,是否删除交换机
  26. return new DirectExchange(DIRECT_EXCHANGE_NAME, true, false);
  27. }
  28. /**
  29. * 这个定义是将交换机和队列绑定起来
  30. * @return
  31. */
  32. @Bean
  33. Binding directBinding1() {
  34. return BindingBuilder
  35. //设置绑定的队列
  36. .bind(directQueue1())
  37. //设置绑定的交换机
  38. .to(directExchange())
  39. //设置 routing_key
  40. .with(DIRECT_QUEUE_NAME);
  41. }
  42. @Bean
  43. Binding directBinding2() {
  44. return BindingBuilder
  45. .bind(directQueue2())
  46. .to(directExchange())
  47. .with(DIRECT_QUEUE_NAME2);
  48. }
  49. }

在mq_demo04工程的consumer模块的org.javaboy.consumer.receiver包下创建消费者

  1. @Component
  2. public class MsgReceiver {
  3. @RabbitListener(queues = RabbitConfig.DIRECT_QUEUE_NAME)
  4. public void msgHandler(String msg) {
  5. System.out.println("msg = " + msg);
  6. }
  7. @RabbitListener(queues = RabbitConfig.DIRECT_QUEUE_NAME2)
  8. public void msgHandler2(String msg) {
  9. System.out.println("msg2 = " + msg);
  10. }
  11. }

先运行在mq_demo04工程的consumer应用

然后在mq_demo04工程的publisher模块的src/test/java目录的org.javaboy.publisher包的PublisherApplicationTests类中发布消息

  1. @SpringBootTest
  2. class PublisherApplicationTests {
  3. @Autowired
  4. RabbitTemplate rabbitTemplate;
  5. @Test
  6. void contextLoads() {
  7. rabbitTemplate.convertAndSend(RabbitConfig.DIRECT_EXCHANGE_NAME, RabbitConfig.DIRECT_QUEUE_NAME, "这条消息发给队列1");
  8. rabbitTemplate.convertAndSend(RabbitConfig.DIRECT_EXCHANGE_NAME, RabbitConfig.DIRECT_QUEUE_NAME2, "这条消息发给队列2");
  9. }
  10. }

运行上面的测试方法,然后查看mq_demo04工程的consumer模块的控制台,发现两个消费者分别收到了一条消息

04-RabbitMQ七种消息收发方式 - 图36

3.3.2. Fanout

FanoutExchange 的数据交换策略是把所有到达 FanoutExchange 的消息转发给所有与它绑定的 Queue 上,在这种策略中,routingkey 将不起任何作用

演示代码:

在mq_demo04工程的consumer模块的org.javaboy.consumer.config包下创建FanoutConfig.java

  1. /**
  2. * fanout 交换机会将到达交换机的所有消息路由到与他绑定的所有队列上面来
  3. */
  4. @Configuration
  5. public class FanoutConfig {
  6. public static final String FANOUT_QUEUE_NAME = "fanout_queue_name";
  7. public static final String FANOUT_QUEUE_NAME2 = "fanout_queue_name2";
  8. public static final String FANOUT_EXCHANGE_NAME = "fanout_exchange_name";
  9. @Bean
  10. Queue fanoutQueue1() {
  11. return new Queue(FANOUT_QUEUE_NAME, true, false, false);
  12. }
  13. @Bean
  14. Queue fanoutQueue2() {
  15. return new Queue(FANOUT_QUEUE_NAME2, true, false, false);
  16. }
  17. @Bean
  18. FanoutExchange fanoutExchange() {
  19. return new FanoutExchange(FANOUT_EXCHANGE_NAME, true, false);
  20. }
  21. @Bean
  22. Binding fanoutBinding1() {
  23. return BindingBuilder.bind(fanoutQueue1())
  24. .to(fanoutExchange());
  25. }
  26. @Bean
  27. Binding fanoutBinding2() {
  28. return BindingBuilder.bind(fanoutQueue2())
  29. .to(fanoutExchange());
  30. }
  31. }

在mq_demo04工程的consumer模块的org.javaboy.consumer.receiver包下创建FanoutMsgReceiver.java

  1. @Component
  2. public class FanoutMsgReceiver {
  3. @RabbitListener(queues = FanoutConfig.FANOUT_QUEUE_NAME)
  4. public void msgHandler(String msg) {
  5. System.out.println("FanoutMsgReceiver = " + msg);
  6. }
  7. @RabbitListener(queues = FanoutConfig.FANOUT_QUEUE_NAME2)
  8. public void msgHandler2(String msg) {
  9. System.out.println("FanoutMsgReceiver2 = " + msg);
  10. }
  11. }

重启mq_demo04工程的consumer应用,把FanoutConfig拷贝到mq_demo04工程的publisher模块的org.javaboy.publisher.config包下面。

在mq_demo04工程的publisher模块的src/test/java目录下的org.javaboy.publisher包的PublisherApplicationTests类中发布消息

  1. @Test
  2. void test01() {
  3. rabbitTemplate.convertAndSend(FanoutConfig.FANOUT_EXCHANGE_NAME, null, "hello fanout!");
  4. }

运行上面的测试方法,然后查看mq_demo04工程的consumer模块的控制台,发现两个消费者都收到了消息

04-RabbitMQ七种消息收发方式 - 图37

注意:我们前面讲的Work queues是队列到消费者的方案,而我们现在讲的Fanout是交换机到队列的方案,所以它们并不冲突。

3.3.3. Topic

TopicExchange 是比较复杂但是也比较灵活的一种路由策略,在 TopicExchange 中,Queue 通过 routingkey 绑定到 TopicExchange 上,当消息到达 TopicExchange 后,TopicExchange 根据消息的 routingkey 将消息路由到一个或者多个 Queue 上。

演示代码:

在mq_demo04工程的consumer模块的org.javaboy.consumer.config包下创建TopicConfig.java

  1. @Configuration
  2. public class TopicConfig {
  3. public static final String XIAOMI_QUEUE_NAME = "xiaomi_queue_name";
  4. public static final String HUAWEI_QUEUE_NAME = "huawei_queue_name";
  5. public static final String PHONE_QUEUE_NAME = "phone_queue_name";
  6. public static final String TOPIC_EXCHANGE_NAME = "topic_queue_name";
  7. @Bean
  8. Queue xiaomiQueue() {
  9. return new Queue(XIAOMI_QUEUE_NAME, true, false, false);
  10. }
  11. @Bean
  12. Queue huaweiQueue() {
  13. return new Queue(HUAWEI_QUEUE_NAME, true, false, false);
  14. }
  15. @Bean
  16. Queue phoneQueue() {
  17. return new Queue(PHONE_QUEUE_NAME, true, false, false);
  18. }
  19. @Bean
  20. TopicExchange topicExchange() {
  21. return new TopicExchange(TOPIC_EXCHANGE_NAME, true, false);
  22. }
  23. @Bean
  24. Binding xiaomiBinding() {
  25. return BindingBuilder.bind(xiaomiQueue())
  26. .to(topicExchange())
  27. //这里的 # 是一个通配符,表示将来消息的 routing_key 只要是以 xiaomi 开头,都将被路由到 xiaomiQueue
  28. .with("xiaomi.#");
  29. }
  30. @Bean
  31. Binding huaweiBinding() {
  32. return BindingBuilder.bind(huaweiQueue())
  33. .to(topicExchange())
  34. //这里的 # 是一个通配符,表示将来消息的 routing_key 只要是以 huawei 开头,都将被路由到 huaweiQueue
  35. .with("huawei.#");
  36. }
  37. @Bean
  38. Binding phoneBinding() {
  39. return BindingBuilder.bind(phoneQueue())
  40. .to(topicExchange())
  41. //这里的 # 是一个通配符,表示将来消息的 routing_key 只要是包含phone,都将被路由到 phoneQueue
  42. .with("#.phone.#");
  43. }
  44. }

在mq_demo04工程的consumer模块的org.javaboy.consumer.receiver包下创建TopicMsgReceiver.java

  1. @Component
  2. public class TopicMsgReceiver {
  3. @RabbitListener(queues = TopicConfig.HUAWEI_QUEUE_NAME)
  4. public void huawei(String msg) {
  5. System.out.println("huawei = " + msg);
  6. }
  7. @RabbitListener(queues = TopicConfig.XIAOMI_QUEUE_NAME)
  8. public void xiaomi(String msg) {
  9. System.out.println("xiaomi = " + msg);
  10. }
  11. @RabbitListener(queues = TopicConfig.PHONE_QUEUE_NAME)
  12. public void phone(String msg) {
  13. System.out.println("phone = " + msg);
  14. }
  15. }

重启mq_demo04工程的consumer应用,把TopicConfig拷贝到mq_demo04工程的publisher模块的org.javaboy.publisher.config包下面。

在mq_demo04工程的publisher模块的src/test/java目录下的org.javaboy.publisher包的PublisherApplicationTests类中发布消息

  1. @Test
  2. void test02() {
  3. //rabbitTemplate.convertAndSend(TopicConfig.TOPIC_EXCHANGE_NAME, "xiaomi.news", "小米新闻");
  4. //rabbitTemplate.convertAndSend(TopicConfig.TOPIC_EXCHANGE_NAME, "huawei.news", "华为新闻");
  5. rabbitTemplate.convertAndSend(TopicConfig.TOPIC_EXCHANGE_NAME, "huawei.phone.news", "华为手机新闻");
  6. }

运行上面的测试方法,然后查看mq_demo04工程的consumer模块的控制台,发现xiaomi消费者和phone消费者都收到了消息

04-RabbitMQ七种消息收发方式 - 图38

3.3.4. Header

HeadersExchange 是一种使用较少的路由策略,HeadersExchange 会根据消息的 Header 将消息路由到不同的 Queue 上,这种策略也和 routingkey无关

演示代码:

在mq_demo04工程的consumer模块的org.javaboy.consumer.config包下创建HeaderConfig.java

  1. /**
  2. * 交换机根据消息的头信息来决定消息去哪一个队列
  3. */
  4. @Configuration
  5. public class HeaderConfig {
  6. public static final String HEADER_QUEUE_NAME_NAME = "header_queue_name_name";
  7. public static final String HEADER_QUEUE_AGE_NAME = "header_queue_age_name";
  8. public static final String HEADER_EXCHANGE_NAME = "header_exchange_name";
  9. @Bean
  10. Queue headerNameQueue() {
  11. return new Queue(HEADER_QUEUE_NAME_NAME, true, false,false);
  12. }
  13. @Bean
  14. Queue headerAgeQueue() {
  15. return new Queue(HEADER_QUEUE_AGE_NAME, true, false,false);
  16. }
  17. @Bean
  18. HeadersExchange headersExchange() {
  19. return new HeadersExchange(HEADER_EXCHANGE_NAME, true, false);
  20. }
  21. @Bean
  22. Binding nameBinding() {
  23. return BindingBuilder.bind(headerNameQueue())
  24. .to(headersExchange())
  25. //如果将来消息头部中包含 name 属性,就算匹配成功
  26. .where("name").exists();
  27. }
  28. @Bean
  29. Binding ageBinding() {
  30. return BindingBuilder.bind(headerAgeQueue())
  31. .to(headersExchange())
  32. //将来头信息中必须要有 age 属性,并且 age 属性值为 99
  33. .where("age")
  34. .matches(99);
  35. }
  36. }

在mq_demo04工程的consumer模块的org.javaboy.consumer.receiver包下创建HeaderMsgReceiver.java

  1. @Component
  2. public class HeaderMsgReceiver {
  3. @RabbitListener(queues = HeaderConfig.HEADER_QUEUE_NAME_NAME)
  4. public void nameMsgHandler(byte[] msg) {
  5. System.out.println("nameMsgHandler >>> " + new String(msg, 0, msg.length));
  6. }
  7. @RabbitListener(queues = HeaderConfig.HEADER_QUEUE_AGE_NAME)
  8. public void ageMsgHandler(byte[] msg) {
  9. System.out.println("ageMsgHandler >>> " + new String(msg, 0, msg.length));
  10. }
  11. }

重启mq_demo04工程的consumer应用,把HeaderConfig拷贝到mq_demo04工程的publisher模块的org.javaboy.publisher.config包下面。

在mq_demo04工程的publisher模块的src/test/java目录下的org.javaboy.publisher包的PublisherApplicationTests类中发布消息

  1. @Test
  2. void test03() {
  3. Message nameMsg = MessageBuilder.withBody("hello zhangsan".getBytes()).setHeader("name", "aaa").build();
  4. rabbitTemplate.send(HeaderConfig.HEADER_EXCHANGE_NAME, null, nameMsg);
  5. Message ageMsg = MessageBuilder.withBody("hello lisi 99".getBytes()).setHeader("age", 99).build();
  6. rabbitTemplate.send(HeaderConfig.HEADER_EXCHANGE_NAME, null, ageMsg);
  7. }

运行上面的测试方法,然后查看mq_demo04工程的consumer模块的控制台

04-RabbitMQ七种消息收发方式 - 图39

3.3.5. 四种交换机总结

交换机路由策略主要是指消息到达交换机之后,如何从交换机到达消息队列。其它的(包括消息怎么到达交换机,消息怎么从消息队列到消费者)它不管

3.4. Routing

这种情况是这样:

一个生产者,一个交换机,两个队列,两个消费者,生产者在创建 Exchange 后,根据 RoutingKey 去绑定相应的队列,并且在发送消息时,指定消息的具体 RoutingKey 即可。

如下图:

04-RabbitMQ七种消息收发方式 - 图40

这个就是按照 routing key 去路由消息,我这里就不再举例子了

3.5. Topics

这种情况是这样:

一个生产者,一个交换机,两个队列,两个消费者,生产者创建 Topic 的 Exchange 并且绑定到队列中,这次绑定可以通过 *# 关键字,对指定 RoutingKey 内容,编写时注意格式 xxx.xxx.xxx 去编写。

如下图:

04-RabbitMQ七种消息收发方式 - 图41

这个我也就不举例啦,前面 3.3.3 小节已经举过例子了,不再赘述。

3.6. RPC

RPC 这种消息收发形式,松哥前两天刚刚写了文章和大家介绍,这里就不多说了,传送门:

3.7. Publisher Confirms

这种发送确认松哥之前有写过相关文章,传送门: