1.前言
不知道你在开发中是否遇到过如下类似场景:消息消费了,但是由于代码问题,导致业务未正确执行。补偿吧,不知道要补偿哪些数据;不补偿吧,又要挨骂;找生产方重新推送吧,人家不大乐意,甚至不愿鸟你。此类问题,可以归结为最终一致性问题。什么是最终一致性呢?就是我不管你采用什么方式、什么手段,只要保证业务最终执行成功即可。如果是你,你会怎么保证消息消费的最终一致性呢?本文将带着你了解rabbitmq
消息队列实现最终一致性的方案。
- rabbtimq
2.1 默认ack方式
如你知道的那样,在springboot
框架中使用rabbitmq
,其ack
方式默认为AUTO
,从代码中也可以证实这一点AbstractRabbitListenerContainerFactory
中setAcknowledgeMode()
声明如下
1 | java复制代码/** |
此AUTO
的含义和你从rabbitmq
官方文档中了解的自动ack
是不一样的,它表示的是由框架本身自动帮你执行ack
和nack
,而不需要你自己手动去执行
2.2 AUTO逻辑代码
1 | java复制代码private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Exception { //NOSONAR |
2.3 消息提交
1 | java复制代码public boolean commitIfNecessary(boolean localTx) throws IOException { |
2.4 消息回滚
1 | java复制代码public void rollbackOnExceptionIfNecessary(Throwable ex) { |
2.5 小结
通过以上分析,想必你已经知道,框架会使用try catch
包裹我们写的消费者逻辑,如果消费者逻辑执行异常,则会将消息重新放入消息队列中;如果消息执行成功,则会进行ack
操作。有了这些理论基础后,一起来看个消费者示例代码。
2.6 消息消费示例
1 | java复制代码@RabbitHandler |
如上示例在没有触发异常的情况下,业务会正常执行;在触发异常的情况下,业务会执行失败,并且没法实现最终一致性。那么怎么做可以达到实现最终一致性的目的呢?
2.7 解决方案
一番分析过后,你不难发现如上示例无法实现最终一致性的原因其实是由开发者自身造成的,开发者使用了try catch
块,导致消费者逻辑不会抛出异常,在没有异常的场景下框架会自动进行ack
。因此,要想实现最终一致性,只需要去除try catch
即可
1 | java复制代码@Component |
2.8 执行结果
1 | java复制代码【receive message】:hello rabbitMQ |
从执行结果可以看到虽然中间出现了偶发异常,但是通过重试实现了业务数据的最终一致性
通过去除try catch
块的确实现了最终一致性,那么此方案会不会存在其它问题呢?
2.9 重试带来的问题
假如你一味地信任你的生产者,未对消息体内容进行校验,大部分场景下你的业务执行也都没有问题。但是你总有收到让你惊喜的消息体,此时你的业务会抛出异常,抛出异常后,框架自动进行nack
,将消息从新放入队列,消费者继续消费消息,然后又被放入消息队列,如此往复,不仅会消耗你的服务器资源,还可能影响其他服务,比如日志服务(因为你打了日志,导致日志量会徒增)。
因此,在利用重试实现最终一致性的时候,你一定要留个心眼,使用消息重试机制时一定要考虑异常场景可以通过重试自动进行修复(比如网络偶发抖动、接口偶发异常)。
2.10 数据库解决方案
只要你执行业务逻辑,总有你想不到的异常会发生,需要考虑的东西太多,反而是一种负担。如果你想最大程度实现最终一致性,那么你的消费者就只需要做一件事情:把消息存入数据库,异步消费。数据库发生故障的概率小之又小,即便异常,通过重试也可以将消息写入数据库,基本可以满足你业务的需求。
本文转载自: 掘金