一、前言
对于黑马商城的支付微服务部分的处理,我们目前是全部使用的同步调用,也就是直接远程调用其他微服务,同步调用的优点有很多,比如稳定性高,及时性强,但是在高并发的场景下,如果所有业务模块全部使用同步调用,就会对服务器造成压力,从而影响用户体验(卡),基于这个问题,我们会选择让一些不太需要及时性,并且不是核心业务功能的模块采用异步调用,而实现异步调用需要使用到消息队列,大致的作用就是在远程调用前用一个队列进行消息的分发,当微服务想进行远程调用时,只需要将消息传给消息队列即可,传给MQ(消息队列)后,就继续执行代码,不用再去关注消息是否传递,或者传递给谁了,这些步骤都交给MQ来实现了。
二、RabbitMQ
1.部署和基本概念辨析
对于消息队列的组件,我们还是选择直接部署在docker中:
bash
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 hm-net\
-d \
rabbitmq:3.8-management
在项目中导包:
XML
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在项目中配置rabbitmq:
bash
spring:
rabbitmq:
host: 192.168.111.111 # 你的虚拟机IP
port: 5672 # 端口
virtual-host: /hmall # 虚拟主机
username: hmall # 用户名
password: 12345 # 密码
listener:
simple:
prefetch: 1
首先要辨析清楚以下的概念,整个消息队列实际上是由交换机和队列两部分组成,交换机负责分发消息给队列(这里会按照一定规则,可能是通配符,也可能是直接广播或者绑定),队列负责传递消息。
生产者是消息的发起人,也就是进行远程调用的微服务,而消费者是被远程调用的微服务,在我们的支付流程中,支付微服务就是生产者,而交易微服务就是消费者。
虚拟主机是为了让各个队列和交换机隔离,其实也就是用户,用户只能操纵自己的交换机和队列,这样就保障的每个项目的消息队列的独立性。
-
publisher:生产者,也就是发送消息的一方 -
consumer:消费者,也就是消费消息的一方 -
queue:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理 -
exchange:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。 -
virtual host:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue
现在我们只需要使用rabbitmq的网页控制台来操作即可:

2.快速入门
我们在控制台手动创建一个simple队列:

先看看项目的结构,我们这里使用两个模块来模拟生产者和消费者:

消费者监听队列,一旦收到队列传递过来的消息就直接打印日志:
java
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(String message) {
log.info("监听到simple.queue的消息:{}", message);
}
在生产者中,我们使用测试代码生产消息,尝试向队列中传递一个message字符串:
首先要自动装配RabbitTemplate(之前已经导了包了),使用模板对象的convertAndSend方法来将指定消息传递到指定的队列中去。
java
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue() {
//1.队列名
String queueName = "simple.queue";
//2.消息
String message = "hello spring amqp!!!";
//3.发送消息
rabbitTemplate.convertAndSend(queueName, message);
}
在我们启动消费者后,运行测试方法,就可以得到:

说明消费者拿到消息了,这个时候就可以随意处理消息了。
3.交换机
前面提到了,交换机负责分发消息给队列(这里会按照一定规则,可能是通配符,也可能是直接广播或者绑定)。
所以显然的,交换机会有很多种类,不同的分发规则就要使用不同的交换机。
交换机绑定的队列默认是按照轮询规则来传递消息的,也就是说,假如有两个队列,同时广播消息进去,队列1会被分到消息0、2、4、6......,队列2会被分到消息1、3、5、7......
这样会出现问题,假如一个队列的处理速度很慢,另一个队列处理得很快,那么就会导致快队列等待慢队列处理,直到慢队列处理完了一条,快队列才处理下一条。
所以我们在配置文件中会写一条:
bash
listener:
simple:
prefetch: 1
让分发规则改为:处理完一条消息马上处理下一条,这样就不会等待其他队列了,能者多劳,这样的规则才更符合实际生产。
(1)fanout交换机
我们在这里将写出完整的代码和过程,后面其他种类的交换机可以同理可得。
fanout交换机负责广播消息,也就是只要和fanout绑定的所有队列,都要接收发给fanout的所有消息。
首先创建这个交换机:

然后创建队列:

将交换机和两个队列绑定:

接下来就开始写代码了:
消费者监听队列消息:
java
//fanout交换机
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String message) throws InterruptedException {
log.info("消费者1监听到 fanout.queue1的消息:{}", message);
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String message) throws InterruptedException {
log.info("消费者2监听到 fanout.queue2的消息:{}", message);
}
测试广播的代码,将消息发给交换机(中间的 "null" 是路由,这里是广播,所以直接空着就是了)
java
@Test
public void testFanoutQueue() {
//1.交换机名
String exchange = "hmall.fanout";
//2.消息
String message = "hello everyone!!!";
//3.发送消息
rabbitTemplate.convertAndSend(exchange, "null", message);
}
测试:

可以看到所有的消费者都拿到了消息,说明是广播成功了的。
(2)direct交换机
这个交换机负责定向传递消息,所以刚刚我们空着的路由,要在这里写出来了。
这个时候绑定队列时就要填路由了,运行测试代码后交换机会向指定路由的队列发送指定消息。
java
//direct交换机
@RabbitListener(queues = "direct.queue1")
public void listenDirectQueue1(String message) throws InterruptedException {
log.info("消费者1监听到 direct.queue1的消息:{}", message);
}
@RabbitListener(queues = "direct.queue2")
public void listenDirectQueue2(String message) throws InterruptedException {
log.info("消费者2监听到 direct.queue2的消息:{}", message);
}
测试一下:
java
@Test
public void testDirectQueue() {
//1.交换机名
String exchange = "hmall.direct";
//2.消息
String message = "hello red!!!";
String message2 = "hello blue!!!";
String message3 = "hello yellow!!!";
//3.发送消息
rabbitTemplate.convertAndSend(exchange, "red", message);
rabbitTemplate.convertAndSend(exchange, "blue", message2);
rabbitTemplate.convertAndSend(exchange, "yellow", message3);
}
这样就根据不同的路由来传递消息了:

(3)topic交换机
按照通配符规则来分发消息:
java
//Topic交换机
@RabbitListener(queues = "topic.queue1")
public void listenTopicQueue1(String message) throws InterruptedException {
log.info("消费者1监听到 topic.queue1的消息:{}", message);
}
@RabbitListener(queues = "topic.queue2")
public void listenTopicQueue2(String message) throws InterruptedException {
log.info("消费者2监听到 topic.queue2的消息:{}", message);
}
测试代码:
java
@Test
public void testTopicQueue() {
//1.交换机名
String exchange = "hmall.topic";
//2.消息
String message = "hello world!!!";
String message2 = "yds news!!!";
String message3 = "china nb!!!";
//3.发送消息
rabbitTemplate.convertAndSend(exchange, "china.news", message);
rabbitTemplate.convertAndSend(exchange, "yds.news", message2);
rabbitTemplate.convertAndSend(exchange, "china.yyds", message3);
}
测试结果:

4.脱离控制台声明
(1)Bean声明
对于刚刚我们在控制台创建的几个关于fanout交换机的队列,我们可以在一个配置类中声明,当项目启动时,将自动创建这些队列、交换机,以及他们的绑定关系。
java
//用Bean声明交换机和队列及绑定
@Configuration
public class FanoutConfiguration {
@Bean
public FanoutExchange fanoutExchange() {
//return new FanoutExchange("hmall.fanout");
return ExchangeBuilder.fanoutExchange("hmall.fanout").build();
}
@Bean
public Queue fanoutQueue1() {
//return new Queue("fanout.queue1");
return QueueBuilder.durable("fanout.queue1").build();
}
@Bean
public Binding fanoutQueue1Binding(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
@Bean
public Queue fanoutQueue2() {
//return new Queue("fanout.queue1");
return QueueBuilder.durable("fanout.queue2").build();
}
@Bean
public Binding fanoutQueue2Binding(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
无论是Queue类、Exchange类还是Binding类,都是基于AMQP包中的,所以使用前需要导包(当然,我们前面已经导入过了)。
(2)注解声明
在刚刚我们创建监听器类的时候,我们就使用到了@RabbitListener注解,当时我们仅仅是用这个注解来标记监听的队列名。
其实这个注解还可以用来声明队列和交换机,下面我们尝试使用注解来声明direct交换机:
java
//direct交换机边监听边声明(注解声明)
@RabbitListener(bindings = @QueueBinding(
value = @Queue("direct.queue1"),
exchange = @Exchange(name = "hmall.direct",type = ExchangeTypes.DIRECT),
key = {"red","blue"}
))
public void listenDirectQueue1(String message) throws InterruptedException {
log.info("消费者1监听到 direct.queue1的消息:{}", message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue("direct.queue2"),
exchange = @Exchange(name = "hmall.direct",type = ExchangeTypes.DIRECT),
key = {"red","yellow"}
))
public void listenDirectQueue2(String message) throws InterruptedException {
log.info("消费者2监听到 direct.queue2的消息:{}", message);
}
注解最方便的一点就是对于路由key的配置,我们不需要一个一个的去绑定了,这里可以直接用字符串数组来定义路由,如果不使用注解,我们的配置类将会很冗长:
java
@Configuration
public class DirectConfiguration {
@Bean
public DirectExchange directExchange() {
//return new FanoutExchange("hmall.fanout");
return ExchangeBuilder.directExchange("hmall.direct").build();
}
@Bean
public Queue directQueue1() {
return QueueBuilder.durable("direct.queue1").build();
}
@Bean
public Binding directQueue1BindingRed(Queue directQueue1, DirectExchange directExchange) {
return BindingBuilder.bind(directQueue1).to(directExchange).with("red");
}
@Bean
public Binding directQueue1BindingBlue(Queue directQueue1, DirectExchange directExchange) {
return BindingBuilder.bind(directQueue1).to(directExchange).with("blue");
}
@Bean
public Queue directQueue2() {
return QueueBuilder.durable("direct.queue2").build();
}
@Bean
public Binding directQueue2BindingRed(Queue directQueue2, DirectExchange directExchange) {
return BindingBuilder.bind(directQueue2).to(directExchange).with("red");
}
@Bean
public Binding directQueue2BindingYellow(Queue directQueue2, DirectExchange directExchange) {
return BindingBuilder.bind(directQueue2).to(directExchange).with("yellow");
}
}
5.消息转换器
如果我们默认传一个消息,队列中的字符串将以序列化的形式写进去,我们进行测试,将如下的键值对传入队列:
java
@Test
public void testSendObject() {
//1.准备消息
Map<String, Object> msg = new HashMap<>(2);
msg.put("name", "yds");
msg.put("age", "20");
//2.发送消息
rabbitTemplate.convertAndSend("object.queue", msg);
}
传入后将看到如下信息,可以看到我们的键值对在这里是被序列化了,这是JDK自带的,不易阅读,而且占用的字节数较大,我们自然会想到使用更加直观常用的JSON格式了,那这就会涉及到消息的转换了。

要使用转换器,我们首先要先配置消息转换器,先导入JSON转换器的包:
XML
<!--jackson-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
然后编写配置类:(这里偷懒了,就直接在启动类中注册Bean了)
java
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
//消息转化器
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
注意:消费者和生产者都需要配置消息转化器。
最后写一个监听器来监听队列:
java
//消息转换器
@RabbitListener(queues = "object.queue")
public void listenObjectQueue(Map<String,Object> message) {
log.info("object.queue的消息:{}", message);
}
运行测试类,成功用JSON格式获取消息:

三、商城业务改造
首先要明确什么业务可以进行改造,我们发现,在支付微服务中将远程调用交易微服务,然后使用交易微服务对订单的支付状态进行更新,这一步既不是支付的核心步骤,也不需要很高的及时性,我们选择将这个远程同步调用改为异步调用。
(1)改业务
可以看到,先前远程调用交易微服务是在第五步:
现在我们改成消息队列的异步调用:
java
@Override
@GlobalTransactional
public void tryPayOrderByBalance(PayOrderFormDTO payOrderFormDTO) {
// 1.查询支付单
PayOrder po = getById(payOrderFormDTO.getId());
// 2.判断状态
if(!PayStatus.WAIT_BUYER_PAY.equalsValue(po.getStatus())){
// 订单不是未支付,状态异常
throw new BizIllegalException("交易已支付或关闭!");
}
// 3.尝试扣减余额
userClient.deductMoney(payOrderFormDTO.getPw(), po.getAmount());
// 4.修改支付单状态
boolean success = markPayOrderSuccess(payOrderFormDTO.getId(), LocalDateTime.now());
if (!success) {
throw new BizIllegalException("交易已支付或关闭!");
}
//TODO 5.修改订单状态
//tradeClient.markOrderPaySuccess(po.getBizOrderNo());
try {
rabbitTemplate.convertAndSend("pay.direct","pay.success",po.getBizOrderNo());
} catch (Exception e){
log.info("发送支付状态通知失败,订单id:{}",po.getBizOrderNo());
}
}
(2)MQ相关配置
在这之前,记得导包和配置MQ,我这里直接写了个共享配置进去,所以只需要在bootstrap中新增一个共享配置id即可:
导包:
XML
<!--amqp-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置:

java
spring:
application:
name: pay-service
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.242.130:8848
config:
file-extension: yaml
shared-configs:
- data-id: shared-jdbc.yaml
- data-id: shared-log.yaml
- data-id: shared-swagger.yaml
- data-id: shared-seata.yaml
- data-id: shared-mq.yaml
至于传入的消息格式嘛,还是使用JSON,所以别忘了配置消息转换器:
java
@Configuration
public class MqConfig {
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
我们在这里将消息转换器写到了hm-common中,因为几乎每个微服务都要用到这个消息转换器。
别忘了在spring.factories中去配置配置类(由于hm-common中没有启动类,所以无法自动识别配置类),否则配置无法生效。
(3)MQ声明和监听器
在交易微服务中创建监听器,专门用来修改订单的支付状态,同时声明MQ:
java
@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.direct"),
key = "pay.success"
))
public void listenPaySuccess(Long orderId){
orderService.markOrderPaySuccess(orderId);
}
}