消息重复问题

在消息传递过程中,如果出现传递失败的情况,发送发会执行重试,重试的过程就又可能会产生重复消息,对使用消息队列的业务系统来说,如果没有对重复消息进行处理,就又可能会导致系统数据出现错误。

消费重复的情况是必然存在

在MQTT协议中,给出了三种传递消息是提供的服务质量标准,这三种服务质量从低到高依次是:
At most once:至多一次;在消息传递时,最多会被送达一次,换一个说法就是,没有什么消息可靠性保证,允许丢消息,一般都是一些对消息可靠性要求不高的监控场景使用,可以接受数据少量丢失。
At least once:至少一次;消息传递时,至少会被送达一次,也就是说,不丢消息,但允许有少量重复消息出现;
Exactly once:恰好一次;消息在传递,只会被送达一次,不允许丢消息,也不允许重复,这是最高等级。
这个服务标准不仅适用于MQTT,对所有消息队列都是使用的,我们现在常用的绝大部分消息队列提供的服务质量都是At least once ,包括RocketMQ、RabbitMQ和kafka都是、

用幂等性解决消费重复问题

一般解决重复消费的办法是,在消费端,让我们消费消息的操作具备幂等性。
幂等是一个数学上的概念。拓展到计算机领域,被用来描述一个操作、方法或者服务。
一个幂等操作的特点是:
任意多次执行所产生的影响均与一次执行的影响相同。
一个幂等方法,使用同样的参数,对他进行多次调用和一次调用,对系统产生的影响是一样的,不用担心重复执行对系统造成任何改变。
在不考虑并发的情况下,将账户X的余额设置为100元,执行一次和多次,账户的余额依然是一百,只要参数不变,结果就不会改变,这就是一个幂等操作。
将账户X余额增加100,这个操作就不幂等,每执行一次该操作,账户余额就会多出一百元,执行一次和多次对系统的影响是不一样的。
对系统的营销结果来说,At least once +幂等消费 = Exactly once。

几种常见的设置幂等的方法

1、利用数据库的唯一约束实现幂等

首先,我们可以限定,对每个账户转账每个账户只能执行一次变更操作,在分布式系统中,这个限制实现的方法有很多,最简单的是我们在数据库建一张转账流水表,这个表三个字段:转账ID、账户ID和金额联合起来创建一个唯一约束,对于相同的转账ID和账户ID,表里至多存在一条记录。
这样,我们消费消息的逻辑可以变为:在转账流水表中增加一条转账记录,然后再根据转账记录,异步操作更新用户余额即可,在转账流水表中增加一条转账记录的操作中,由于我们这个表中定义了账户ID 和转账ID 的唯一约束,对于同一个转账单,同一个账户,只能插入一条记录,后续重复插入的操作都会失败,这样就实现了一个幂等操作,我们只需要写一个sq正确的实现它就可以了。

2、为更新的数据设置前置条件

给数据变量设置一个前置条件,如果满足条件就可以更新数据,否则拒绝更新数据,在更新数据的时候,同事变更前置条件中需要判断的数据;这样重复执行这个操作,由于第一次更新数据的时候已经变更了前置条件中需要判断的依据,不满足前置条件的就不会执行重复更新的操作了
比如说,x账户余额为100元,我必须满足这个条件才能进行操作;
但是如果我们要更新的数据不是数值,或者我们要做一个比较复杂的更新操作,更加通用的方法是,给数据增加一个版本号属性,每次更新数据前,比较当前数据的版本是否和消息中的版本号一致;如果不一致就会拒绝更新数据,更新数据的同时将版本号+1,一样可以实现幂等更新;

3、记录并检查操作

还有一致通用性强的,记录并检查操作,也称为token机制或者CUID(全局唯一ID)机制,实现思路:在执行数据更新操作之前,先检查是否执行这个操作,在消费发送时,给每一个消息指定一个全局的唯一ID,消费时,先根据这个ID检查这条消息是否被消费过,才更新数据,然后将消费状态设置为已消费;
但是在分布式系统中,这个方法实现起来非常难,不太好同时满足简单、高可用、高性能。或多或少有些牺牲,更加麻烦的是,在检查消费状态,然后更新数据并设置状态中,三个操作必须作为一组操作保障原子操作 ,才能真正的实现幂等,否则会出现bug
对于原子操作,我们可以用事务来实现,也可以用锁来实现。

总结:

1、使用数据库的唯一索引防止消息被重复消费,感觉如果业务系统存在分库分表,消费消息被路由到不同的库或表,还是会存在问题。
一般来说分库分表也不会有问题,为什么?因为,使用我们的方法,对于一条具体的消息,总是会落到确定的某个库表上,它的重复消息也会落地同样的库表上,所以分库分表不是问题。
2、为更新的数据设置前置条件,可以在消息中附带属性,比如当前账户的总金额,或者表中多加一个版本号字段,配合数据库行锁,类似乐观锁的概念,Java CAS,比较内存中的旧值是否和预先的旧值相等,如果是替换成新值。存在的问题和1类似。
3、记录并检查操作,在每个消息中维护一个全局唯一的ID,根据全局唯一ID进行判断消息是否已经被消费。存在的问题,全局唯一ID的实现有一定的复杂度,需要确保检查消费状态、更新数据、以及更新消费状态三个操作原子性,解决方式涉及到分布式锁和分布式事务,并且对高性能、高并发也有一定的影响。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值