消息幂等与重复消费的问题

1、什么是消息幂等?

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

2、什么情况下需要消息幂等?

    业务开发中,经常会遇到重复提交的情况,无论是由于网络问题无法收到请求结果而重新发起请求,或是前端的操作抖动而造成重复提交情况。 在交易系统,支付系统这种重复提交造成的问题有尤其明显。

    比如:

    用户在APP上连续点击了多次提交订单,后台应该只产生一个订单。在向支付系统发起支付请求的时候,由于网络问题或系统BUG重发,支付系统应该只扣一次钱。 很显然,声明幂等的服务认为,外部调用者会存在多次调用的情况,为了防止外部多次调用对系统数据状态的发生多次改变,将服务设计成幂等。

3、幂等和防重的区别

人为的进行多次操作,导致不满足幂等要求的服务多次改变状态。而幂等更多使用的情况是第一次请求不知道结果(比如超时)或者失败的异常情况下,发起多次请求,目的是多次确认第一次请求成功,却不会因多次请求而出现多次的状态变化。

4、消息重复的场景

在互联网应用中,尤其在网络不稳定的情况下,消息队列RabbitMQ的消息有可能会出现重复。如果消息重复会影响您的业务处理,请对消息做幂等处理。

消息重复的场景如下:

1)发送时消息重复

     当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且Message ID也相同的消息。

2)投递时消息重复

     消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。为了保证消息至少被消费一次,消息队列RabbitMQ版的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且Message ID也相同的消息。

3)负载均衡时消息重复(包括但不限于网络抖动、Broker重启以及消费者应用重启)

     当消息队列RabbitMQ的Broker或客户端重启、扩容或缩容时,会触发Rebalance,此时消费者可能会收到重复消息。

5、为什么我们要保证幂等性,不保证幂等性,会不会有问题?

     回答这个问题的根源得从业务场景上进行分析。比如正常业务情况下,我们是不允许同个订单重复支付,这种业务场景我们就需要确保幂等性。再比如日志记录,这种业务场景,我们可能就不需要做幂等判断。

增加了服务提供者的逻辑和成本,是否有必要,需要根据具体场景具体分析,因此除了业务上的特殊要求外,尽量不提供幂等的接口。

6、消息队列的消费幂等性如何保证?

     没法保证。前面说了要保证幂等性,得基于业务场景进行考量。

     消息队列本身就不是用来做业务幂等性用的。如果要实现业务幂等性,靠消息队列是没法帮助我们完成的,还得根据自身业务场景,来实现幂等。

7、常用的业务幂等性保证方法

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

      比如将订单表中的订单编号设置为唯一索引,创建订单时,根据订单编号就可以保证幂等

2)利用redis的原子性

     每次操作都直接set到redis里面,然后将redis数据定时同步到数据库中

3)多版本(乐观锁)控制

      此方案多用于更新的场景下。其实现的大体思路是:给业务数据增加一个版本号属性,每次更新数据前,比较当前数据的版本号是否和消息中的版本一致。如果不一致则拒绝更新数据,更新数据的同时将版本号+1。

4)状态机机制

     此方案多用于更新且业务场景存在多种状态流转的场景。

5)token机制

     生产者发送每条数据的时候,增加一个全局唯一的id,这个id通常是业务的唯一标识,比如订单编号。在消费端消费时,则验证该id是否被消费过,如果还没消费过,则进行业务处理。处理结束后,在把该id存入redis,同时设置状态为已消费。如果已经消费过了,则不进行处理。

总结:

      消息队列没法帮助我们做到消费端的幂等性,消费端的幂等性得基于业务场景进行实现。

保证消息不能丢,至少保证被消费一次,不然消息都丢了,没数据还谈什么业务幂等。在实现消费端处理业务时,要确保消费端是采用手工确认应答机制(ACK),而不是自动应答机制。这样能够确保消费端一旦业务处理失败,生产者还能再次发送同个消息给消费端。

+