写在前面
大家好,我是爱刨根问底的小明不明,这一篇我们来讨论消息重复消费的问题,让一起来好好刨一刨这个问题的根吧~
为什么会有重复消息?
我们可以从业务和技术角度切入。
业务角度
- 前端重复提交表单
- 用户恶意进行刷单
技术角度
MQTT协议中对传递消息时的服务质量进行了分类,虽然由MQTT协议定义但是在所有消息传递场景都适用。
At most once:最多分发一次。也就是说不保证消息可靠性,允许丢消息。
At least once:至少分发一次。也就是说保证消息可靠性,允许重复消息。
Exactly once:只分发一次。这是最高级别的消息传递,消息丢失和重复都是不可接受的,使用这个服务质量等级会有额外的开销。
消息队列传递消息时的服务质量通常是At least once,因为保证消息的可靠性符合大部分业务的需求。
At least once->保证消息可靠性->同一条消息会被重复发送->重复消费问题
怎么解决消息被重复消费的问题?
因为消息队列的服务质量是At least once ,因此消息队列无法保证消息不重复,因此消费重复的问题得由Consumer端来解决。
一般采用幂等性解决重复消息问题。
什么是幂等?
幂等操作的特点:任意多次执行所产生的影响均与一次执行的影响相同。
举个栗子:
幂等操作:“将某账户余额设置为100元”,这个操作执行多次之后账户余额始终是100元,因此这个操作是幂等的。
不幂等操作:“将某账户余额增加100元”,每次一执行,余额都会增加100元,因此这个操作是不幂等的。
在Restful中哪些操作需要考虑幂等?
现在流行的 Restful 推荐的几种 HTTP 接口方法中,分别存在幂等行与不能保证幂等的方法,如下:
√
满足幂等x
不满足幂等-
可能满足也可能不满足幂等,根据实际业务逻辑有关
如何实现幂等?
实现幂等的最好方式是从业务逻辑设计上入手,将消费的业务逻辑设计成具备幂等性的操作。
利用数据库的唯一约束实现幂等
举个栗子:改造“将某账户余额增加100元”的业务逻辑。
1.增加限定,每个转账单每个账户只可以执行一次变更操作。具体实现:在数据库中建一张转账流水表,表包含字段转账单ID、账户ID、变更金额,对(转账单ID、账户ID)创建唯一键约束。
2.“将某账户余额增加100元”的业务逻辑变为:“在转账流水表中增加一条转账记录,然后再根据转账记录,异步操作更新用户余额”。
如果重复消费,就回触发唯一键约束,从而实现了操作的幂等性。
为更新的数据设置前置条件
核心思想:乐观锁
举个栗子:改造“将某账户余额增加100元”的业务逻辑。
为“将某账户余额增加100元”添加前置条件,变为:“如果某账户余额版本为2,则将账户X的余额增加100元”。
每次更新时,若数据中的版本号和消息中的版本号一直,则更新数据并且版本号+1,否则拒绝更新,从而实现了操作的幂等性。
记录并检查操作(也叫Token机制或GUID机制)
基本思路:在执行数据更新操作之前,先检查一下是否执行过这个更新操作。
举个栗子:改造“将某账户余额增加100元”的业务逻辑。
给“将某账户余额增加100元”生成全局唯一ID,存入Redis中,假设Redis中存在全局唯一ID则消息没有被消费,否则消息已经被消费。
消费时,第一步,先到Redis检查全局唯一ID是否存在,第二步,存在则消费,第三步,消费完成之后,到Redis删除全局唯一ID。
该方法需要保证消费时三步操作的原子性,才能实现幂等,否则会出现Bug。具体原子性的实现可以是分布式事务,也可以是分布式锁。
总结
参考文献
- 一口气说出四种幂等性解决方案,面试官露出了姨母笑~
- 极客时间——李玥老师的消息队列高手课