RabbitMQ是一个高性能的异步通讯组件。(同步通信就像两个人打视频电话,实时传输数据,还不能有其他人再加入,异步通信像微信发消息,不具备实时性,也能有其他人加入。)
内容概述:
初识MQ:
同步调用:
以余额支付服务为例:
优势:
时效性强,等待到结果才返回
问题:
也存在一些问题,比如拓展性差、性能下降、级联失败问题(雪崩)。
异步调用:
以余额支付服务为例:
优势:
耦合度低,拓展性强
异步调用,无需等待,性能好
故障隔离,下游服务故障不影响上游服务
缓存消息,流量削峰填谷
问题:
不能立即得到调用结果,时效性差
不确定下游业务执行是否成功
业务安全依赖于Broker的可靠性
技术选型:
了解常用消息队列(MQ):
本文主要讲解RabbitMQ。
RabbitMQ:
RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网地址:
RabbitMQ: One broker to queue them all | RabbitMQ
介绍:
安装:
docker run \
-e RABBITMQ_DEFAULT_USER=han\
-e RABBITMQ_DEFAULT_PASS=123321 \
-v mq-plugins:/plugins \
--name mq \
--hostname mq \
-p 15672:15672 \
-p 5672:5672 \
--network my-net\
-d \
rabbitmq:3.8-management
使用:
打开RabbitMQ管理界面(通过ip与定义的端口号),可以让交换机绑定队列,可以利用交换机模拟发送消息,也可以在绑定的队列中查看发送的消息。
消息发送的注意事项:
数据隔离:
在RabbitMQ管理界面,点击Admin按钮,右侧选择Users,先新增一个用户:
新增的用户是没有绑定虚拟主机的,也不能查看别的虚拟主机内的队列中的信息。
接下来新建虚拟主机,注意切换到刚刚新建的用户登录然后创建,这样用户直接绑定登录用户。
Java客户端:
协议与规范:
SpringAmqp官方地址:Spring AMQP
快速入门:
1.创建队列:
2.引入依赖:
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3.配置信息:
spring:
rabbitmq:
host: ***.***.***.*** # 你的虚拟机IP
port: 5672 # 端口
virtual-host: /hmall # 虚拟主机
username: hmall # 用户名
password: 123 # 密码
4.发送消息:
利用RabbitTemplate发送消息
@SpringBootTest
class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue() {
//1.队列名
String queueName = "simple.queue";
//2.消息
String message = "Hello,Spring Amqp!";
//3.发送消息
rabbitTemplate.convertAndSend(queueName, message);
}
}
5.接收消息:
利用注解@RabbitListener声明要监听的队列,监听消息。
@Slf4j
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(String message) {
log.info("监听到simple.queue发送的消息: {}", message);
}
}
WorkQueue:
如果我们只简单的定义两个consumer监听一个queue,那么当publisher发送大量数据时,这两个consumer是不能接收到发送的全部消息的,比如consumer1会接收到1,3,5……,consumer2会接收到2,4,6……,即使两个consumer处理消息速度不一致,它们也会各自完成各自要接收的消息,然后快的会等待慢的,效率很低
消费者消息推送限制:
这样设置完成之后,发送大量消息,两个consumer就会能者多劳,会一直处理消息不停。
Work模型的使用:
交换机:
Fanout交换机:
演示:
定义两个消费者绑定不同队列
@Slf4j
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String message) {
log.info("消费者1监听到fanout.queue1发送的消息: {}", message);
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String message) {
log.info("消费者2监听到fanout.queue2发送的消息: {}", message);
}
}
编写测试类,通过fanout交换机发消息
@Test
public void testFanoutQueue() {
//1.交换机名
String exchangeName = "hmall.fanout";
//2.消息
String message = "Hello,everyone!";
//3.发送消息
rabbitTemplate.convertAndSend(exchangeName, null, message);
}
结果:
Direct交换机:
演示:
定义两个消费者绑定不同队列,绑定队列1的bindingkey为red和blue,队列2的bindingkey为red和yellow。
@Slf4j
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "direct.queue1")
public void listenDirectQueue1(String message) {
log.info("消费者1监听到direct.queue1发送的消息: {}", message);
}
@RabbitListener(queues = "direct.queue2")
public void listenDirectQueue2(String message) {
log.info("消费者2监听到direct.queue2发送的消息: {}", message);
}
}
编写测试类,通过direct交换机发消息
@Test
public void testDirectQueue() {
//1.交换机名
String exchangeName = "hmall.direct";
//2.消息
String message = "Hello,blue!";
//3.发送消息
rabbitTemplate.convertAndSend(exchangeName, "blue", message);
}
结果:
与Fanout差异:
Topic交换机:
演示:
定义两个消费者绑定不同队列,绑定队列1的bindingkey为china.#,队列2的bindingkey为#.news。
@Slf4j
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "topic.queue1")
public void listenTopicQueue1(String message) {
log.info("消费者1监听到topic.queue1发送的消息: {}", message);
}
@RabbitListener(queues = "topic.queue2")
public void listenTopicQueue2(String message) {
log.info("消费者2监听到topic.queue2发送的消息: {}", message);
}
}
编写测试类,通过direct交换机发消息
@Test
public void testTopicQueue() {
//1.交换机名
String exchangeName = "hmall.topic";
//2.消息
String message = "Hello,Chinese news!";
//3.发送消息
rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
}
结果:
与Direct差异:
代码声明队列交换机:
new方式:
bulider方式:
@Configuration
public class FanoutConfiguration {
@Bean
public FanoutExchange fanoutExchange() {
return ExchangeBuilder.fanoutExchange("hmall.fanout").build();
}
@Bean
public Queue fanoutQueue() {
return QueueBuilder.durable("hmall.fanout").build();
}
@Bean
public Binding fanoutQueue1Binding(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
}
注解方式:
消息转换器:
这样传递的其他集合等复杂Object类对象信息可以转化为json类型,更简洁、易懂。
业务实例改造:
1.引入依赖:
在消费者和发送者的pom.xml文件都引入依赖
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.配置RabbitMQ信息:
在消费者和发送者中都需要配置,这里我把它配置到了nacos注册中心的共享配置中。
spring:
rabbitmq:
host: ***.***.***.***# 你的虚拟机IP
port: 5672 # 端口
virtual-host: /hmall # 虚拟主机
username: hmall # 用户名
password: 123 # 密码
3.配置消息转换器
在消费者和发送者中都需要配置,我将它配置到了common模块下:
@Configuration
public class MqConfig {
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
因为在其他模块扫描不到该配置类,所以利用Spring自动装配原理,将这个类放入spring.factories文件中:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.MvcConfig,\
com.hmall.common.config.MqConfig,\
com.hmall.common.config.JsonConfig
4.编写消费者:
@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", type = "direct"),
key = "pay.success"
))
public void listenPaySuccess(Long orderId) {
orderService.markOrderPaySuccess(orderId);
}
}
5.编写发送者:
使用try,catch发送消息,这样不管消息发送是否成功,都不会对原代码造成影响。
@Override
@Transactional
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.error("发送支付状态通知失败,订单id:{}", po.getBizOrderNo(), e);
}
}
消息可靠性问题:
发送者的可靠性:
发送者重连:
注意:
发送者确认:
如何实现:
(使用@PostConstruct注解的方法必须是void类型,且不能有参数。这个方法会在对象创建并完成依赖注入后被自动调用。通常用于执行一些资源初始化、配置加载等操作。)
注意:
由于发送者确认需要与MQ进行通讯与确认,所以会大大的影响消息发送的效率,不推荐打开。如果要使用,也不要让消息重发无限的重试,要限制重发次数。
MQ的可靠性:
数据持久化:
交换机持久化:
默认是持久的
队列持久化:
和交换机持久化类似,并且默认也是持久的
消息持久化:
发送持久化的消息,不仅会保存到内存,还会写入磁盘中,即使MQ重启也不会消失,但是重启后非持久化的消息会消失。写代码使用Spring AMQP发送消息默认也是持久化的。
因为发送非持久化消息时,消息过多会写入磁盘,导致接收消息的速度呈现波浪形,效率很低。而发送持久化消息会对每条消息都持久化,并且接收速度可以一直处于大概峰值的位置,效率更高。
注意:
推荐打开它们的持久化,可以大大提高效率和可靠性。
Lazy Queue:
因为发送持久化的消息,不仅会保存到内存,还会写入磁盘中,耗时较长,就会导致整体的并发能力下降,为了解决这个问题,MQ引入了Lazy Queue。
如何使用:
总结:
RabbitMQ自身如何保证消息的可靠性:
消费者的可靠性:
消费者确认机制:
如何使用:
注意如果是接收数据后处理数据导致了业务异常,那么SpringAMQP是不会抛异常的,这种情况一般需要程序员自己编写代码让MQ返回nack或者reject。
失败重试策略:
失败消息处理策略:
业务幂等性:
幂等:
解决方案:
1.唯一消息id:
2.业务判断:
延迟消息:
介绍:
延迟消息:发送者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息。
延迟任务:设置在一定时间之后才执行的任务
死信交换机:
代码声明死信交换机:
代码设置发送消息过期时间:
延迟消息插件:
下载网址:
rabbitmq/rabbitmq-delayed-message-exchange: Delayed Messaging for RabbitMQ (github.com)
安装:
因为我们是基于Docker安装,所以需要先查看RabbitMQ的插件目录对应的数据卷。
docker volume inspect mq-plugins
结果如下:
[
{
"CreatedAt": "2024-06-19T09:22:59+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/mq-plugins/_data",
"Name": "mq-plugins",
"Options": null,
"Scope": "local"
}
]
插件目录被挂载到了/var/lib/docker/volumes/mq-plugins/_data
这个目录,我们上传插件到该目录下。
接下来执行命令,安装插件:
docker exec -it mq rabbitmq-plugins enable rabbitmq_delayed_message_exchange
使用:
首先将交换机的delay属性设为true
发送消息时需要通过x-delay来设置过期时间
注意:
因为每个延迟消息都有自己的计时器时钟,所以延迟消息过多对CPU压力会很大,我们应该尽量避免同一时间出现过多延迟消息,可以采用减少延迟等待时间的方式,一般采用10或15秒即10000或15000毫秒。
面试可能出现问题:
1.如何保证支付服务与交易服务之间的订单一致性?
2.如果交易服务消息处理失败,有什么兜底方案?
①消息重试:一种常见的方法是在消息处理失败后进行重试。您可以配置一个重试机制,例如在消息处理失败后将消息重新放回队列,让消费者再次尝试处理。您可以设置最大重试次数,避免无限重试导致死循环。
②死信队列(Dead Letter Exchange):通过配置死信队列,可以将处理失败的消息路由到一个专门的队列中,以便进一步处理或分析失败的消息。当消息处理失败时,可以将消息发送到死信队列,然后根据需要进行处理。
③消息持久化:确保消息是持久化的,这样即使消费者在处理消息时发生故障,消息也不会丢失。消息持久化可以通过将消息标记为持久化并配置队列为持久化来实现。
④监控和报警:设置监控和报警系统来及时发现消息处理失败的情况。通过监控消息队列的状态、消费者的运行状况以及消息处理失败的次数,可以及时发现问题并采取措施。
⑤人工干预:在极端情况下,如果消息处理失败的情况无法通过自动化手段解决,可能需要人工干预。在这种情况下,可以设置警报,通知相关人员介入处理。