探索RocketMQ的重复消费和乱序问题
前言
在之前的MQ专题中,我们已经解决了消息中间件的一大难题,消息丢失问题。
但MQ在实际应用中不是说保证消息不丢失就万无一失了,它还有两个令人头疼的问题:重复消费和乱序。
今天我们就来聊一聊这两个常见的问题,看看RocketMQ是如何解决这两个问题的。
为什么会重复消费
首先我们来聊一聊重复消费的问题,要解决一个问题最开始的一步当然是去查找问题发生的原因了。
那出现重复消费的原因到底是什么呢?
我们先来思考一下生产者发送消息这一过程中是不是有可能重复发送消息到MQ呢?
答案是肯定的,比如生产者发送消息的时候使用了重试机制,发送消息后由于网络原因没有收到MQ的响应信息,报了个超时异常,然后又去重新发送了一次消息。
但其实MQ已经接到了消息,并返回了响应,只是因为网络原因超时了。
这种情况下,一条消息就会被发送两次。

当然,这只是列举了一种情况,实际有很多情况会造成消息的重新发送。
那么假如生产者没有重复发送消息,消费者就能保证不重复消费了吗?
当然不能保证,我们知道,在消费者处理了一条消息后会返回一个offset给MQ,证明这条消息被处理过了。
但是,假如这条消息已经处理过了,在返回offset给MQ的时候服务宕机了,MQ就没有接收到这条offset,那么服务重启后会再次消费这条消息。

如何解决重复消费
解决重复消费的关键就是引入幂等性机制,什么是幂等性机制呢?我们可以把它理解成,假如一个接口被重复调用,依然可以保证数据的准确性。
对于生产者重复发送消息到MQ这一过程,其实我们没有必要去保证幂等性,只要在消费者处理消息时保证幂等性就可以了。
这块其实就比较简单了,只要处理消息之前先根据业务判断一下本次操作是否已经执行过了,如果已经执行过了,那就不再执行了,这样就可以保证消费者的幂等性。
举个例子,比如每条消息都会有一条唯一的消息ID,消费者接收到消息会存储消息日志,如果日志中存在相同ID的消息,就证明这条消息已经被处理过了。
消息重试、延时消息、死信队列
解决完重复消费问题,我们来思考一种极端情况,比如某一时刻,消费者操作的数据库宕机了,这个时候消费者会发生异常,当然不能返回给MQ一个CONSUME_SUCCESS了,我们可以返回RECONSUME_LATER,他的意思是我现在没法处理这些消息,一会再来试试能不能处理。
简单来说,RocketMQ会有一个针对当前Consumer Group的重试队列,如果你返回了RECONSUME_LATER,MQ会把你的这批消费放到当前消费组的重试队列中,然后过一段时间重试队列中的消息会再次发送给消费者,默认可以重试16次,每次重试的间隔是不同的,这个时间间隔是可以配置的,默认配置如下:
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
细心的小伙伴会发现,这个配置一共有18个时间,为什么最多重试16次,配置中却有18个时间呢,这里就要说到延时消息了。
上边的配置其实不是针对重试队列的,而是针对延时消息的,18个时间分别代表延迟level1-level18,延时消息大概流程如下:
1 所有的延迟消息到达broker后,会存放到SCHEDULE_TOPIC_XXX的topic下(这个topic比较特殊,对客户端是不可见的,包括使用rocketmq-console,也查不到这个topic)
2 SCHEDULE_TOPIC_XXX这个topic下存在18个队列,每个队列中存放的消息都是同一个延迟级别消息
3 broker端启动了一个timer和timerTask的任务,定时从此topic下拉取数据,如果延迟时间到了,就会把此消息发送到指定的topic下,完成延迟消息的发送
刚才我们说如果你返回了RECONSUME_LATER,消息就会进入重试队列,其实不完全准确。
当MQ接收到RECONSUME_LATER后,首先会完成消息的转换,把消息存到延时队列中,然后再根据消息的延时时间保存到重试队列中。
如果重试了16次之后依然无法处理,就会把这些消费放入死信队列。死信队列中的消息RocketMQ不会再做处理,这部分数据要怎么处理就要看我们的业务场景了,我们可以做一个后台线程去订阅这个死信队列,完成后续消息的处理。

消息乱序
接下来我们聊一聊消息乱序问题,为什么会出现这个问题呢,这个其实不难理解。
我们都学过,每个Topic可以有多个MessageQueue,写入消息的时候实际上会平均分配给不同的MessageQueue。
然后假如我们有一个Consume Group,这个消费组中的每台机器都会负责一部分MessageQueue,那么就会导致消息的顺序乱序问题。
举个例子,生产者发送了两条顺序消息,先是insert,后是update,分别分配到两个MessageQueue中,消费者组中的两台机器分别处理两个队列的消息,这个时候是无法保证顺序性的,有可能会先执行update,后执行insert,导致数据发生错误。
那么如何解决消息乱序问题呢?
其实道理也很简单,把需要保持顺序的消息都放入到同一个MessageQueue中,让同一台机器处理不就可以了吗。
我们完全可以根据唯一ID与队列的数量进行hash运算,保证这些消息进入到同一个队列中,最简单的算法就是取余运算了。
现在我们能保证这批消息进入到同一个队列中了,似乎这样就能保证消息不会乱序了,但真的是这样吗?
上文我们说到如果消费者数据库出现问题,使用重试队列重试消息,那么对于需要保证顺序的消息也可以使用这套方案吗?
肯定是不能的,如果使用重试机制是无法保证顺序性的。
RocketMQ提供了另一个状态,SUSPEND_CURRENT_QUEUE_A_MOMENT,意思是先等一会,再接着处理这批消息,而不是把这批消息放入重试队列里去处理其他消息。
所以我们只要返回这个状态就可以了。
总结
好了,到这里关于RocketMQ重复消费和乱序问题的产生原因和解决方案我们就介绍完了,同时也介绍了RocketMQ的重试机制、延时消息和死信队列。
有些地方可能比较复杂,可能需要小伙伴们重复阅读几次才能理解,如果哪里有想不清楚的,或者有疑问的可以联系王子共同探讨。
往期文章推荐:

探索RocketMQ的重复消费和乱序问题的更多相关文章
- Wireshark抓包实例分析TCP重复ACK与乱序
转载请在文首保留原文出处: EMC 中文支持论坛https://community.emc.com/go/chinese 介绍 TCP 的一大常见问题在于重复 ACK 与快速重传.这一现象的发生也是由 ...
- 程序重启RocketMQ消息重复消费
最近在调试RocketMQ消息发送与消费的Demo时,发现一个问题:只要重启程序,RocketMQ消息就会重复消费. 那么这是什么原因导致的,又该如何解决呢? 经过一番排查,发现程序使用的Rocket ...
- RocketMQ(消息重发、重复消费、事务、消息模式)
分布式开放消息系统(RocketMQ)的原理与实践 RocketMQ基础:https://github.com/apache/rocketmq/tree/rocketmq-all-4.5.1/docs ...
- 疯狂位图之——位图生成12GB无重复随机乱序大整数集
上一篇讲述了用位图实现无重复数据的排序,排序算法一下就写好了,想弄个大点数据测试一下,因为小数据在内存中快排已经很快. 一.生成的数据集要求 1.数据为0--2147483647(2^31-1)范围内 ...
- 笔试算法题(28):删除乱序链表中的重复项 & 找出已经排好序的两个数组中的相同项
出题:给定一个乱序链表,节点值为ASCII字符,但是其中有重复项,要求去除重复项并保证不改变剩余项的原有顺序: 分析:创建一个256(2^8)大小的bool数组,初始化为false,顺序读取链表,将字 ...
- RocketMq重复消费问题排查
前情 出现了重复消费的问题,同一个消息被重复消费了多次,导致了用户端收到了多条重复的消息,最终排查发现,是因为消费者在处理消息的方法onMessage中有异常没有捕获到,导致异常上抛,被consume ...
- Kafka丢数据、重复消费、顺序消费的问题
面试官:今天我想问下,你觉得Kafka会丢数据吗? 候选者:嗯,使用Kafka时,有可能会有以下场景会丢消息 候选者:比如说,我们用Producer发消息至Broker的时候,就有可能会丢消息 候选者 ...
- FlinkSQL 之乱序问题
乱序问题 在业务编写 FlinkSQL 时, 非常常见的就是乱序相关问题, 在出现问题时,非常难以排查,且无法稳定复现,这样无论是业务方,还是平台方,都处于一种非常尴尬的地步. 在实时 join 中, ...
- 由乱序播放说开了去-数组的打乱算法Fisher–Yates Shuffle
之前用HTML5的Audio API写了个音乐频谱效果,再之后又加了个播放列表就成了个简单的播放器,其中弄了个功能是'Shuffle'也就是一般播放器都有的列表打乱功能,或者理解为随机播放. 但我觉得 ...
随机推荐
- 【Go语言入门系列】Go语言工作目录介绍及命令工具的使用
[Go语言入门系列]前面的文章: [保姆级教程]手把手教你进行Go语言环境安装及相关VSCode配置 [Go语言入门系列](八)Go语言是不是面向对象语言? [Go语言入门系列](九)写这些就是为了搞 ...
- php curl 获取请求头与DNS解析
1 php-curl方法相关设置具体方法在最下方的示例函数有相关编著, 这里主要描述两个小众需求a 设置访问DNS解析问题点: get请求网页获取返回值速度很快, 但是使用curl请求数据时, 响应速 ...
- 在SpringBoot项目中怎样引入.yml文件中的设置
SpringBoot中获取application.yml文件内容 原始方式pro.load()与 pro.getProperty()配合的方式 @Value注解方式 @ConfigurationPro ...
- Cesium资料
CesiumLab论坛:https://github.com/cesiumlab/cesium-lab-forum/issues简书上的Cesium实验室文集:https://www.jianshu. ...
- bufferedReader 读取文件第一行第一个字符丢失问题
在做一个解析pacp文件的时候需要读取文件中的每个属性,但是每次读出来的内容的每一行的第一个字符都被吞掉了,找了半天不知道为什么,后来看到在读取的时候用的read()方法,而且返回值是int类型,在变 ...
- java流程控制学习
Java流程控制 计算的步骤就是算法. 1.用户交互Scanner next()不能得到带有空格的字符串.[它是以空格为结束符]nextline()可以,[它是以回车为结束符] 2.顺序结构 从上到下 ...
- 多测师讲解selenium—自动化测试课堂面试题总结—高级讲师肖sir
1.你有做过自动化?你用什么语言? python2.自动化中如何使用语言打开一个网址?浏览器,浏览器对应驱动,导入库,类,get,url3.在一个浏览器中打开多个窗口?open_windows dri ...
- 2019年CSP-J初赛试题(普及组)试题详解
https://www.jianshu.com/p/9e58f455c1ee https://blog.csdn.net/weixin_39104847/article/details/1086711 ...
- C语言实现表达式求值,支持+、-、*、/四则运算,并且支持多级括号,自定义了栈的操作。
以下是代码的实现使用gcc已经成功运行了,下面是效果图 #include <stdio.h> #include <stdlib.h> #define OPT_ADD 43 /* ...
- JVM系列【6】GC与调优2.md
JVM系列笔记目录 虚拟机的基础概念 class文件结构 class文件加载过程 jvm内存模型 JVM常用指令 GC与调优 了解HotSpot常用命令行参数 JVM的命令行参数参考: https:/ ...