如何处理消息队列中的重复消息
开篇
前面的文章中我们讨论了消息丢失的情况, 也介绍了如何在生产, 存储和消费阶段如何保证消息的可靠性。不知道大家有没有注意到这样一种情况,如果生产者成功将消息发送到 Broker, Broker 也成功将消息写入队列,但是在想生产者返回确认信息的时网络出现问题,导致生产者长时间没有收到确认的回复。这时生产者如果重新发送消息,是不是就重复了?
再搬出我们经常举的转账的例子,比如我要给我男朋友转 21 块钱, 如果我发送了两次转账的消息,那不就亏大方了, 我这么穷怎么能允许把我的钱都转走了。那该怎么处理重复的消息的问题呢?
消息中间件可以保证消息不重复吗
作为消息中间件的使用者,你是不是跟我一样,如果消息中间件可以帮我们保证消息不重复是不是就太棒了,毕竟开箱即用的简单是每个使用者都爱的呀, 那我们就看看消息中间件有没有可能实现这个功能。
MQTT 协议中, 给出了三种传递消息时能够提供的服务质量标准, 分别是:
At most once: 至多一次。消息在传递时,最多会被送达一次。换一个说法就是,没什么消息可靠性保证,允许丢消息。一般都是一些对消息可靠性要求不太高的监控场景使用,比如每分钟上报一次机房温度数据,可以接受数据少量丢失。
At least once: 至少一次。消息在传递时,至少会被送达一次。也就是说,不允许丢消息,但是允许有少量重复消息出现。
Exactly once:恰好一次。消息在传递时,只会被送达一次,不允许丢失也不允许重复,这个是最高的等级。
目前我们常用的消息中间件都使用的是 At least once(至少一次的方案),也就是说,消息中间件都不能保证消息不重复。这么看来,我们希望消息中间件替我们解决消息不重复的梦想算是破灭了。
解决方案----幂等性
目前解决重复消息的方法是在消费端保证消息消费的幂等性,即在消息的消费过程中,无论消费多少次,执行的结果都是相同的。
在回到我们开篇提到的转账的例子,我要给我男盆友转账 21 元,那每消费一次都会都会给他转账 21 元,这种情况就不是幂等的。但是我的消息是把我男朋友的账号余额变成 21 元,那无论这个消息被消费多少次,结果都是一样的, 就是账户余额变成 21 元,这个操作就是幂等性的。
从上面的分析可以知道尽管消息中间件做不到 Exactly once, 但是我们可以通过 At least once 和幂等性来避免重复消息带来的影响,那如果实现消息的幂等性呢?
利用数据库的唯一约束实现幂等性
基于数据库的唯一键来保证重复数据不会重复插入多条,比如我们的转账例子,可以创建一个转账记录表,表中为账户 ID 和账单 ID 建立唯一的索引, 所以针对同一个账户 ID 和账单 ID 至多只有一条记录。
那实际过程中,就可以先将转账记录表中插入一条数据,然后再根据转账记录表,异步的更新用户的账户余额,这样重复的账单和账户就不会重复插入多条数据来,也就避免了重复消息。
为更新的数据设置前置条件
如果在实际场景中没办法建立唯一的索引, 我们还可以转换一种思路:为要更新的数据设置前置条件,即当满足一定条件时才执行更新操作。
还是转账的例子,我们的消息可以携带着当前的账户余额,在执行更新操作前,先判断账户中的余额是否和消息中携带的余额一致, 一致才进行更新,否则不执行更新操作。
记录并检查操作
如果前两种方式都不适用,还可以适用最后这种记录并检查的通用方法,即在发送消息时,为每条消息制定一个全局唯一的 ID,消费时,要先根据全局唯一 ID 检查这条消息是否被消费过,如果没有消费过才会更新数据,然后将消费状态变成已消费。
总结
今天,我们学习了消费队列的重复消息的问题,并介绍了解决重复消息的核心方法----幂等性。也介绍了三种常用的方法,利用数据库唯一约束, 为更新的数据设置前置条件和记录并检查操作方法。实际使用中可以根据自己的实际使用场景采用合适的方案。
版权声明: 本文为 InfoQ 作者【废材姑娘】的原创文章。
原文链接:【http://xie.infoq.cn/article/4739e68048a061532367fff26】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论