💥 该系列属于【SpringBoot基础】专栏,如您需查看其他SpringBoot相关文章,请您点击左边的连接
目录
3. 收发消息
一、MQ
1. 同步调用
基于OpenFeign的调用都属于是同步调用,那么这种方式存在哪些问题呢?
举个例子,以下面的余额支付功能为例来分析,首先看下整个流程:
三个步骤依次执行。这其中就存在3个问题:
(1)扩展性差
我们目前的业务相对简单,但是随着业务规模扩大,产品的功能也在不断完善。最终支付业务会越来越臃肿:
(2)性能下降
由于我们采用了同步调用,调用者需要等待服务提供者执行完返回结果后,才能继续向下执行,也就是说每次远程调用,调用者都是阻塞等待状态。最终整个业务的响应时长就是每次远程调用的执行时长之和:
假如每个微服务的执行时长都是50ms,则最终整个业务的耗时可能高达300ms,性能太差了。
(3)级联失败
由于我们是基于OpenFeign调用交易服务、通知服务。当交易服务、通知服务出现故障时,整个事务都会回滚,交易失败。
这其实就是同步调用的级联失败问题。
而要解决这些问题,我们就必须用异步调用的方式来代替同步调用。
2. 异步调用
异步调用方式其实就是基于消息通知的方式,一般包含三个角色:
-
消息发送者:投递消息的人,就是原来的调用方
-
消息代理(Broker):管理、暂存、转发消息
-
消息接收者:接收和处理消息的人,就是原来的服务提供方
在异步调用中,发送者不再直接同步调用接收者的业务接口,而是发送一条消息投递给消息Broker。然后接收者根据自己的需求从消息Broker那里订阅消息。每当发送方发送消息后,接受者都能获取消息并处理。
这样,发送消息的人和接收消息的人就完全解耦了。
以余额支付业务为例:
除了扣减余额、更新支付流水单状态以外,其它调用逻辑全部取消。而是改为发送一条消息到Broker。而相关的微服务都可以订阅消息通知,一旦消息到达Broker,则会分发给每一个订阅了的微服务,处理各自的业务。
假如产品经理提出了新的需求,比如要在支付成功后更新用户积分。支付代码完全不用变更,而仅仅是让积分服务也订阅消息即可。
不管后期增加了多少消息订阅者,作为支付服务来讲,执行问扣减余额、更新支付流水状态后,发送消息即可。业务耗时仅仅是这三部分业务耗时,仅仅100ms,大大提高了业务性能。另外,不管是交易服务、通知服务,还是积分服务,他们的业务与支付关联度低。现在采用了异步调用,解除了耦合,他们即便执行过程中出现了故障,也不会影响到支付服务。
综上所述,异步通信的优缺点如下:
优点:
耦合度更低
性能更好
业务拓展性强
故障隔离,避免级联失败
缺点:
完全依赖于Broker的可靠性、安全性和性能
架构复杂,后期维护和调试麻烦
3. 几种MQ
几种常见MQ的对比:
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,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.queue1
或hello.queue2
队列,自然就能接收到消息了。
4. 数据隔离
(1)用户管理
点击Admin
选项卡,首先会看到RabbitMQ控制台的用户管理界面:
这里的用户都是RabbitMQ的管理或运维人员。目前只有安装RabbitMQ时添加的itheima
这个用户。仔细观察用户表格中的字段,如下:
-
Name
:itheima
,也就是用户名 -
Tags
:administrator
,说明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
:
然后使用red
和blue
作为key,绑定direct.queue1
到hmall.direct,
同理,使用red
和yellow
作为key,绑定direct.queue2
到hmall.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
类型的Exchange
与Direct
相比,都是可以根据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方式来做序列化和反序列化。
在publisher
和consumer
两个服务中都引入jackson依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
配置消息转换器,在publisher
和consumer
两个服务的启动类中添加一个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);
}
}