}
}
测试的时候可以在发送消息时故意写错交换机、路由键的名称,然后就会回调到我们刚刚写的监听方法, cause 会给我们展示具体没有发到交换机的原因;returned 对象中包含了消息相关信息。
实际上据我了解一些企业并不会在这两个监听里面去做重发,为什么呢?成本太高了…首先 RabbitMQ 本身丢失的可能性就非常低,其次如果这里需要落库再用定时任务扫描重发还要开发一堆代码,分布式定时任务…再其次定时任务扫描肯定会增加消息延迟,不是很有必要。真实业务场景是记录一下日志就行了,方便问题回溯,顺便发个邮件给相关人员,如果真的极其罕见的是生产者弄丢消息,那么开发往数据库补数据就行了。
========================================================================
不开启持久化的情况下 RabbitMQ 重启之后所有队列和消息都会消失,所以我们创建队列时设置持久化,发送消息时再设置消息的持久化即可(设置 deliveryMode 为 2 就行了)。一般来说在实际业务中持久化是必须开的。
==================================================================
所谓消费端弄丢消息就是消费端执行业务代码报错了,那么该做的业务其实没有做。比如创建订单成功了,优惠券结算报错了,默认情况下 RabbitMQ 只要把消息推送到消费者就会认为消息已经被消费,就从队列中删除了,但是优惠券还没有结算,这样就相当于消息变相丢失了。这种情况还是很常见的,毕竟我们开发人员不能保证自己的代码不报错,这种问题一定得解决。 否则用户下了订单,优惠券没有扣减,你这个月的绩效估计是没了…
RabbitMQ 给我们提供了消费者应答(ack)机制,默认情况下这个机制是自动应答,只要消息推送到消费者就会自动 ack ,然后 RabbitMQ 删除队列中的消息。启用手动应答之后我们在消费端调用 API 手动 ack 确认之后,RabbitMQ 才会从队列删除这条消息。
首先在配置文件中开启手动 ack
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual #手动应答
然后在消费端代码中手动应答签收消息
@RabbitListener(queues = “queue”)
public void listen(String object, Message message, Channel channel) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
log.info(“消费成功:{},消息内容:{}”, deliveryTag, object);
try {
/**
-
执行业务代码…
-
*/
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
log.error(“签收失败”, e);
try {
channel.basicNack(deliveryTag, false, true);
} catch (IOException exception) {
log.error(“拒签失败”, exception);
}
}
}
===============================================================
如果生产环境你用上述方案的代码,一旦发生一次消费报错你就会崩溃。因为 basicNack 方法的第三个参数代表是否重回队列,如果你填 false 那么消息就直接丢弃了,相当于没有保障消息可靠。如果你填 true ,当发生消费报错之后,这个消息会被重回消息队列顶端,继续推送到消费端,继续消费这条消息,通常代码的报错并不会因为重试就能解决,所以这个消息将会出现这种情况:继续被消费,继续报错,重回队列,继续被消费…死循环
所以真实的场景一般是三种选择
- **当消费失败后将此消息存到 Red
《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》
【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享
is,记录消费次数,如果消费了三次还是失败,就丢弃掉消息,记录日志落库保存**
-
直接填 false ,不重回队列,记录日志、发送邮件等待开发手动处理
-
不启用手动 ack ,使用 SpringBoot 提供的消息重试
=============================================================================
其实很多场景并不是一定要启用消费者应答模式,因为 SpringBoot 给我们提供了一种重试机制,当消费者执行的业务方法报错时会重试执行消费者业务方法。
启用 SpringBoot 提供的重试机制
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true
max-attempts: 3 #重试次数
消费者代码
@RabbitListener(queues = “queue”)
public void listen(String object, Message message, Channel channel) throws IOException {
try {
/**
-
执行业务代码…
-
*/
int i = 1 / 0; //故意报错测试
} catch (Exception e) {
log.error(“签收失败”, e);
/**
-
记录日志、发送邮件、保存消息到数据库,落库之前判断如果消息已经落库就不保存
-
*/
throw new RuntimeException(“消息消费失败”);
}
}
注意一定要手动 throw 一个异常,因为 SpringBoot 触发重试是根据方法中发生未捕捉的异常来决定的。值得注意的是这个重试是 SpringBoot 提供的,重新执行消费者方法,而不是让 RabbitMQ 重新推送消息。
==================================================================
其实认真研究下来你会发现所谓的消息可靠性本身就是无法保证的…所谓的各种可靠性机制只是为了以后消息丢失提供可查询的日志而已,不过通过这些机制耗费一些(巨大)成本的确是能够缩小消息丢失的可能性
================================================================
有些业务场景会需要让消息顺序消费,比如使用 canal 订阅 MySQL 的 binary 日志来更新 Redis,通常我们会把 canal 订阅到的数据变化发送到消息队列。
如果不保证 RabbitMQ 的顺序消费, Redis 中就有可能会出现脏数据。
==================================================================
其实队列本身是有顺序的,但是生产环境服务实例一般都是集群,当消费者是多个实例时,队列中的消息会分发到所有实例进行消费(同一个消息只能发给一个消费者实例),这样就不能保证消息顺序的消费,因为你不能确保哪台机器执行消费端业务代码的速度快
所以对于需要保证顺序消费的业务,我们可以只部署一个消费者实例,然后设置 RabbitMQ 每次只推送一个消息,再开启手动 ack 即可,配置如下
spring:
rabbitmq:
listener:
simple:
prefetch: 1 #每次只推送一个消息
acknowledge-mode: manual
这样 RabbitMQ 每次只会从队列推送一个消息过来,处理完成之后我们 ack 回应,再消费下一个,就能确保消息顺序性。
==================================================================
RabbitMQ 多消费实例情况下要想保证消息的顺序性,非常困难,细节非常多,一句话:我不会…
======================================================================
这个也是生产环境业务中经常出现的场景,我的博客使用了 RabbitMQ ,就很奇怪经常日志上会显示消息被消费了两次。
我们解决消息重复消费有两种角度,第一种就是不让消费端执行两次,第二种是让它重复消费了,但是不会对我的业务数据造成影响就行了。
=====================================================================
一般来说消息重复消费都是在短暂的一瞬间消费多次,我们可以使用 redis 将消费过的消息唯一标识存储起来,然后在消费端业务执行之前判断 redis 中是否已经存在这个标识。举个例子,订单使用优惠券后,要通知优惠券系统,增加使用流水。这里可以用订单号 + 优惠券 id 做唯一标识。业务开始先判断 redis 是否已经存在这个标识,如果已经存在代表处理过了。不存在就放进 redis 设置过期时间,执行业务。
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(“orderNo+couponId”);
//先检查这条消息是不是已经消费过了
if (!Boolean.TRUE.equals(flag)) {
return;
}
//执行业务…
//消费过的标识存储到 Redis,10 秒过期
stringRedisTemplate.opsForValue().set(“orderNo+couponId”,“1”, Duration.ofSeconds(10L));
=============================================================================
- 数据库唯一键约束
如果消费端业务是新增操作,我们可以利用数据库的唯一键约束,比如优惠券流水表的优惠券编号,如果重复消费将会插入两条相同的优惠券编号记录,数据库会给我们报错,可以保证数据库数据不会插入两条。
- 数据库乐观锁思想
如果消费端业务是更新操作,可以给业务表加一个 version 字段,每次更新把 version 作为条件,更新之后 version + 1。由于 MySQL 的 innoDB 是行锁,当其中一个请求成功更新之后,另一个请求才能进来,由于版本号 version 已经变成 2,必定更新的 SQL 语句影响行数为 0,不会影响数据库数据。
===================================================================
所谓消息积压一般是由于消费端消费的速度远小于生产者发消息的速度,导致大量消息在 RabbitMQ 的队列中无法消费。
其实这玩意我也不知道为什么面试这么喜欢问…既然消费者速度跟不上生产者,那么提高消费者的速度就行了呀!个人认为有以下几种思路