05丨MQ面试问题三连:如何解决消息不丢失、重复消费、积压?(二)

3,148 阅读4分钟

写在前面

大家好,我是爱刨根问底的小明不明,这一篇我们来讨论消息重复消费的问题,让一起来好好刨一刨这个问题的根吧~

为什么会有重复消息?

我们可以从业务和技术角度切入。

业务角度

  1. 前端重复提交表单
  2. 用户恶意进行刷单

技术角度

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 接口方法中,分别存在幂等行与不能保证幂等的方法,如下:

  1. √ 满足幂等
  2. x 不满足幂等
  3. - 可能满足也可能不满足幂等,根据实际业务逻辑有关

image.png

如何实现幂等?

实现幂等的最好方式是从业务逻辑设计上入手,将消费的业务逻辑设计成具备幂等性的操作。

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

举个栗子:改造“将某账户余额增加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。具体原子性的实现可以是分布式事务,也可以是分布式锁。

总结

image.png

参考文献