基础MQ
同步调用优势:时效性强,等待结果后返回
同步调用问题:拓展性差、性能下降、级联失败问题
异步调用的问题:
- 不能立即得到调用结果,时效性差
- 不能确定下游业务执行是否成功
- 业务安全依赖于Broker的可靠性
- 架构复杂,后期维护和调试麻烦
RabbitMQ的安装参考黑马在线文档
消息发送的注意事项:
1.交换机智能路由消息,无法存储消息
2.交换机只会路由消息给与其绑定的队列,因此队列必需与交换机绑定
数据隔离具体操作步骤见黑马在线文档
1.Java客户端-SpringAmqp
开发业务功能时候不会直接在控制台收发消息,RabbitMQ官方提供的Java客户端编码相对复杂,一般生产环境下我们更多会结合Spring来使用。而Spring的官方刚好基于RabbitMQ提供了这样一套消息收发的模板工具:SpringAMQP。并且还基于SpringBoot对其实现了自动装配,使用起来非常方便。
SpringAMQP提供了三个功能:
- 自动声明队列、交换机及其绑定关系
- 基于注解的监听器模式,异步接收消息
- 封装了RabbitTemplate工具,用于发送消息
SpringAmqp简单收发消息过程:
1.1WorkQueues - 任务模型
任务模型:简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息
背景:当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。多个消费者共同处理消息处理,消息处理的速度就能大大提高了。但消费者处理消息的速度也不同,故均分也不合理。可以通过prefetch来设置让其处理完当前的消息才能取下一条。
prefetch在消费者的配置文件中添加配置:
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
1.2交换机
Exchange(交换机)只负责转发消息,不具备存储消息的能力
交换机类型:
- Fanout:广播,将消息交给所有绑定到交换机的队列。我们最早在控制台使用的正是Fanout交换机
- Direct:订阅,基于RoutingKey(路由key)发送给订阅了消息的队列
- Topic:通配符订阅,与Direct类似,只不过RoutingKey可以使用通配符
- Headers:头匹配,基于MQ的消息头匹配,用的较少。
Fanout交换机:
Direct交换机:
描述下Direct交换机与Fanout交换机的差异?
- Fanout交换机将消息路由给每一个与之绑定的队列
- Direct交换机根据RoutingKey判断路由给哪个队列
- 如果多个队列具有相同的RoutingKey,则与Fanout功能类似
Topic交换机:
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定BindingKey 的时候使用通配符
通配符验证:
- #:匹配一个或多个词
- *:匹配不多不少恰好1个词
举例: - item.#:能够匹配item.spu.insert 或者 item.spu
- item.*:只能匹配item.spu
描述下Direct交换机与Topic交换机的差异?
- Topic交换机接收的消息RoutingKey必须是多个单词,以 . 分割
- Topic交换机与队列绑定时的bindingKey可以指定通配符
- #:代表0个或多个词
- *:代表1个词
声明队列和交换机:
基于RabbitMQ控制台来创建队列、交换机。但是在实际开发时,队列和交换机是程序员定义的,将来项目上线,又要交给运维去创建。那么程序员就需要把程序中运行的所有队列和交换机都写下来,交给运维。在这个过程中是很容易出现错误的。推荐的做法是由程序启动时检查队列和交换机是否存在,如果不存在自动创建。
基于Bean声明队列和交换机:
基于注解声明队列和交换机:
消息转换器:
业务改造:
- 添加amqp依赖
- 配置文件配置MQ地址
- 配置消息转换器,生产者消费者都使用故配置在hm-common模块,此时配置没有生效,其他微服务不包包名不一样不会扫描该配置类,因此采用Springboot自动装配原理在“wp-hmall\hm-common\src\main\resources\META-INF\spring.factories”添加config,让Springboot能够扫描到。
- 在trade-service服务中定义一个消息监听类(文档定义的交换机名称为pay.topic)
- 改pay-service服务下的com.hmall.pay.service.impl.PayOrderServiceImpl类中的tryPayOrderByBalance方法(此时文档使用的交换机名称为pay.direct,和第4步不一致,以视频为主,定义为pay.direct)
注意在common模块配置消息转换器之后其他模块报错:由于所有的服务都依赖于common,当MqConfig自动配置到Spring时,除去trade和pay服务,其他服务也会自动注入MqConfig中的消息转换器Bean,但其他服务并没有引入amqp的依赖,自然注入失败报错。两种解决方法都行,一种是common里面引amqp的依赖,另一种就是ConditionalOnClass(在MqConfig配置类上添加@ConditionalOnClass(RabbitTemplate.class))
拓展作业第一题,将MQ配置抽取到Nacos中管理,微服务中直接使用共享配置,此时需要给pay-service也添加bootstrap.yaml文件,不要忘了添加相关的两个依赖
<!--统一配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
拓展作业第二题:改造下单功能,将基于OpenFeign的清理购物车同步调用,改为基于RabbitMQ的异步通知
- cart服务添加amqp依赖,并在bootstrap.yaml文件文件添加mq配置
- 在cart-service服务中定义一个消息监听类
package com.hmall.cart.listener;
import com.hmall.cart.service.ICartService;
import com.hmall.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import org.springframework.amqp.core.Message;
import java.util.Collection;
@Component
@Slf4j
@RequiredArgsConstructor
public class PaySuccessListener {
private final ICartService cartService;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "cart.clear.queue", durable = "true"),
exchange = @Exchange(name = "trade.topic"),
key = "order.create"
))
public void listenPaySuccess(Collection<Long> itemIds, Message message){
Long userId = message.getMessageProperties().getHeader("userId");
log.info("监听到清空购物车的用户id:{},商品id:{}", userId,itemIds);
UserContext.setUser(userId);
cartService.removeByItemIds(itemIds);
}
}
- 改trade-service服务下的trade-service\src\main\java\com\hmall\trade\service\impl\OrderServiceImpl.java类中的createOrder方法的第3步cartClient.deleteCartItemByIds清理购物车商品,不需要cartClient,只需要向队列发送消息。
rrabbitTemplate.convertAndSend("trade.topic", "order.create", itemIds, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
Long userId = UserContext.getUser();
log.info("清理购物车商品消息发送成功,用户id,{}", userId);
message.getMessageProperties().setHeader("userId",userId);
return message;
}
});
重启服务cartservice报错:上述报错可以看到错误处在cartservice,才发现是之前注入cartservice时候private final ICartService cartService少了final
步骤2和3的代码我是参考评论里的大佬,评论里还有另一种写法放在这里: