参考地址
参考地址二

1、消息队列定义

在消息的传输过程中保存消息的容器。在消息队列中,通常有生产者和消费者两个角色。生产者只负责发送数据到消息队列,谁从消息队列中取出数据处理,他不管。消费者只负责从消息队列中取出数据处理,他不管这是谁发送的数据。
image.png

2、消息队列的由来

2.1、解耦

如图所示。假设有系统B、C、D都需要系统A的数据,于是系统A调用三个方法发送数据到B、C、D。这时,系统D不需要了,那就需要在系统A把相关的代码删掉。假设这时有个新的系统E需要数据,这时系统A又要增加调用系统E的代码。为了降低这种强耦合,就可以使用MQ,系统A只需要把数据发送到MQ,其他系统如果需要数据,则从MQ中获取即可
image.png

2.2、异步

如图所示。一个客户端请求发送进来,系统A会调用系统B、C、D三个系统,同步请求的话,响应时间就是系统A、B、C、D的总和,也就是800ms。如果使用MQ,系统A发送数据到MQ,然后就可以返回响应给客户端,不需要再等待系统B、C、D的响应,可以大大地提高性能。对于一些非必要的业务,比如发送短信,发送邮件等等,就可以采用MQ。
image.png

2.3、削峰

如图所示。这其实是MQ一个很重要的应用。假设系统A在某一段时间请求数暴增,有5000个请求发送过来,系统A这时就会发送5000条SQL进入MySQL进行执行,MySQL对于如此庞大的请求当然处理不过来,MySQL就会崩溃,导致系统瘫痪。如果使用MQ,系统A不再是直接发送SQL到数据库,而是把数据发送到MQ,MQ短时间积压数据是可以接受的,然后由消费者每次拉取2000条进行处理,防止在请求峰值时期大量的请求直接发送到MySQL导致系统崩溃

image.png

3、特点

RabbitMQ是一款使用Erlang语言开发的,实现AMQP(高级消息队列协议)的开源消息中间件。首先要知道一些RabbitMQ的特点,官网可查:

  • 可靠性。支持持久化,传输确认,发布确认等保证了MQ的可靠性。
  • 灵活的分发消息策略。这应该是RabbitMQ的一大特点。在消息进入MQ前由Exchange(交换机)进行路由消息。分发消息策略有:简单模式、工作队列模式、发布订阅模式、路由模式、通配符模式。
  • 支持集群。多台RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker。
  • 多种协议。RabbitMQ支持多种消息队列协议,比如 STOMP、MQTT 等等。
  • 支持多种语言客户端。RabbitMQ几乎支持所有常用编程语言,包括 Java、.NET、Ruby 等等。
  • 可视化管理界面。RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker。
  • 插件机制。RabbitMQ提供了许多插件,可以通过插件进行扩展,也可以编写自己的插件。

4、RabbitMQ初体验

4.1 安装RabbitMQ (Win10系统)

由于只是学习需要,所以安装在win10系统,就懒得开虚拟机。如果用Linux系统安装的话,我建议用Docker拉一个RabbitMQ的镜像下来,这样会方便一点。

4.1.1 安装erLang语言,配置环境变量

首先到erlang官网下载win10版安装包。
image.png
下载完之后,就得到这个东西:
image.png
接着双击安装,一直点next(下一步)就行了,安装完之后,配置环境变量。
image.png
image.png
使用cmd命令,输入 erl -version 验证:
image.png

4.1.2 安装RabbitMQ服务端

在RabbitMQ的gitHub项目中,下载window版本的服务端安装包。
image.png
下载后,就得到这个东西:
image.png
接着到双击安装,一直点下一步安装即可,安装完成后,找到安装目录:
image.png
在此目录下打开cmd命令,输入rabbitmq-plugins enable rabbitmq_management命令安装管理页面的插件:
image.png
然后双击rabbitmq-server.bat启动脚本,然后打开服务管理可以看到RabbitMQ正在运行:
image.png
这时,打开浏览器输入http://localhost:15672,账号密码默认是:guest/guest
image.png
到这一步,安装就大功告成了!
image.png

4.2 永远的Hello Word

目录结构
image.png
因为我用的是SpringBoot,所以在生产者这边加入对应的starter依赖即可:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-amqp</artifactId>
  4. </dependency>

一般需要创建一个公共项目common,共享一些配置,比如队列主题,交换机名称,路由匹配键名称等等。

  1. package com.mq.common;
  2. /**
  3. * @Description 配置mq公共信息
  4. * @Author xinxiaokang
  5. * @Date 2021/12/20 14:41
  6. */
  7. public class RabbitMQConfig {
  8. /**
  9. * RabbitMQ的队列主题名称
  10. */
  11. public static final String RABBITMQ_DEMO_TOPIC = "rabbitmq.demo.topic";
  12. /**
  13. * RabbitMQ的DIRECT交换机名称
  14. */
  15. public static final String RABBITMQ_DEMO_DIRECT_EXCHANGE = "rabbitmq.demo.direct.exchange";
  16. /**
  17. * RabbitMQ的DIRECT交换机和队列绑定的匹配键 DirectRouting
  18. */
  19. public static final String RABBITMQ_DEMO_DIRECT_ROUTING = "rabbitmq.demo.direct.routing";
  20. /**
  21. * RabbitMQ的FANOUT_EXCHANG交换机类型的队列 A 的名称
  22. */
  23. public static final String FANOUT_EXCHANGE_QUEUE_TOPIC_A = "fanout.A";
  24. /**
  25. * RabbitMQ的FANOUT_EXCHANG交换机类型的队列 B 的名称
  26. */
  27. public static final String FANOUT_EXCHANGE_QUEUE_TOPIC_B = "fanout.B";
  28. /**
  29. * RabbitMQ的FANOUT_EXCHANG交换机类型的名称
  30. */
  31. public static final String FANOUT_EXCHANGE_DEMO_NAME = "fanout.exchange.demo.name";
  32. /**
  33. * RabbitMQ的TOPIC_EXCHANGE交换机名称
  34. */
  35. public static final String TOPIC_EXCHANGE_DEMO_NAME = "topic.exchange.demo.name";
  36. /**
  37. * RabbitMQ的TOPIC_EXCHANGE交换机的队列A的名称
  38. */
  39. public static final String TOPIC_EXCHANGE_QUEUE_A = "topic.queue.a";
  40. /**
  41. * RabbitMQ的TOPIC_EXCHANGE交换机的队列B的名称
  42. */
  43. public static final String TOPIC_EXCHANGE_QUEUE_B = "topic.queue.b";
  44. /**
  45. * RabbitMQ的TOPIC_EXCHANGE交换机的队列C的名称
  46. */
  47. public static final String TOPIC_EXCHANGE_QUEUE_C = "topic.queue.c";
  48. /**
  49. * HEADERS_EXCHANGE交换机名称
  50. */
  51. public static final String HEADERS_EXCHANGE_DEMO_NAME = "headers.exchange.demo.name";
  52. /**
  53. * RabbitMQ的HEADERS_EXCHANGE交换机的队列A的名称
  54. */
  55. public static final String HEADERS_EXCHANGE_QUEUE_A = "headers.queue.a";
  56. /**
  57. * RabbitMQ的HEADERS_EXCHANGE交换机的队列B的名称
  58. */
  59. public static final String HEADERS_EXCHANGE_QUEUE_B = "headers.queue.b";
  60. }

首先在application.yml文件加上RabbitMQ的配置信息:

  1. spring:
  2. rabbitmq:
  3. host: 127.0.0.1
  4. port: 5672
  5. username: guest
  6. password: guest
  7. server:
  8. port: 8080

然后在生产者这边,加上common包的maven依赖,然后创建一个Direct交换机以及队列的配置类:

  1. @Configuration
  2. public class DirectRabbitConfig {
  3. @Bean
  4. public Queue rabbitmqDemoDirectQueue() {
  5. /**
  6. * 1、name: 队列名称
  7. * 2、durable: 是否持久化
  8. * 3、exclusive: 是否独享、排外的。如果设置为true,定义为排他队列。则只有创建者可以使用此队列。也就是private私有的。
  9. * 4、autoDelete: 是否自动删除。也就是临时队列。当最后一个消费者断开连接后,会自动删除。
  10. * */
  11. return new Queue(RabbitMQConfig.RABBITMQ_DEMO_TOPIC, true, false, false);
  12. }
  13. @Bean
  14. public DirectExchange rabbitmqDemoDirectExchange() {
  15. //Direct交换机
  16. return new DirectExchange(RabbitMQConfig.RABBITMQ_DEMO_DIRECT_EXCHANGE, true, false);
  17. }
  18. @Bean
  19. public Binding bindDirect() {
  20. //链式写法,绑定交换机和队列,并设置匹配键
  21. return BindingBuilder
  22. //绑定队列
  23. .bind(rabbitmqDemoDirectQueue())
  24. //到交换机
  25. .to(rabbitmqDemoDirectExchange())
  26. //并设置匹配键
  27. .with(RabbitMQConfig.RABBITMQ_DEMO_DIRECT_ROUTING);
  28. }
  29. }

然后再创建一个发送消息的Service类:

  1. public interface RabbitMQService {
  2. String sendMsg(String msg) throws Exception;
  3. String sendMsgByFanoutExchange(String msg) throws Exception;
  4. String sendMsgByTopicExchange(String msg, String routingKey) throws Exception;
  5. String sendMsgByHeadersExchange(String msg, Map<String, Object> map) throws Exception;
  6. }
  1. @Service
  2. public class RabbitMQServiceImpl implements RabbitMQService {
  3. //日期格式化
  4. private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  5. @Resource
  6. private RabbitTemplate rabbitTemplate;
  7. @Override
  8. public String sendMsg(String msg) throws Exception {
  9. try {
  10. String msgId = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
  11. String sendTime = sdf.format(new Date());
  12. Map<String, Object> map = new HashMap<>();
  13. map.put("msgId", msgId);
  14. map.put("sendTime", sendTime);
  15. map.put("msg", msg);
  16. rabbitTemplate.convertAndSend(RabbitMQConfig.RABBITMQ_DEMO_DIRECT_EXCHANGE, RabbitMQConfig.RABBITMQ_DEMO_DIRECT_ROUTING, map);
  17. return "ok";
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. return "error";
  21. }
  22. }
  23. }

然后根据业务放在需要用的地方,比如定时任务,或者接口。我这里就简单一点使用Controller层进行发送:

  1. @RestController
  2. @RequestMapping("/mall/rabbitmq")
  3. public class RabbitMQController {
  4. @Resource
  5. private RabbitMQService rabbitMQService;
  6. /**
  7. * 发送消息
  8. * @author
  9. */
  10. @PostMapping("/sendMsg")
  11. public String sendMsg(@RequestParam(name = "msg") String msg) throws Exception {
  12. return rabbitMQService.sendMsg(msg);
  13. }
  14. }

生产者写完之后,就写消费者端的代码,消费者很简单。maven依赖,yml文件配置和生产者一样。只需要创建一个类,@RabbitListener注解写上监听队列的名称

  1. @Component
  2. @RabbitListener(queues = {RabbitMQConfig.RABBITMQ_DEMO_TOPIC})
  3. public class RabbitConsumer {
  4. @RabbitHandler
  5. public void process(Map map){
  6. System.out.println("消费者从RabbitMQ服务端获取消息:"+map.toString());
  7. }
  8. }

image.png
这里有个小坑,一开始RabbitMQ服务器里还没有创建队列:
image.png
这时如果启动消费者,会报错:
image.png
要先启动生产者,发送一条消息:
image.png
连接客户端
image.png
交换机
image.png
队列
image.png
最后再启动消费者,进行消费:
image.png
这时候就会持续监听队列的消息,只要生产者发送一条消息到MQ,消费者就消费一条。我这里尝试发送4条:
image.png

由于队列不存在,启动消费者报错的这个问题。最好的方法是生产者和消费者都尝试创建队列,怎么写呢,有很多方式,我这里用一个相对简单一点的:
生产者的配置类加点东西:

  1. //实现BeanPostProcessor类,使用Bean的生命周期函数
  2. @Component
  3. public class DirectRabbitConfig implements BeanPostProcessor {
  4. //这是创建交换机和队列用的rabbitAdmin对象
  5. @Resource
  6. private RabbitAdmin rabbitAdmin;
  7. //初始化rabbitAdmin对象
  8. @Bean
  9. public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
  10. RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
  11. // 只有设置为 true,spring 才会加载 RabbitAdmin 这个类
  12. rabbitAdmin.setAutoStartup(true);
  13. return rabbitAdmin;
  14. }
  15. //实例化bean后,也就是Bean的后置处理器
  16. @Override
  17. public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
  18. //创建交换机
  19. rabbitAdmin.declareExchange(rabbitmqDemoDirectExchange());
  20. //创建队列
  21. rabbitAdmin.declareQueue(rabbitmqDemoDirectQueue());
  22. return null;
  23. }
  24. }

这样启动生产者就会自动创建交换机和队列,不用等到发送消息才创建。
消费者需要加一点代码:

  1. @Component
  2. //使用queuesToDeclare属性,如果不存在则会创建队列
  3. @RabbitListener(queuesToDeclare = @Queue(RabbitMQConfig.RABBITMQ_DEMO_TOPIC))
  4. public class RabbitDemoConsumer {
  5. //...省略
  6. }

5、RabbitMQ中的组成部分

  • Broker:消息队列服务进程。此进程包括两个部分:Exchange和Queue。
  • Exchange:消息队列交换机。按一定的规则将消息路由转发到某个队列
  • Queue:消息队列,存储消息的队列。
  • Producer:消息生产者。生产方客户端将消息同交换机路由发送到队列中。
  • Consumer:消息消费者。消费队列中存储的消息。

这些组成部分是如何协同工作的呢,大概的流程如下,请看下图:
image.png

  • 消息生产者连接到RabbitMQ Broker,创建connection,开启channel。
  • 生产者声明交换机类型、名称、是否持久化等。
  • 生产者发送消息,并指定消息是否持久化等属性和routing key。
  • exchange收到消息之后,根据routing key路由到跟当前交换机绑定的相匹配的队列里面。
  • 消费者监听接收到消息之后开始业务处理。

6、Exchange的四种类型以及用法

从上面的工作流程可以看出,实际上有个关键的组件Exchange,因为消息发送到RabbitMQ后首先要经过Exchange路由才能找到对应的Queue
实际上Exchange类型有四种,根据不同的类型工作的方式也有所不同。在HelloWord例子中,我们就使用了比较简单的Direct Exchange,翻译就是直连交换机。其余三种分别是:Fanout exchange、Topic exchange、Headers exchange

6.1 直连交换:Direct Exchange

见文知意,直连交换机意思是此交换机需要绑定一个队列,要求该消息与一个特定的路由键完全匹配。简单点说就是一对一的,点对点的发送。
image.png
完整的代码就是上面的HelloWord的例子,不再重复代码。

6.2 发布与订阅模式:Fanout exchange

这种类型的交换机需要将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。简单点说就是发布订阅。
image.png
代码怎么写呢,演示一下:
首先要先配置交换机和队列的名称:

  1. public class RabbitMQConfig {
  2. /**
  3. * RabbitMQ的FANOUT_EXCHANG交换机类型的队列 A 的名称
  4. */
  5. public static final String FANOUT_EXCHANGE_QUEUE_TOPIC_A = "fanout.A";
  6. /**
  7. * RabbitMQ的FANOUT_EXCHANG交换机类型的队列 B 的名称
  8. */
  9. public static final String FANOUT_EXCHANGE_QUEUE_TOPIC_B = "fanout.B";
  10. /**
  11. * RabbitMQ的FANOUT_EXCHANG交换机类型的名称
  12. */
  13. public static final String FANOUT_EXCHANGE_DEMO_NAME = "fanout.exchange.demo.name";
  14. }

再配置FanoutExchange类型的交换机和A、B两个队列,并且绑定。这种类型不需要配置routing key:

  1. @Component
  2. public class DirectRabbitConfig implements BeanPostProcessor {
  3. @Resource
  4. private RabbitAdmin rabbitAdmin;
  5. @Bean
  6. public Queue fanoutExchangeQueueA() {
  7. //队列A
  8. return new Queue(RabbitMQConfig.FANOUT_EXCHANGE_QUEUE_TOPIC_A, true, false, false);
  9. }
  10. @Bean
  11. public Queue fanoutExchangeQueueB() {
  12. //队列B
  13. return new Queue(RabbitMQConfig.FANOUT_EXCHANGE_QUEUE_TOPIC_B, true, false, false);
  14. }
  15. @Bean
  16. public FanoutExchange rabbitmqDemoFanoutExchange() {
  17. //创建FanoutExchange类型交换机
  18. return new FanoutExchange(RabbitMQConfig.FANOUT_EXCHANGE_DEMO_NAME, true, false);
  19. }
  20. @Bean
  21. public Binding bindFanoutA() {
  22. //队列A绑定到FanoutExchange交换机
  23. return BindingBuilder.bind(fanoutExchangeQueueA()).to(rabbitmqDemoFanoutExchange());
  24. }
  25. @Bean
  26. public Binding bindFanoutB() {
  27. //队列B绑定到FanoutExchange交换机
  28. return BindingBuilder.bind(fanoutExchangeQueueB()).to(rabbitmqDemoFanoutExchange());
  29. }
  30. @Override
  31. public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
  32. //启动项目即创建交换机和队列
  33. rabbitAdmin.declareExchange(rabbitmqDemoFanoutExchange());
  34. rabbitAdmin.declareQueue(fanoutExchangeQueueB());
  35. rabbitAdmin.declareQueue(fanoutExchangeQueueA());
  36. return null;
  37. }
  38. }

创建service发布消息的方法:

  1. @Service
  2. public class RabbitMQServiceImpl implements RabbitMQService {
  3. private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  4. @Resource
  5. private RabbitTemplate rabbitTemplate;
  6. //发布消息
  7. @Override
  8. public String sendMsgByFanoutExchange(String msg) throws Exception {
  9. Map<String, Object> message = getMessage(msg);
  10. try {
  11. rabbitTemplate.convertAndSend(RabbitMQConfig.FANOUT_EXCHANGE_DEMO_NAME, "", message);
  12. return "ok";
  13. } catch (Exception e) {
  14. e.printStackTrace();
  15. return "error";
  16. }
  17. }
  18. //组装消息体
  19. private Map<String, Object> getMessage(String msg) {
  20. String msgId = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
  21. String sendTime = sdf.format(new Date());
  22. Map<String, Object> map = new HashMap<>();
  23. map.put("msgId", msgId);
  24. map.put("sendTime", sendTime);
  25. map.put("msg", msg);
  26. return map;
  27. }
  28. }

Controller接口:

  1. @RestController
  2. @RequestMapping("/mall/rabbitmq")
  3. public class RabbitMQController {
  4. /**
  5. * 发布消息
  6. *
  7. * @author java技术爱好者
  8. */
  9. @PostMapping("/publish")
  10. public String publish(@RequestParam(name = "msg") String msg) throws Exception {
  11. return rabbitMQService.sendMsgByFanoutExchange(msg);
  12. }
  13. }

接着在消费者项目这边,创建两个队列的监听类,监听队列进行消费:

  1. @Component
  2. @RabbitListener(queuesToDeclare = @Queue(RabbitMQConfig.FANOUT_EXCHANGE_QUEUE_TOPIC_A))
  3. public class FanoutExchangeConsumerA {
  4. @RabbitHandler
  5. public void process(Map<String, Object> map) {
  6. System.out.println("队列A收到消息:" + map.toString());
  7. }
  8. }
  1. @Component
  2. @RabbitListener(queuesToDeclare = @Queue(RabbitMQConfig.FANOUT_EXCHANGE_QUEUE_TOPIC_B))
  3. public class FanoutExchangeConsumerB {
  4. @RabbitHandler
  5. public void process(Map<String, Object> map) {
  6. System.out.println("队列B收到消息:" + map.toString());
  7. }
  8. }

然后启动生产者和消费者两个项目,可以看到管理界面创建了一个FanoutExchange交换机和两个队列,并且绑定了:
image.png
使用POSTMAN进行发送消息,测试:
image.png
然后可以看到控制台,两个队列同时都收到了相同的消息,形成了发布订阅的效果:
image.png

6.3 通配符匹配模式:Topic Exchange

直接翻译的话叫做主题交换机,如果从用法上面翻译可能叫通配符交换机会更加贴切。这种交换机是使用通配符去匹配,路由到对应的队列。通配符有两种:”*” 、 “#”。需要注意的是通配符前面必须要加上”.”符号。

    • 符号:有且只匹配一个词。比如 a.*可以匹配到”a.b”、”a.c”,但是匹配不了”a.b.c”。
  • 符号:匹配一个或多个词。比如”rabbit.#”既可以匹配到”rabbit.a.b”、”rabbit.a”,也可以匹配到”rabbit.a.b.c”。


image.png

废话不多说,代码演示一下:
依然是配置TopicExchange名称和三个队列的名称:

  1. /**
  2. * RabbitMQ的TOPIC_EXCHANGE交换机名称
  3. */
  4. public static final String TOPIC_EXCHANGE_DEMO_NAME = "topic.exchange.demo.name";
  5. /**
  6. * RabbitMQ的TOPIC_EXCHANGE交换机的队列A的名称
  7. */
  8. public static final String TOPIC_EXCHANGE_QUEUE_A = "topic.queue.a";
  9. /**
  10. * RabbitMQ的TOPIC_EXCHANGE交换机的队列B的名称
  11. */
  12. public static final String TOPIC_EXCHANGE_QUEUE_B = "topic.queue.b";
  13. /**
  14. * RabbitMQ的TOPIC_EXCHANGE交换机的队列C的名称
  15. */
  16. public static final String TOPIC_EXCHANGE_QUEUE_C = "topic.queue.c";

然后还是老配方,配置交换机和队列,然后绑定,创建:

  1. @Component
  2. public class DirectRabbitConfig implements BeanPostProcessor {
  3. //省略...
  4. @Bean
  5. public TopicExchange rabbitmqDemoTopicExchange() {
  6. //配置TopicExchange交换机
  7. return new TopicExchange(RabbitMQConfig.TOPIC_EXCHANGE_DEMO_NAME, true, false);
  8. }
  9. @Bean
  10. public Queue topicExchangeQueueA() {
  11. //创建队列1
  12. return new Queue(RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_A, true, false, false);
  13. }
  14. @Bean
  15. public Queue topicExchangeQueueB() {
  16. //创建队列2
  17. return new Queue(RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_B, true, false, false);
  18. }
  19. @Bean
  20. public Queue topicExchangeQueueC() {
  21. //创建队列3
  22. return new Queue(RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_C, true, false, false);
  23. }
  24. @Bean
  25. public Binding bindTopicA() {
  26. //队列A绑定到FanoutExchange交换机
  27. return BindingBuilder.bind(topicExchangeQueueB())
  28. .to(rabbitmqDemoTopicExchange())
  29. .with("a.*");
  30. }
  31. @Bean
  32. public Binding bindTopicB() {
  33. //队列A绑定到FanoutExchange交换机
  34. return BindingBuilder.bind(topicExchangeQueueC())
  35. .to(rabbitmqDemoTopicExchange())
  36. .with("a.*");
  37. }
  38. @Bean
  39. public Binding bindTopicC() {
  40. //队列A绑定到FanoutExchange交换机
  41. return BindingBuilder.bind(topicExchangeQueueA())
  42. .to(rabbitmqDemoTopicExchange())
  43. .with("rabbit.#");
  44. }
  45. @Override
  46. public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
  47. rabbitAdmin.declareExchange(rabbitmqDemoTopicExchange());
  48. rabbitAdmin.declareQueue(topicExchangeQueueA());
  49. rabbitAdmin.declareQueue(topicExchangeQueueB());
  50. rabbitAdmin.declareQueue(topicExchangeQueueC());
  51. return null;
  52. }
  53. }

然后写一个发送消息的service方法:

  1. @Service
  2. public class RabbitMQServiceImpl implements RabbitMQService {
  3. @Override
  4. public String sendMsgByTopicExchange(String msg, String routingKey) throws Exception {
  5. Map<String, Object> message = getMessage(msg);
  6. try {
  7. //发送消息
  8. rabbitTemplate.convertAndSend(RabbitMQConfig.TOPIC_EXCHANGE_DEMO_NAME, routingKey, message);
  9. return "ok";
  10. } catch (Exception e) {
  11. e.printStackTrace();
  12. return "error";
  13. }
  14. }
  15. }

写一个Controller接口:

  1. @RestController
  2. @RequestMapping("/mall/rabbitmq")
  3. public class RabbitMQController {
  4. @Resource
  5. private RabbitMQService rabbitMQService;
  6. /**
  7. * 通配符交换机发送消息
  8. *
  9. * @author java技术爱好者
  10. */
  11. @PostMapping("/topicSend")
  12. public String topicSend(@RequestParam(name = "msg") String msg, @RequestParam(name = "routingKey") String routingKey) throws Exception {
  13. return rabbitMQService.sendMsgByTopicExchange(msg, routingKey);
  14. }
  15. }

生产者这边写完,就写消费端,消费端比较简单,写三个监听类:

  1. @Component
  2. @RabbitListener(queuesToDeclare = @Queue(RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_A))
  3. public class TopicExchangeConsumerA {
  4. @RabbitHandler
  5. public void process(Map<String, Object> map) {
  6. System.out.println("队列[" + RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_A + "]收到消息:" + map.toString());
  7. }
  8. }
  9. @Component
  10. @RabbitListener(queuesToDeclare = @Queue(RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_B))
  11. public class TopicExchangeConsumerB {
  12. @RabbitHandler
  13. public void process(Map<String, Object> map) {
  14. System.out.println("队列[" + RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_B+ "]收到消息:" + map.toString());
  15. }
  16. }
  17. @Component
  18. @RabbitListener(queuesToDeclare = @Queue(RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_C))
  19. public class TopicExchangeConsumerC {
  20. @RabbitHandler
  21. public void process(Map<String, Object> map) {
  22. System.out.println("队列[" + RabbitMQConfig.TOPIC_EXCHANGE_QUEUE_C + "]收到消息:" + map.toString());
  23. }
  24. }

大功告成,然后启动项目开始调试。启动成功后可以看到队列和路由键绑定的关系:
image.png

通过POSTMAN进行测试,测试一下 rabbit.# 的路由键是否能够匹配成功:
image.png

测试成功,队列A消费到消息:
image.png
接着测试 a.* 路由键,发送 routingKey = a.b :
image.png
image.png
比较常用的就是以上三种:直连(DirectExchange),发布订阅(FanoutExchange),通配符(TopicExchange)。熟练运用这三种交换机类型,基本上可以解决大部分的业务场景。
实际上稍微思考一下,可以发现通配符(TopicExchange)这种模式其实是可以达到直连(DirectExchange)和发布订阅(FanoutExchange)这两种的效果的。
FanoutExchange不需要绑定routingKey,所以性能相对TopicExchange会好一点。

6.4 Headers Exchange

这种交换机用的相对没这么多。它跟上面三种有点区别,它的路由不是用routingKey进行路由匹配,而是在匹配请求头中所带的键值进行路由。如图所示:
image.png

image.png
创建队列需要设置绑定的头部信息,有两种模式:全部匹配和部分匹配。如上图所示,交换机会根据生产者发送过来的头部信息携带的键值去匹配队列绑定的键值,路由到对应的队列。代码怎么实现呢,往下看演示代码:
首先还是需要定义交换机名称,队列名称:

  1. /**
  2. * HEADERS_EXCHANGE交换机名称
  3. */
  4. public static final String HEADERS_EXCHANGE_DEMO_NAME = "headers.exchange.demo.name";
  5. /**
  6. * RabbitMQ的HEADERS_EXCHANGE交换机的队列A的名称
  7. */
  8. public static final String HEADERS_EXCHANGE_QUEUE_A = "headers.queue.a";
  9. /**
  10. * RabbitMQ的HEADERS_EXCHANGE交换机的队列B的名称
  11. */
  12. public static final String HEADERS_EXCHANGE_QUEUE_B = "headers.queue.b";

然后设置交换机,队列,进行绑定:

  1. @Component
  2. public class DirectRabbitConfig implements BeanPostProcessor {
  3. @Bean
  4. public Queue headersQueueA() {
  5. return new Queue(RabbitMQConfig.HEADERS_EXCHANGE_QUEUE_A, true, false, false);
  6. }
  7. @Bean
  8. public Queue headersQueueB() {
  9. return new Queue(RabbitMQConfig.HEADERS_EXCHANGE_QUEUE_B, true, false, false);
  10. }
  11. @Bean
  12. public HeadersExchange rabbitmqDemoHeadersExchange() {
  13. return new HeadersExchange(RabbitMQConfig.HEADERS_EXCHANGE_DEMO_NAME, true, false);
  14. }
  15. @Bean
  16. public Binding bindHeadersA() {
  17. Map<String, Object> map = new HashMap<>();
  18. map.put("key_one", "java");
  19. map.put("key_two", "rabbit");
  20. //全匹配
  21. return BindingBuilder.bind(headersQueueA())
  22. .to(rabbitmqDemoHeadersExchange())
  23. .whereAll(map).match();
  24. }
  25. @Bean
  26. public Binding bindHeadersB() {
  27. Map<String, Object> map = new HashMap<>();
  28. map.put("headers_A", "coke");
  29. map.put("headers_B", "sky");
  30. //部分匹配
  31. return BindingBuilder.bind(headersQueueB())
  32. .to(rabbitmqDemoHeadersExchange())
  33. .whereAny(map).match();
  34. }
  35. @Override
  36. public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
  37. rabbitAdmin.declareExchange(rabbitmqDemoHeadersExchange());
  38. rabbitAdmin.declareQueue(headersQueueA());
  39. rabbitAdmin.declareQueue(headersQueueB());
  40. return null;
  41. }
  42. }

再写一个Service方法发送消息:

  1. @Service
  2. public class RabbitMQServiceImpl implements RabbitMQService {
  3. @Resource
  4. private RabbitTemplate rabbitTemplate;
  5. @Override
  6. public String sendMsgByHeadersExchange(String msg, Map<String, Object> map) throws Exception {
  7. try {
  8. MessageProperties messageProperties = new MessageProperties();
  9. //消息持久化
  10. messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
  11. messageProperties.setContentType("UTF-8");
  12. //添加消息
  13. messageProperties.getHeaders().putAll(map);
  14. Message message = new Message(msg.getBytes(), messageProperties);
  15. rabbitTemplate.convertAndSend(RabbitMQConfig.HEADERS_EXCHANGE_DEMO_NAME, null, message);
  16. return "ok";
  17. } catch (Exception e) {
  18. e.printStackTrace();
  19. return "error";
  20. }
  21. }
  22. }

再写一个Controller接口:

  1. @RestController
  2. @RequestMapping("/mall/rabbitmq")
  3. public class RabbitMQController {
  4. @Resource
  5. private RabbitMQService rabbitMQService;
  6. @PostMapping("/headersSend")
  7. @SuppressWarnings("unchecked")
  8. public String headersSend(@RequestParam(name = "msg") String msg,
  9. @RequestParam(name = "json") String json) throws Exception {
  10. ObjectMapper mapper = new ObjectMapper();
  11. Map<String, Object> map = mapper.readValue(json, Map.class);
  12. return rabbitMQService.sendMsgByHeadersExchange(msg, map);
  13. }
  14. }

生产者这边写完了,再写两个队列的监听类进行消费:

  1. @Component
  2. public class HeadersExchangeConsumerA {
  3. @RabbitListener(queuesToDeclare = @Queue(RabbitMQConfig.HEADERS_EXCHANGE_QUEUE_A))
  4. public void process(Message message) throws Exception {
  5. MessageProperties messageProperties = message.getMessageProperties();
  6. String contentType = messageProperties.getContentType();
  7. System.out.println("队列[" + RabbitMQConfig.HEADERS_EXCHANGE_QUEUE_A + "]收到消息:" + new String(message.getBody(), contentType));
  8. }
  9. }
  10. @Component
  11. public class HeadersExchangeConsumerB {
  12. @RabbitListener(queuesToDeclare = @Queue(RabbitMQConfig.HEADERS_EXCHANGE_QUEUE_B))
  13. public void process(Message message) throws Exception {
  14. MessageProperties messageProperties = message.getMessageProperties();
  15. String contentType = messageProperties.getContentType();
  16. System.out.println("队列[" + RabbitMQConfig.HEADERS_EXCHANGE_QUEUE_B + "]收到消息:" + new String(message.getBody(), contentType));
  17. }
  18. }

大功告成~启动项目,打开管理界面,我们可以看到交换机绑定队列的信息:
image.png

跟上面示意图一样~证明没有问题,一切尽在掌握之中。使用POSTMAN发送,测试全匹配的队列A:
image.png
image.png

再测试部分匹配的队列B:
image.png
image.png