RabbitMQ基础

💥 该系列属于【SpringBoot基础】专栏,如您需查看其他SpringBoot相关文章,请您点击左边的连接

目录

一、MQ

1. 同步调用

2. 异步调用

3. 几种MQ

二、RabbitMQ

1. 安装

2. RabbitMQ架构图

3. 收发消息​​​​​​​

4. 数据隔离

三、SpringAMQP

1. 案例需求

2. 准备Demo

3. 快速入门 

4. WorkQueues模型

5. 交换机类型

6. Fanout交换机

7. Direct交换机

8. Topic交换机

9. 基于Bean声明队列和交换机

10. 基于注解声明队列和交换机

11. 消息转换器

12. 业务示例


一、MQ

1. 同步调用

基于OpenFeign的调用都属于是同步调用,那么这种方式存在哪些问题呢?

举个例子,以下面的余额支付功能为例来分析,首先看下整个流程:

三个步骤依次执行。这其中就存在3个问题:

(1)扩展性差

我们目前的业务相对简单,但是随着业务规模扩大,产品的功能也在不断完善。最终支付业务会越来越臃肿:

(2)性能下降

由于我们采用了同步调用,调用者需要等待服务提供者执行完返回结果后,才能继续向下执行,也就是说每次远程调用,调用者都是阻塞等待状态。最终整个业务的响应时长就是每次远程调用的执行时长之和:

假如每个微服务的执行时长都是50ms,则最终整个业务的耗时可能高达300ms,性能太差了。

(3)级联失败

由于我们是基于OpenFeign调用交易服务、通知服务。当交易服务、通知服务出现故障时,整个事务都会回滚,交易失败。

这其实就是同步调用的级联失败问题。

而要解决这些问题,我们就必须用异步调用的方式来代替同步调用

2. 异步调用

异步调用方式其实就是基于消息通知的方式,一般包含三个角色:

  • 消息发送者:投递消息的人,就是原来的调用方

  • 消息代理(Broker):管理、暂存、转发消息

  • 消息接收者:接收和处理消息的人,就是原来的服务提供方

在异步调用中,发送者不再直接同步调用接收者的业务接口,而是发送一条消息投递给消息Broker。然后接收者根据自己的需求从消息Broker那里订阅消息。每当发送方发送消息后,接受者都能获取消息并处理。

这样,发送消息的人和接收消息的人就完全解耦了。

 以余额支付业务为例:

除了扣减余额、更新支付流水单状态以外,其它调用逻辑全部取消。而是改为发送一条消息到Broker。而相关的微服务都可以订阅消息通知,一旦消息到达Broker,则会分发给每一个订阅了的微服务,处理各自的业务。

假如产品经理提出了新的需求,比如要在支付成功后更新用户积分。支付代码完全不用变更,而仅仅是让积分服务也订阅消息即可。

不管后期增加了多少消息订阅者,作为支付服务来讲,执行问扣减余额、更新支付流水状态后,发送消息即可。业务耗时仅仅是这三部分业务耗时,仅仅100ms,大大提高了业务性能。另外,不管是交易服务、通知服务,还是积分服务,他们的业务与支付关联度低。现在采用了异步调用,解除了耦合,他们即便执行过程中出现了故障,也不会影响到支付服务。

综上所述,异步通信的优缺点如下:

优点:

  • 耦合度更低

  • 性能更好

  • 业务拓展性强

  • 故障隔离,避免级联失败

缺点:

  • 完全依赖于Broker的可靠性、安全性和性能

  • 架构复杂,后期维护和调试麻烦

3. 几种MQ

几种常见MQ的对比:

RabbitMQActiveMQRocketMQKafka
公司/社区RabbitApache阿里Apache
开发语言ErlangJavaJavaScala&Java
协议支持AMQP,XMPP,SMTP,STOMPOpenWire,STOMP,REST,XMPP,AMQP自定义协议自定义协议
可用性一般
单机吞吐量一般非常高
消息延迟微秒级毫秒级毫秒级毫秒以内
消息可靠性一般一般

二、RabbitMQ

1. 安装

docker run \
 -e RABBITMQ_DEFAULT_USER=itheima \
 -e RABBITMQ_DEFAULT_PASS=123321 \
 -v mq-plugins:/plugins \
 --name mq \
 --hostname mq \
 -p 15672:15672 \
 -p 5672:5672 \
 --network hmall\
 -d \
 rabbitmq:3.8-management

其中

  • 15672:RabbitMQ提供的管理控制台的端口

  • 5672:RabbitMQ的消息发送处理接口

安装完成后,我们访问 http://192.168.150.101:15672即可看到管理控制台。首次访问需要登录,默认的用户名和密码在配置文件中已经指定了,即itheima和123321。

登录后即可看到管理控制台总览页面:

2. RabbitMQ架构图

 RabbitMQ对应的架构如图:

其中包含几个概念:

  • publisher:生产者,也就是发送消息的一方

  • consumer:消费者,也就是消费消息的一方

  • queue:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理

  • exchange:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。

  • virtual host:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue

3. 收发消息

(1)交换机

打开Exchanges选项卡,可以看到已经存在很多交换机:

点击任意交换机,即可进入交换机详情页面。仍然会利用控制台中的publish message 发送一条消息:

这里是由控制台模拟了生产者发送的消息。由于没有消费者存在,最终消息丢失了,这样说明交换机没有存储消息的能力。

(2)队列

打开Queues选项卡,新建一个队列:

 再以相同的方式,创建一个队列,名字为hello.queue2,最终队列列表如下:

此时,我们再次向amq.fanout交换机发送一条消息。会发现消息依然没有到达队列,这是因为没有绑定交换机和队列的关系。发送到交换机的消息,只会路由到与其绑定的队列,因此仅仅创建队列是不够的,我们还需要将其与交换机绑定。

 (3)绑定交换机和队列的关系

点击Exchanges选项卡,点击amq.fanout交换机,进入交换机详情页,然后点击Bindings菜单,在表单中填写要绑定的队列名称:

相同的方式,将hello.queue2也绑定到改交换机。

(4)发送消息

再次回到exchange页面,找到刚刚绑定的amq.fanout,点击进入详情页,再次发送一条消息:

回到Queues页面,可以发现hello.queue中已经有一条消息了:

点击队列名称,进入详情页,查看队列详情,这次我们点击get message:

这个时候如果有消费者监听了MQ的hello.queue1hello.queue2队列,自然就能接收到消息了。

4. 数据隔离

(1)用户管理

点击Admin选项卡,首先会看到RabbitMQ控制台的用户管理界面:

这里的用户都是RabbitMQ的管理或运维人员。目前只有安装RabbitMQ时添加的itheima这个用户。仔细观察用户表格中的字段,如下:

  • Nameitheima,也就是用户名

  • Tagsadministrator,说明itheima用户是超级管理员,拥有所有权限

  • Can access virtual host/,可以访问的virtual host,这里的/是默认的virtual host

对于小型企业而言,出于成本考虑,我们通常只会搭建一套MQ集群,公司内的多个不同项目同时使用。这个时候为了避免互相干扰, 我们会利用virtual host的隔离特性,将不同项目隔离。一般会做两件事情:

  • 给每个项目创建独立的运维账号,将管理权限分离。

  • 给每个项目创建不同的virtual host,将每个项目的数据隔离。

比如,我们给黑马商城创建一个新的用户,命名为hmall,密码为123

会发现此时hmall用户没有任何virtual host的访问权限No access:

(2)virtual host

先退出登录:

切换到刚刚创建的hmall用户登录,然后点击Virtual Hosts菜单,进入virtual host管理页:

可以看到目前只有一个默认的virtual host,名字为 /。可以给黑马商城项目创建一个单独的virtual host,而不是使用默认的/

由于我们是登录hmall账户后创建的virtual host,因此回到users菜单,你会发现当前用户已经具备了对/hmall这个virtual host的访问权限了:

此时,点击页面右上角的virtual host下拉菜单,切换virtual host/hmall,然后再次查看queues选项卡,会发现之前的队列已经看不到了:

 这就是基于virtual host 的隔离效果。

三、SpringAMQP

将来我们开发业务功能的时候,肯定不会在控制台收发消息,而是应该基于编程的方式。由于RabbitMQ采用了AMQP协议,因此它具备跨语言的特性。任何语言只要遵循AMQP协议收发消息,都可以与RabbitMQ交互。并且RabbitMQ官方也提供了各种不同语言的客户端。

但是,RabbitMQ官方提供的Java客户端编码相对复杂,一般生产环境下我们更多会结合Spring来使用。而Spring的官方刚好基于RabbitMQ提供了这样一套消息收发的模板工具:SpringAMQP。并且还基于SpringBoot对其实现了自动装配,使用起来非常方便。

SpringAMQP提供了三个功能:

  • 自动声明队列、交换机及其绑定关系

  • 基于注解的监听器模式,异步接收消息

  • 封装了RabbitTemplate工具,用于发送消息

1. 案例需求

需求如下:

  • 利用控制台创建队列simple.queue
  • 在publisher服务中,利用SpringAMQP直接向simple.queue发送消息
  • 在consumer服务中,利用SpringAMQP编写消费者,监听simple.queue队列

2. 准备Demo

该Demo包括三部分:

  • mq-demo:父工程,管理项目依赖

  • publisher:消息的发送者

  • consumer:消息的消费者

        <!--AMQP依赖,包含RabbitMQ-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

在mq-demo这个父工程中,已经配置好了SpringAMQP相关的依赖:

3. 快速入门 

在案例中,我们的演示是这样的简单模型,如图:

也就是:

  • publisher直接发送消息到队列

  • 消费者监听并处理队列中的消息

(1)创建队列

使用hmall用户,现在控制台新建一个队列:simple.queue

添加成功:

(2)配置yaml

publisher和consumer服务的application.yml中添加配置:

spring:
  rabbitmq:
    host: 192.168.88.128 # 你的虚拟机IP
    port: 5672 # 端口
    virtual-host: /hmall # 虚拟主机
    username: hmall # 用户名
    password: 123 # 密码

(3)消息发送

在idea中选中启动类名,按Alt+回车键,点击创建测试

@SpringBootTest
class SpringAMQPTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSimpleQueue() {
        // 队列名称
        String queueName = "simple.queue";
        // 消息
        String message = "hello, spring AMQP!";
        // 发送消息
        rabbitTemplate.convertAndSend(queueName, message);
    }

}

启动测试,在rabbitMQ控制台中查看,可以看到消息已经发送到队列中:

(4)消息接收

consumer服务的com.itheima.consumer.listener包中新建一个类SpringRabbitListener,代码如下:

@Component
public class SpringRabbitListener {
    // 利用RabbitListener来声明要监听的队列信息
    // 将来一旦监听的队列中有了消息,就会推送给当前服务,调用当前方法,处理消息。
    // 可以看到方法体中接收的就是消息体的内容
    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueueMessage(String msg) throws InterruptedException {
        System.out.println("spring 消费者接收到消息:【" + msg + "】");
    }
}

 启动该consumer服务:

读取消息后,消息队列中清除该消息

4. WorkQueues模型

简单来说就是多个消费者绑定到一个队列,共同消费队列中的消息

当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。

此时就可以使用work 模型,多个消费者共同处理消息处理,消息处理的速度就能大大提高了。

接下来,我们就来模拟这样的场景。

首先,我们在控制台创建一个新的队列,命名为work.queue

(1)消息发送

这次我们循环发送,模拟大量消息堆积现象。

在publisher服务中的SpringAmqpTest类中添加一个测试方法:

    /**
     * workQueue
     * 向队列中不停发送消息,模拟消息堆积。
     */
    @Test
    public void testWorkQueue() throws InterruptedException {
        // 队列名称
        String queueName = "simple.queue";
        // 消息
        String message = "hello, message_";
        for (int i = 0; i < 50; i++) {
            // 发送消息,每20毫秒发送一次,相当于每秒发送50条消息
            rabbitTemplate.convertAndSend(queueName, message + i);
            Thread.sleep(20);
        }
    }

(2)消息接收

    @RabbitListener(queues = "work.queue")
    public void listenWorkQueue1(String msg) throws InterruptedException {
        System.out.println("消费者1接收到消息:【" + msg + "】" + LocalTime.now());
        Thread.sleep(50);
    }

    @RabbitListener(queues = "work.queue")
    public void listenWorkQueue2(String msg) throws InterruptedException {
        System.err.println("消费者2........接收到消息:【" + msg + "】" + LocalTime.now());
        Thread.sleep(200);
    }

注意到这两消费者,都设置了Thead.sleep,模拟任务耗时:

  • 消费者1 sleep了20毫秒,相当于每秒钟处理50个消息

  • 消费者2 sleep了200毫秒,相当于每秒处理5个消息

(3)能者多劳

在spring中有一个简单的配置,每个监听者一次就是只领取一个任务,能力大的一方处理的快,就领取的任务多。修改消费者的配置文件:

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息

测试结果:

消费者1接收到消息:【hello, message_1】19:49:05.355
消费者2........接收到消息:【hello, message_0】19:49:05.355
消费者1接收到消息:【hello, message_2】19:49:05.416
消费者1接收到消息:【hello, message_3】19:49:05.476
消费者1接收到消息:【hello, message_4】19:49:05.539
消费者2........接收到消息:【hello, message_5】19:49:05.571
消费者1接收到消息:【hello, message_6】19:49:05.601
消费者1接收到消息:【hello, message_7】19:49:05.662
消费者1接收到消息:【hello, message_8】19:49:05.724
消费者2........接收到消息:【hello, message_10】19:49:05.785
消费者1接收到消息:【hello, message_9】19:49:05.785
消费者1接收到消息:【hello, message_11】19:49:05.850
消费者1接收到消息:【hello, message_12】19:49:05.911
消费者1接收到消息:【hello, message_13】19:49:05.972
消费者2........接收到消息:【hello, message_14】19:49:05.987
消费者1接收到消息:【hello, message_15】19:49:06.032
消费者1接收到消息:【hello, message_16】19:49:06.094
消费者1接收到消息:【hello, message_17】19:49:06.156
消费者2........接收到消息:【hello, message_18】19:49:06.203
消费者1接收到消息:【hello, message_19】19:49:06.218
消费者1接收到消息:【hello, message_20】19:49:06.280
消费者1接收到消息:【hello, message_21】19:49:06.341
消费者1接收到消息:【hello, message_22】19:49:06.402
消费者2........接收到消息:【hello, message_23】19:49:06.417
消费者1接收到消息:【hello, message_24】19:49:06.463
消费者1接收到消息:【hello, message_25】19:49:06.525
消费者1接收到消息:【hello, message_26】19:49:06.587
消费者2........接收到消息:【hello, message_27】19:49:06.618
消费者1接收到消息:【hello, message_28】19:49:06.649
消费者1接收到消息:【hello, message_29】19:49:06.710
消费者1接收到消息:【hello, message_30】19:49:06.772
消费者2........接收到消息:【hello, message_31】19:49:06.833
消费者1接收到消息:【hello, message_32】19:49:06.833
消费者1接收到消息:【hello, message_33】19:49:06.895
消费者1接收到消息:【hello, message_34】19:49:06.956
消费者1接收到消息:【hello, message_35】19:49:07.019
消费者2........接收到消息:【hello, message_36】19:49:07.050
消费者1接收到消息:【hello, message_37】19:49:07.080
消费者1接收到消息:【hello, message_38】19:49:07.142
消费者1接收到消息:【hello, message_39】19:49:07.203
消费者2........接收到消息:【hello, message_40】19:49:07.265
消费者1接收到消息:【hello, message_41】19:49:07.265
消费者1接收到消息:【hello, message_42】19:49:07.326
消费者1接收到消息:【hello, message_43】19:49:07.387
消费者1接收到消息:【hello, message_44】19:49:07.449
消费者2........接收到消息:【hello, message_45】19:49:07.480
消费者1接收到消息:【hello, message_46】19:49:07.511
消费者1接收到消息:【hello, message_47】19:49:07.573
消费者1接收到消息:【hello, message_48】19:49:07.633
消费者2........接收到消息:【hello, message_49】19:49:07.680

正所谓能者多劳,这样充分利用了每一个消费者的处理能力,可以有效避免消息积压问题。

总之,Work模型的使用:

  • 多个消费者绑定到一个队列,同一条消息只会被一个消费者处理

  • 通过设置prefetch来控制消费者预取的消息数量

5. 交换机类型

一旦引入交换机,消息发送的模式会有很大变化:

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失。

交换机的类型有四种:

  • Fanout:广播,将消息交给所有绑定到交换机的队列。我们最早在控制台使用的正是Fanout交换机

  • Direct:订阅,基于RoutingKey(路由key)发送给订阅了消息的队列

  • Topic:通配符订阅,与Direct类似,只不过RoutingKey可以使用通配符

  • Headers:头匹配,基于MQ的消息头匹配,用的较少。

下面讲解前三种。

6. Fanout交换机

Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue,所以也叫广播模式。

实现思路如下:

  • 在RabbitMQ控制台中,声明队列fanout.queue1和fanout.queue2
  • 在RabbitMQ控制台中,声明交换机hmall.fanout,将两个队列与其绑定
  • 在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
  • 在publisher中编写测试方法,向hmall.fanout发送消息 

(1)声明队列和交换机

创建队列:

然后再创建一个交换机:

绑定两个队列到交换机:

(2)消息发送

在publisher服务的SpringAMQPTest类中添加测试方法:

    @Test
    public void testFanoutExchange() {
        // 交换机名称
        String exchangeName = "hmall.fanout";
        // 消息
        String message = "hello, everyone!";
        rabbitTemplate.convertAndSend(exchangeName, "", message);
    }

(3)消息接收

在consumer服务的SpringRabbitListener中添加两个方法,作为消费者:

@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) {
    System.out.println("消费者1接收到Fanout消息:【" + msg + "】");
}

@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) {
    System.out.println("消费者2接收到Fanout消息:【" + msg + "】");
}

(4)测试

消费者1接收到Fanout消息:【hello, everyone!】
消费者2接收到Fanout消息:【hello, everyone!】

两个消费者都收到了消息。

7. Direct交换机

在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。

  • 每一个Queue都与Exchange设置一个BindingKey
  • 发布者发送消息时,指定消息的RoutingKey
  • Exchange将消息路由到BindingKey与消息RoutingKey一致的队列

实现思路如下:

  • 在RabbitMQ控制台中,声明队列direct.queue1和direct.queue2
  • 在RabbitMQ控制台中,声明交换机hmall.direct,将两个队列与其绑定
  • 在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
  • 在publisher中编写测试方法,利用不同的RoutingKey向hmall.direct发送消息

(1)声明队列和交换机

创建队列:

然后声明一个direct类型的交换机,命名为hmall.direct:

然后使用redblue作为key,绑定direct.queue1hmall.direct,同理,使用redyellow作为key,绑定direct.queue2hmall.direct,最终结果:

(2)消息发送

    @Testh
    public void testSendDirectExchange() {
        // 交换机名称
        String exchangeName = "hmall.direct";
        // 消息
        String message = "红色警报!日本乱排核废水,导致海洋生物变异,惊现哥斯拉!";
        // 发送消息
        rabbitTemplate.convertAndSend(exchangeName, "red", message);
    }
    @Test
    public void testSendDirectExchange() {
        // 交换机名称
        String exchangeName = "hmall.direct";
        // 消息
        String message = "红色警报!日本乱排核废水,导致海洋生物变异,惊现哥斯拉!";
        // 发送消息
        rabbitTemplate.convertAndSend(exchangeName, "blue", message);
    }

(3)消息接收

@RabbitListener(queues = "direct.queue1")
public void listenDirectQueue1(String msg) {
    System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】");
}

@RabbitListener(queues = "direct.queue2")
public void listenDirectQueue2(String msg) {
    System.out.println("消费者2接收到direct.queue2的消息:【" + msg + "】");
}

(4)测试

red测试:

消费者1接收到direct.queue1的消息:【红色警报!日本乱排核废水,导致海洋生物变异,惊现哥斯拉!】
消费者2接收到direct.queue2的消息:【红色警报!日本乱排核废水,导致海洋生物变异,惊现哥斯拉!】

blue测试:

消费者1接收到direct.queue1的消息:【红色警报!日本乱排核废水,导致海洋生物变异,惊现哥斯拉!】

(5)Direct与Fanout交换机的差异

  • Fanout交换机将消息路由给每一个与之绑定的队列
  • Direct交换机根据RoutingKey判断路由给哪个队列
  • 如果多个队列具有相同RoutingKey,则与Fanout功能类似

8. Topic交换机

Topic类型的ExchangeDirect相比,都是可以根据RoutingKey把消息路由到不同的队列。

只不过Topic类型Exchange可以让队列在绑定BindingKey 的时候使用通配符!

BindingKey 一般都是有一个或多个单词组成,多个单词之间以.分割,例如: item.insert

通配符规则:

  • #:匹配一个或多个词

  • *:匹配不多不少恰好1个词

举例:

  • item.#:能够匹配item.spu.insert 或者 item.spu

  • item.*:只能匹配item.spu

思路如下:

  • 在RabbitMQ控制台中,声明队列topic.queue1和topic.queue2
  • 在RabbitMQ控制台中,声明交换机hmall.topic,将两个队列与其绑定
  • 在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2
  • 在publisher中编写测试方法,利用不同的RoutingKey向hmall.topic发送消息

(1)声明队列和交换机

创建队列:

创建交换机:

绑定交换机和消息队列:

(2)消息发送

/**
 * topicExchange
 */
@Test
public void testSendTopicExchange() {
    // 交换机名称
    String exchangeName = "hmall.topic";
    // 消息
    String message = "喜报!孙悟空大战哥斯拉,胜!";
    // 发送消息
    rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
}

(3)消息接收

@RabbitListener(queues = "topic.queue1")
public void listenTopicQueue1(String msg){
    System.out.println("消费者1接收到topic.queue1的消息:【" + msg + "】");
}

@RabbitListener(queues = "topic.queue2")
public void listenTopicQueue2(String msg){
    System.out.println("消费者2接收到topic.queue2的消息:【" + msg + "】");
}

(4)测试

queue1接收china.#

queue2接收#.news

"china.news"可被两个queue接收

消费者2接收到topic.queue2的消息:【喜报!孙悟空大战哥斯拉,胜!】
消费者1接收到topic.queue1的消息:【喜报!孙悟空大战哥斯拉,胜!】

(5)Direct与Topic交换机的差异

  • Topic交换机接收的消息RoutingKey必须是多个单词,以 . 分割

  • Topic交换机与队列绑定时的bindingKey可以指定通配符

  • #:代表0个或多个词

  • *:代表1个词

9. 基于Bean声明队列和交换机

(1)fanout示例

@Configuration
public class FanoutConfig {
    /**
     * 声明交换机
     * @return Fanout类型交换机
     */
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("hmall.fanout");
    }

    /**
     * 第1个队列
     */
    @Bean
    public Queue fanoutQueue1(){
        return new Queue("fanout.queue1");
    }

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    }

    /**
     * 第2个队列
     */
    @Bean
    public Queue fanoutQueue2(){
        return new Queue("fanout.queue2");
    }

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }
}

(2)direct示例

@Configuration
public class DirectConfig {

    /**
     * 声明交换机
     *
     * @return Direct类型交换机
     */
    @Bean
    public DirectExchange directExchange() {
        return ExchangeBuilder.directExchange("hmall.direct").build();
    }

    /**
     * 第1个队列
     */
    @Bean
    public Queue directQueue1() {
        return new Queue("direct.queue1");
    }

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue1WithRed(Queue directQueue1, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue1).to(directExchange).with("red");
    }

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue1WithBlue(Queue directQueue1, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue1).to(directExchange).with("blue");
    }

    /**
     * 第2个队列
     */
    @Bean
    public Queue directQueue2() {
        return new Queue("direct.queue2");
    }

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue2WithRed(Queue directQueue2, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue2).to(directExchange).with("red");
    }

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue2WithYellow(Queue directQueue2, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue2).to(directExchange).with("yellow");
    }
}

绑定关系时,显得比较臃肿,因此可以采用基于注解的方式声明队列和交换机

10. 基于注解声明队列和交换机

(1)direct示例

声明Direct模式的交换机和队列:

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "direct.queue1"),
    exchange = @Exchange(name = "hmall.direct", type = ExchangeTypes.DIRECT),
    key = {"red", "blue"}
))
public void listenDirectQueue1(String msg){
    System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】");
}

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "direct.queue2"),
    exchange = @Exchange(name = "hmall.direct", type = ExchangeTypes.DIRECT),
    key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg){
    System.out.println("消费者2接收到direct.queue2的消息:【" + msg + "】");
}

(2)topic示例

Topic模式:

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "topic.queue1"),
    exchange = @Exchange(name = "hmall.topic", type = ExchangeTypes.TOPIC),
    key = "china.#"
))
public void listenTopicQueue1(String msg){
    System.out.println("消费者1接收到topic.queue1的消息:【" + msg + "】");
}

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "topic.queue2"),
    exchange = @Exchange(name = "hmall.topic", type = ExchangeTypes.TOPIC),
    key = "#.news"
))
public void listenTopicQueue2(String msg){
    System.out.println("消费者2接收到topic.queue2的消息:【" + msg + "】");
}

11. 消息转换器

(1)测试默认转换器发送

@Test
public void testSendMap() throws InterruptedException {
    // 准备消息
    Map<String,Object> msg = new HashMap<>();
    msg.put("name", "柳岩");
    msg.put("age", 21);
    // 发送消息
    rabbitTemplate.convertAndSend("object.queue", msg);
}

消息格式非常不友好。

(2)配置JSON转换器

显然,JDK序列化方式并不合适。我们希望消息体的体积更小、可读性更高,因此可以使用JSON方式来做序列化和反序列化。

publisherconsumer两个服务中都引入jackson依赖:

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.9.10</version>
</dependency>

配置消息转换器,在publisherconsumer两个服务的启动类中添加一个Bean即可:

@Bean
public MessageConverter messageConverter(){
    // 1.定义消息转换器
    Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
    // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
    jackson2JsonMessageConverter.setCreateMessageIds(true);
    return jackson2JsonMessageConverter;
}

(3)消费者接收Object

我们在consumer服务中定义一个新的消费者,publisher是用Map发送,那么消费者也一定要用Map接收,格式如下:

@RabbitListener(queues = "object.queue")
public void listenSimpleQueueMessage(Map<String, Object> msg) throws InterruptedException {
    System.out.println("消费者接收到object.queue消息:【" + msg + "】");
}

12. 业务示例

改造余额支付功能,将支付成功后基于OpenFeign的交易服务的更新订单状态接口的同步调用,改为基于RabbitMQ的异步通知,仅实现交易服务的mq即可。

消息发送者:pay-service

消息接收者:trade-service

(1)配置MQ

不管是生产者还是消费者,都需要添加依赖和配置MQ的基本信息。

添加依赖

  <!--消息发送-->
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-amqp</artifactId>
  </dependency>

  <!--消息转换器jackson-->
  <dependency>
      <groupId>com.fasterxml.jackson.dataformat</groupId>
      <artifactId>jackson-dataformat-xml</artifactId>
      <version>2.9.10</version>
  </dependency>

配置MQ地址

spring:
  rabbitmq:
    host: 192.168.88.128 # 你的虚拟机IP
    port: 5672 # 端口
    virtual-host: /hmall # 虚拟主机
    username: hmall # 用户名
    password: 123 # 密码

添加消息转换器

(2)接收消息

在trade-service服务中定义一个消息监听类:

其代码如下,声明了消息队列名字、交换机名字、binding-key:

@Component
@RequiredArgsConstructor
public class PayStatusListener {

    private final IOrderService orderService;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "trade.pay.success.queue", durable = "true"),
            exchange = @Exchange(name = "pay.topic"),
            key = "pay.success"
    ))
    public void listenPaySuccess(Long orderId){
        orderService.markOrderPaySuccess(orderId);
    }
}

先运行消息接收者,就可以在rabbitmq控制台创建消息队列、交换机和绑定关系

(3)发送消息

private final RabbitTemplate rabbitTemplate;

@Override
@Transactional
public void tryPayOrderByBalance(PayOrderDTO payOrderDTO) {
    // 1.查询支付单
    PayOrder po = getById(payOrderDTO.getId());
    // 2.判断状态
    if(!PayStatus.WAIT_BUYER_PAY.equalsValue(po.getStatus())){
        // 订单不是未支付,状态异常
        throw new BizIllegalException("交易已支付或关闭!");
    }
    // 3.尝试扣减余额
    userClient.deductMoney(payOrderDTO.getPw(), po.getAmount());
    // 4.修改支付单状态
    boolean success = markPayOrderSuccess(payOrderDTO.getId(), LocalDateTime.now());
    if (!success) {
        throw new BizIllegalException("交易已支付或关闭!");
    }
    // 5.修改订单状态
    // tradeClient.markOrderPaySuccess(po.getBizOrderNo()); //同步调用
    try {
        // 异步调用
        rabbitTemplate.convertAndSend("pay.direct", "pay.success", po.getBizOrderNo());
    } catch (Exception e) {
        log.error("支付成功的消息发送失败,支付单id:{}, 交易单id:{}", po.getId(), po.getBizOrderNo(), e);
    }
}

(4)消息转换器

  • 15
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值