06 | 如何处理消费过程中的重复消息?
06 | 如何处理消费过程中的重复消息?
讲述:李玥
时长13:59大小12.81M
消息重复的情况必然存在
用幂等性解决重复消息问题
小结
思考题
赞 25
提建议
精选留言(115)
- 微微一笑2019-08-03解决一个问题,往往会引发别的问题。若消息队列实现了exactly once,会引发的问题有:①消费端在pull消息时,需要检测此消息是否被消费,这个检测机制无疑会拉低消息消费的速度。可以预想到,随着消息的剧增,消费性能势必会急剧下降,导致消息积压;②检查机制还需要业务端去配合实现,若一条消息长时间未返回ack,消息队列需要去回调看下消费结果(这个类似于事物消息的回查机制)。这样就会增加业务端的压力,与很多的未知因素。 所以,消息队列不实现exactly once,而是at least once + 幂等性,这个幂等性让给我们去处理。展开
作者回复: 👍👍👍
236 - oscarwin2019-08-03我觉得最重要的原因是消息队列即使做到了Exactly once级别,consumer也还是要做幂等。因为在consumer从消息队列取消息这里,如果consumer消费成功,但是ack失败,consumer还是会取到重复的消息,所以消息队列花大力气做成Exactly once并不能解决业务侧消息重复的问题。
作者回复: 👍👍👍
共 13 条评论209 - linqw2019-08-03学习完如何处理消费过程中的重复消息,写下自己的理解,老师有空帮忙看下哦 1、使用数据库的唯一索引防止消息被重复消费,感觉如果业务系统存在分库分表,消费消息被路由到不同的库或表,还是会存在问题。 2、为更新的数据设置前置条件,可以在消息中附带属性,比如当前账户的总金额,或者表中多加一个版本号字段,配合数据库行锁,类似乐观锁的概念,Java CAS,比较内存中的旧值是否和预先的旧值相等,如果是替换成新值。存在的问题和1类似。 3、记录并检查操作,在每个消息中维护一个全局唯一的ID,根据全局唯一ID进行判断消息是否已经被消费。存在的问题,全局唯一ID的实现有一定的复杂度,需要确保检查消费状态、更新数据、以及更新消费状态三个操作原子性,解决方式涉及到分布式锁和分布式事务,并且对高性能、高并发也有一定的影响。 4、尝试回答下课后习题①设置成Exactly once从消息队列的角度来看,为了确保消息没有被丢失或者重复,队列需采取一定的类似回查的手段,检测消费者是否有收到消息进行处理,在一定程度上会导致队列堆积等一系列问题,并且队列实现的复杂度上升。②从消费者的角度而言,因为消费者端和Broker Service端都是会各自集群,消费者端可能会存在网络抖动,导致Broker Service为了确保消息不丢失和重复,需要一直进行回查类似的操作,但是由于网络问题,导致队列堆积。 5、有个疑问如果队列的实现是At least once,但是为了确保消息不丢失,Broker Service会进行一定的重试,但是不可能一直重试,如果一直重试失败怎么处理了?展开
作者回复: 第一个问题,一般来说分库分表也不会有问题,为什么?因为,使用我们的方法,对于一条具体的消息,总是会落到确定的某个库表上,它的重复消息也会落地同样的库表上,所以分库分表不是问题。 第五个问题,有的消息队列会有一个特殊的队列来保存这些总是消费失败的“坏消息”,然后继续消费之后的消息,避免坏消息卡死队列。这种坏消息一般不会是因为网络原因或者消费者死掉导致的,大多都是消息数据本身有问题,消费者的业务逻辑处理不了导致的。
共 9 条评论32 - Dovelol2019-08-07老师好,想问下关于幂等的情况,像设置帐户余额为100元,或者给余额为500的加100,如果有中间状态的变更或者ABA问题,也能算是幂等操作吗?
作者回复: 确实这个例子解决不了ABA问题,如果要解决这个问题,只能使用版本号的方式。
共 2 条评论26 - 年年2019-08-14这课买的太值了,是本平台最吸引我的一门课,一口气看了八篇
作者回复: 感谢支持!
共 6 条评论23 - 谢清2019-08-05exactly once,实现有性能损耗,并发高时易出现消息堆积;消息队列设计初衷是解决解耦,而解耦的对象往往是高并发,对性能要求较高的,从产品需求层面讲,消息队列设计更注重性能,而非精准(exactly once);基础架构角度来说,关注点是占比大的需求(不能不发,可以重发),占比极小的需求(敏感型,只能触发一次)可以单独抽出来另外实现。最后,请教老师有没有比较具体的业务场景,非用这种exactly once不可的17
- 游弋云端2019-08-03我的理解如下: 1、按照您给的公式:At least once + 幂等消费 = Exactly once,所以对于消息队列来讲,要做到Exactly once,其实是需消费端的共同配合(幂等消费)才可完成,消息队列基本只提供At least once的实现; 2、从给的几种幂等消费的方案看,需要引入数据库、条件更新、分布式事务或锁等额外辅助,消息队列如果需要保障Exactly once,会导致消费端代码侵入,例如需要消费端增加消息队列用来处理幂等的client端,而消费端的形态可是太多了,兼容适配工作量巨大。故这个Exactly once留给用户自己处理,并且具有选择权,毕竟不是所有业务场景都需要Exactly once,例如老师讲的机房温度上报的案例。展开
作者回复: 👍👍👍
16 - leslie2019-08-03对于老师说的为何都是支持At least once:是不是与以下几种情况相关;不对之处还望老师指出,因为我是刚好最近有时会有些异常数据联想到的也算是学习此课的初衷之一。 1.硬件异常或者系统异常导致的数据丢失:这里想咨询老师一下,消息队列为何不能做成像数据库一样的用undo log和redo log去避免硬件的这种异常。 2.就像为何网络协议中一样TCP和UDP的区别:消息反馈可能不是每一个反馈一次,有时是一批反馈异常,传输中可能会出现丢包或者顺序不一致。 最近几个刚好同时在学:刘超老师的网络协议、操作系统以及您的消息队列觉得之间有彼此的关系;能力有限,故而仅仅是猜测,只能通过不断的向各位老师学习才能不断的找出问题提升自己,不足之处还望老师提点-谢谢。展开
作者回复: A1:主要是出于性能考虑。 A2:大部分消息队列在实现的时候,都是批量收发的,但是,采用基于位置的确认机制,是可以保证顺序的。
共 2 条评论13 - a、2019-08-03因为目前消息队列,在发送消息给客户端的时候,一般需要客户端ack之后才能确定,这条消息是不是真的被消费了。 1.如果客户端设置的是自动ack,那么mq就能保证只发送一次,但是这样会因为客户端消费消息不成功,而导致消息丢失 2.如果客户端都设置手动ack,这样又有一个问题,如果mq发送消息给客户端成功了,客户端也已经消费完成了,就在准备ack的时候,和mq失去了联系,这时候mq是不知道,这条消息是否真的被消费了,只能选择重发消息。 所以我觉得:如果消息队列保证了只发一次,那么消息队列就无法保证消息由于客户端消费失败而不丢失,就好像分布式系统中的cap理论,只能保证其中的两种,而无法三个都保证。展开
作者回复: 架构设计就是在取舍之间选择最合适的实现方式。
共 2 条评论11 - 李先生2020-03-12我有个疑问,如果使用redis来实现幂等,那么在redis中设置的唯一id肯定要设置失效时间的。比如失效时间设置为10s,在这10s之内可以保证拥有唯一id的消息只被消费一次。那么10s之后又出现一个相同的唯一id,由于redis中这个唯一id已经失效,这个消息将再次被消费。这种如何处理呢?
作者回复: 你说的这个情况确实是这样,但10s(或者更长时间)之后再出现一个重复ID的情况是非常罕见的,所以也就无所谓的。 其实,很多工程上解决问题的方法,理论上都存在缺陷。比如几乎所有一致性算法都解决不了拜占庭将军问题,很多分布式事务理论也不能保证所有情况下的数据一致性。 但这些方法确实能解决实际问题,这才是我们需要关注的。
共 2 条评论8 - 张三丰2019-10-21文中有句话想跟老师确认下,如下: ”t0 时刻:Consumer A 收到条消息,检查消息执行状态,发现消息未处理过,开始执行“账户增加 100 元”; t1 时刻:Consumer B 收到条消息,检查消息执行状态,发现消息未处理过,因为这个时刻,Consumer A 还未来得及更新消息执行状态。” 1.这是因为每个队列配置多个消费组导致的吧? 2.通常情况下配置多个消费组是为了提升消费能力? ”t0 时刻:Consumer A 收到条消息,检查消息执行状态,发现消息未处理过,开始执行“账户增加 100 元”; t1 时刻:Consumer B 收到条消息,检查消息执行状态,发现消息未处理过,因为这个时刻,Consumer A 还未来得及更新消息执行状态。” 这个情况只能在有消息重发,且重发到了不同队列上才可能发生吧,否则再怎么重发都会由相同的消费者消费。展开
作者回复: 是的
共 2 条评论6 - 小伟2020-01-05李老师,我有个问题: 我看很多小伙伴们留言说业务端实现幂等就完全解决问题了,但我觉得幂等不是银弹,不能解决所有问题。 幂等只能保证消息被重复消费后的结果正确,但重复消费消息本身也是有代价的。举例,一个业务操作是写文件,执行成本是锁定文件、IO、网络传输、CPU时间片占用等,这些都是没有价值的,类似于不挂挡踩油门,听响而已。 个人观点,如果业务执行开销较小,那么业务幂等就够了。如果业务执行开销大,那么前置条件判断就比较好,虽然条件的判断也会有不小开销,但两害相权取其轻。如果任一种的代价都太大,那么看能否拆分业务操作,拆分后哪个代价小就用哪个。 另,根据老师的定义,这里的幂等“其任意多次执行所产生的影响均与一次执行的影响相同”应该不包含前置条件检查和记录并检查,因为这两个都没有“多次执行”,所以应该是与幂等并列的解决重复消息问题的实现方式。 有不妥处,请不吝指教。展开
作者回复: 实际上,MQ只有在异常情况下(比如Broker宕机,网络中断),才会产生重复消息,一般情况下是不会有重复消息的,所以代价和开销问题不用太考虑。
共 3 条评论4 - james2019-11-10第三种方案,b也会收到同样消息的原因是啥,是a消费时间太长重发导致的吗? kafka中每个consumer一个queue不会出现这种场景,而rocketmq的顺序消费也不会出现
作者回复: 正常情况下不会出现,如果有故障,比如broker故障、consumer故障或者网络连接抖动,都可能会出现重复消费,也就是一个消息被不同的consumer都消费到的情况,kafka和rocketmq都有可能会出现这种情况。
共 2 条评论4 - 丁小明2020-04-26老师关于幂等性第三点有些疑问,是不是队列开启ACK模式就能避免这个情况发生呢。重复消息投递应该投到相同队列。这样就不会出现一个consumerA还未消费完返回ACK之前,另一个consumerB就去消费下一条消息。对于同一个队列来说,队列是不是保证了串行消费。这样就只需要保证业务流水号唯一就行了。希望老师解答
作者回复: 一般来说,如果不做特殊的设置(比如按照key哈希到特定分区上),是保证不了“重复消息投递应该投到相同队列”的。
3 - Josey2020-03-04老师,我想问一下,假如消费者和生产者连接的不是同一个数据库,那怎么能通过数据库约束的方式实现幂等操作?还有全局ID的形式,如果这个ID的消息被消费过是要把处理状态更新到数据库中吗?如果也是连接的不同数据库,这种情况下怎么处理?忘老师回复!
作者回复: 不用要求生产者和消费者连同一个数据库。 生产者只要保证,发出去的每条转账消息都有一个唯一的转账单ID,这个“转账单 ID”可以存在生产者的数据库中,也可以不存,看业务需求。只要这个转账单ID不重复就可以了,这个很容易做到,比如我们用MySQL的Sequence就可以生成。 消费者用这个转账单ID结合数据库(消费者的数据库,可以和生产者数据库不同)的唯一约束,就可以来实现消费幂等了。
3 - Geek_8655952019-08-29具体的实现方法是,在发送消息时,给每条消息指定一个全局唯一的 ID,消费时,先根据这个 ID 检查这条消息是否有被消费过,如果没有消费过,才更新数据,然后将消费状态置为已消费。 请问:这个消费状态是什么?怎么获取这个消息状态,然后怎么设置它的状态?
作者回复: 消费状态就是这条消息是否被消费过了。 怎么获取和设置这个状态是由业务代码实现的,比如你可以用Redis来保存这个状态。
共 3 条评论3 - Shen2020-04-21业务系统本身得具有幂等性,不管是前端来的http请求还是其他业务系统发来的rpc请求,都有可能重复发送,业务系统在处理涉及交易等重要的业务逻辑时,要通过业务主键ID、业务单状态等进行幂等处理。2