rabbitTemplate.setReturnsCallback(returned -> {

//记录日志、发送邮件通知、落库定时任务扫描重发

});

}

}

测试的时候可以在发送消息时故意写错交换机、路由键的名称,然后就会回调到我们刚刚写的监听方法, cause 会给我们展示具体没有发到交换机的原因;returned 对象中包含了消息相关信息。

实际上据我了解一些企业并不会在这两个监听里面去做重发,为什么呢?成本太高了…首先 RabbitMQ 本身丢失的可能性就非常低,其次如果这里需要落库再用定时任务扫描重发还要开发一堆代码,分布式定时任务…再其次定时任务扫描肯定会增加消息延迟,不是很有必要。真实业务场景是记录一下日志就行了,方便问题回溯,顺便发个邮件给相关人员,如果真的极其罕见的是生产者弄丢消息,那么开发往数据库补数据就行了。

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 ,当发生消费报错之后,这个消息会被重回消息队列顶端,继续推送到消费端,继续消费这条消息,通常代码的报错并不会因为重试就能解决,所以这个消息将会出现这种情况:继续被消费,继续报错,重回队列,继续被消费…死循环

所以真实的场景一般是三种选择

当消费失败后将此消息存到 Redis,记录消费次数,如果消费了三次还是失败,就丢弃掉消息,记录日志落库保存 直接填 false ,不重回队列,记录日志、发送邮件等待开发手动处理 不启用手动 ack ,使用 SpringBoot 提供的消息重试

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));

允许消费端执行多次,保证数据不受影响

=============================================================================

数据库唯一键约束

如果消费端业务是新增操作,我们可以利用数据库的唯一键约束,比如优惠券流水表的优惠券编号,如果重复消费将会插入两条相同的优惠券编号记录,数据库会给我们报错,可以保证数据库数据不会插入两条。

数据库乐观锁思想

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

总结

就写到这了,也算是给这段时间的面试做一个总结,查漏补缺,祝自己好运吧,也希望正在求职或者打算跳槽的 程序员看到这个文章能有一点点帮助或收获,我就心满意足了。多思考,多问为什么。希望小伙伴们早点收到满意的offer! 越努力越幸运!

金九银十已经过了,就目前国内的面试模式来讲,在面试前积极的准备面试,复习整个 Java 知识体系将变得非常重要,可以很负责任的说一句,复习准备的是否充分,将直接影响你入职的成功率。但很多小伙伴却苦于没有合适的资料来回顾整个 Java 知识体系,或者有的小伙伴可能都不知道该从哪里开始复习。我偶然得到一份整理的资料,不论是从整个 Java 知识体系,还是从面试的角度来看,都是一份含技术量很高的资料。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取! 收到满意的offer! 越努力越幸运!**

金九银十已经过了,就目前国内的面试模式来讲,在面试前积极的准备面试,复习整个 Java 知识体系将变得非常重要,可以很负责任的说一句,复习准备的是否充分,将直接影响你入职的成功率。但很多小伙伴却苦于没有合适的资料来回顾整个 Java 知识体系,或者有的小伙伴可能都不知道该从哪里开始复习。我偶然得到一份整理的资料,不论是从整个 Java 知识体系,还是从面试的角度来看,都是一份含技术量很高的资料。

[外链图片转存中…(img-WGibASIL-1713372589196)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

推荐阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: