云计算百科
云计算领域专业知识百科平台

第四篇:消息为什么会重复消费?

消息为什么会重复消费?

上一篇我们聊了《消息队列如何保证消息可靠性?》

为了尽可能避免消息丢失,大多数消息队列都会采用 At Least Once(至少投递一次) 的策略。

也就是说:Broker 宁愿把消息再投递一次,也不会轻易认为消息已经消费成功。

这也带来了另一个问题。同一条消息,可能会被消费两次,甚至更多次。

一条消息,为什么会消费两次?

来看一个最简单的消费者。

@KafkaListener(topics = "order-topic")
public void consume(OrderMessage message) {

orderService.create(message);

}

生产者只发送了一次消息:kafkaTemplate.send(“order-topic”, orderMessage);

按理说:

发送一次

消费一次

结果线上却出现了:

发送一次

消费两次

数据库甚至出现了两条相同的数据。

id order_no
1 10001
2 10001

Producer 没有重复发送。Broker 也没有复制消息。

那问题出在哪里?一条消息,到底经历了什么?上一篇我们已经画过消息链路。

Producer

Broker

Consumer

业务处理

消费确认

这里真正关键的是最后一步。

消费确认。只有 Consumer 告诉 Broker:这条消息已经处理完成,Broker 才会把它标记为已消费,如果确认没有成功,Broker 会认为:这条消息还没有处理。

于是再次投递,最容易出现重复消费的场景。假设订单消费者收到一条消息,

订单创建

更新数据库

提交消费确认

正常情况下,没有问题。但是如果流程变成这样:

订单创建

更新数据库(成功)

服务器突然宕机

ACK 没有提交

Broker 并不知道数据库已经更新。

它只知道:消费者没有确认,于是消费者恢复以后:

再次投递

再次执行

数据库就变成:订单10001

创建两次,重复消费就是这样产生的。

为什么不能先确认,再处理业务?

有人可能会想到,既然确认这么重要。

是不是可以:

收到消息

ACK

处理业务

这样 Broker 就不会重复发送了。

看起来不错,实际上问题更严重。

假设:

收到消息

ACK 成功

程序崩溃

Broker 已经认为消费完成,但是业务根本没有执行,消息彻底丢失。

所以,ACK 一定要放在业务处理之后,这也是绝大多数 MQ 的处理方式。为什么 MQ 宁愿重复,也不愿丢失?这里就回到了上一篇讲的可靠性。

假设只有两个选择。

方案一:可能重复消费,但是不会丢消息

方案二:不会重复消费,但是可能丢消息

几乎所有 MQ 都会选择第一种。

原因很简单,消息重复业务还能补救。

消息丢失,很多时候根本无法恢复。

例如:

支付成功

消息丢失

订单一直未支付

这种问题比重复扣一次积分严重得多。因此,重复消费,是可靠性设计主动接受的一种代价。

Kafka 如何判断消息是否消费成功?

Kafka 并不会记录,这条消息是否已经消费,它记录的是 Consumer 当前消费到了哪个 Offset

例如:

Partition0

offset 0

offset 1

offset 2

offset 3

消费者处理完成:

offset 0

提交 Offset=1

表示:0 已经消费,下一次从 1 开始

如果:

业务成功

Offset 提交失败

下一次启动 Kafka 仍然会从:offset 0 重新消费。

所以:重复消费并不是 Kafka 又保存了一份消息。而是:消费进度没有成功更新。

RocketMQ 又是怎么做的?消费者处理成功以后需要返回:

ConsumeConcurrentlyStatus.CONSUME_SUCCESS

如果返回失败:RECONSUME_LATER ,Broker 会重新投递。

因此:Kafka 提交的是 Offset,RocketMQ 返回的是消费状态。

虽然实现方式不同,但设计思想完全一致。只有确认成功,消息才真正完成消费。

如何避免重复消费?消息队列无法保证消息只消费一次。真正解决重复消费的,是业务系统。

最常见的方法有三种。

1、唯一业务 ID

例如订单号,数据库建立唯一索引。

CREATE UNIQUE INDEX uk_order_no ON t_order(order_no);

即使消息重复,第二次插入也会失败。

2、业务状态判断

例如:

if(order.isPaid()){
return;
}

只有未支付才继续执行。

3、幂等表或 Redis 去重

先记录消息 ID,处理过 msgId=1001,再次收到直接返回。很多支付系统都会采用这种方式。

为什么很难做到 Exactly Once?

很多人听说过 Exactly Once(恰好一次),听起来非常完美。实际上,它的实现成本远高于 At Least Once。因为它要求:

消息发送

消息存储

业务执行

消费确认

整个过程都必须保证:

成功一次,也只能成功一次,这不仅需要 MQ 支持,还需要数据库、业务代码、事务机制共同配合。所以在实际项目中:大多数系统采用的仍然是:

At Least Once
+
业务幂等

既保证消息可靠,又兼顾系统性能。

总结:消息重复消费,并不是消息队列的缺陷。恰恰相反,它是消息可靠性设计带来的结果,为了避免消息丢失,Broker 不会轻易认为一条消息已经消费完成,只有收到消费者的确认,才会更新消费进度。

如果确认失败:重新投递

如果业务已经执行:重复消费

因此,真正需要解决重复消费问题的,不是消息队列,而是业务系统的幂等设计。

上一篇:《消息队列如何保证消息可靠性?》

下一篇:《为什么还能保证顺序消费?》

赞(0)
未经允许不得转载:网硕互联帮助中心 » 第四篇:消息为什么会重复消费?
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!