消息为什么会重复消费?
上一篇我们聊了《消息队列如何保证消息可靠性?》
为了尽可能避免消息丢失,大多数消息队列都会采用 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 不会轻易认为一条消息已经消费完成,只有收到消费者的确认,才会更新消费进度。
如果确认失败:重新投递
如果业务已经执行:重复消费
因此,真正需要解决重复消费问题的,不是消息队列,而是业务系统的幂等设计。
上一篇:《消息队列如何保证消息可靠性?》
下一篇:《为什么还能保证顺序消费?》
网硕互联帮助中心



评论前必须登录!
注册