2023-08-12
原文作者:Ressmix 原文地址:https://www.tpvlog.com/article/119

本章,我们来讲下使用消息队列时另外一个经常需要关注的问题——消费消息时的幂等处理。

所谓 幂等 ,就是当出现消费者对某条消息重复消费的情况时,重复消费的结果与消费一次的结果是相同的,并且多次消费并未对业务系统产生任何负面影响,那么这整个过程就实现可消息幂等。

例如,在支付场景下,消费者消费扣款消息,对一笔订单执行扣款操作,扣款金额为 100 元。如果因网络不稳定等原因导致扣款消息重复投递,消费者重复消费了该扣款消息,但最终的业务结果是只扣款一次,扣费 100 元,且用户的扣款记录中对应的订单只有一条扣款流水,不会多次扣除费用。那么这次扣款操作是符合要求的,整个消费过程实现了消费幂等。

本章针对消息队列讲解消息消费的幂等性,本质其实是讲消费时接口设计的幂等性。

一、重复消费

首先,我们来看下在使用消息队列过程中,什么时候会出现“重复消费”的问题。

1.1 示例

我们以Kafka为例,来看下消费者端可能出现的重复消费问题。

在Kafka中,每个消息都有唯一的 offset顺序号 ,代表了这个消息的序号。当生产者往消息队列里扔消息时,Kafka会按数据进入的顺序给它们分配顺序号。当消费者在消息消息时,会按照这个顺序号依次去消费。

消费者每消费完一条消息,就会提交消息的offset给Kafka,告诉它“我已经消费到这条消息了”,Kafka会通过Zookeeper记录消费者当前已经消费到了哪个offset的消息。

如下图,比如消费者已经消费完offset=153的消息:

202308122218538311.png

如果消费者消费完offfset=153的消息,并本地处理完成后,还没来得及提交offset就自己挂掉了,那么当消费者进程重启恢复后,此时再次去找Kafka消费,Kafka会把offset=153那条消息再次给消费者,消费者就重复消费153这条消息两次。

事实上,消费者消费完一条数据后,不会立即提交offset,而是定时定期提交一次。

二、幂等设计

既然有重复消费问题的存在,那么消费者就必须自己做好消费接口的幂等性设计,接口幂等设计的方式有很多,主要有两种:

  1. 数据库方式
    基于数据库唯一索引,先根据索引查一下,如果记录都有了,就别插入了,update一下。
  2. 缓存方式
    比如基于Redis实现一套接口的防重框架 。生产者发送每条数据的时候,里面加一个全局唯一的id,消费时先根据这个id去Redis里查一下之前是否消费过?如果没有消费过,就正常处理,然后将这个id写redis;如果消费过了,就别处理了。

2.1 示例

下面我们来看一个示例,更好的理解下接口幂等性。假如有一个支付服务接口,该服务对等的部署在3台机器上。前端上操作的时候,不小心针对同笔订单发起了两次支付请求,然后这俩请求分散在了不同的机器上。这个时候,如果不对支付接口做幂等性设计,就可能导致重复扣款。

202308122218551662.png

所以,保证幂等性主要是三点:

  1. 对于每个请求必须有一个唯一的标识,比如订单支付请求,肯定得包含订单id,一个订单id最多支付一次;
  2. 每次处理完请求之后,必须有一个状态标识这个请求处理过了,最常见的方案就是在mysql中记录这个标识的状态,比如支付成功后,记录订单的状态为“已支付”;
  3. 每次接收请求后,首先根据标识判断之前是否已经处理过了,比如有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId已经存在了,唯一键约束生效,会报错插入不进去,那就不会再扣款了。

上面支付的场景只是给大家举个例子,实际运作过程中,必须要结合自己的业务来。 接口的幂等性设计,没有一个通用的方法,要结合业务来看,具体问题具体分析。

三、总结

本章,我们从消息队列重复消费的问题开始,引出了接口的幂等性设计,然后给出了一些通用的设计思路。但一般来讲,中大型互联网公司里也很少会去做一个统一的防重幂等框架。

接口的幂等设计,一般都是由各个服务针对自己核心的接口,判断是否要保证幂等性,如果需要的话则根据自己的业务逻辑来实现,而且一般仅仅是少数核心接口需要做幂等性设计保障。

同时,我们要理解的是 幂等设计的本质是通过唯一标识,标记同一操作的方式,来消除多次执行的副作用。 幂等性设计并没有什么万金油,还是要结合实际具体问题具体分析。

阅读全文