消息队列——如何保持消息的幂等性(不被重复消费)

本文探讨了消息队列中消息重复消费的问题及其解决方案,重点介绍了如何通过不同方法保证消息消费的幂等性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题

如何保证消息不被重复消费?或者说,如何保证消息消费的幂等性?

分析

其实这是很常见的一个问题,这俩问题基本可以连起来问。既然是消费消息,那肯定要考虑会不会重复消费?能不能避免重复消费?或者重复消费了也别造成系统异常可以吗?这个是 MQ 领域的基本问题,其实本质上还是问你使用消息队列如何保证幂等性,这个是你架构里要考虑的一个问题。

面试题剖析

回答这个问题,首先你别听到重复消息这个事儿,就一无所知吧,你先大概说一说可能会有哪些重复消费的问题

首先,比如 RabbitMQ、RocketMQ、Kafka,都有可能会出现消息重复消费的问题,正常。因为这问题通常不是 MQ 自己保证的,是由我们开发来保证的。挑一个 Kafka 来举个例子,说说怎么重复消费吧。

Kafka 实际上有个 offset 的概念,就是每个消息写进去,都有一个 offset,代表消息的序号,然后 consumer 消费了数据之后,每隔一段时间(定时定期),会把自己消费过的消息的 offset 提交一下,表示“我已经消费过了,下次我要是重启啥的,你就让我继续从上次消费到的 offset 来继续消费吧”。

但是凡事总有意外,比如我们之前生产经常遇到的,就是你有时候重启系统,看你怎么重启了,如果碰到点着急的,直接 kill 进程了,再重启。这会导致 consumer 有些消息处理了,但是没来得及提交 offset,尴尬了。重启之后,少数消息会再次消费一次。

举个栗子。

有这么个场景。数据 1/2/3 依次进入 kafka,kafka 会给这三条数据每条分配一个 offset,代表这条数据的序号,我们就假设分配的 offset 依次是 152/153/154。消费者从 kafka 去消费的时候,也是按照这个顺序去消费。假如当消费者消费了 offset=153 的这条数据,刚准备去提交 offset 到 zookeeper,此时消费者进程被重启了。那么此时消费过的数据 1/2 的 offset 并没有提交,kafka 也就不知道你已经消费了 offset=153 这条数据。那么重启之后,消费者会找 kafka 说,嘿,哥儿们,你给我接着把上次我消费到的那个地方后面的数据继续给我传递过来。由于之前的 offset 没有提交成功,那么数据 1/2 会再次传过来,如果此时消费者没有去重的话,那么就会导致重复消费。

mq-10

如果消费者干的事儿是拿一条数据就往数据库里写一条,会导致说,你可能就把数据 1/2 在数据库里插入了 2 次,那么数据就错啦。

其实重复消费不可怕,可怕的是你没考虑到重复消费之后,怎么保证幂等性

举个例子吧。假设你有个系统,消费一条消息就往数据库里插入一条数据,要是你一个消息重复两次,你不就插入了两条,这数据不就错了?但是你要是消费到第二次的时候,自己判断一下是否已经消费过了,若是就直接扔了,这样不就保留了一条数据,从而保证了数据的正确性。

一条数据重复出现两次,数据库里就只有一条数据,这就保证了系统的幂等性。

幂等性,通俗点说,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错

所以第二个问题来了,怎么保证消息队列消费的幂等性?

其实还是得结合业务来思考,我这里给几个思路:

  • 比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update 一下好吧。
  • 比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。
  • 比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。
  • 比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。

mq-11

当然,如何保证 MQ 的消费是幂等性的,需要结合具体的业务来看。

转载来源:https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/how-to-ensure-that-messages-are-not-repeatedly-consumed.md

 

### Kafka 消息队列重复消费解决方案 #### 问题分析 Kafka 是一种分布式流处理平台,广泛应用于高吞吐量的消息传递场景。然而,在实际生产环境中,可能会遇到消费者端重复消费消息的情况。这种现象通常由以下几个原因引起: - **消费者崩溃或重启**:当消费者实例发生异常退出并重新启动时,可能未及时提交偏移量(offset),从而导致再次拉取消息。 - **手动提交偏移量失败**:如果采用手动方式管理 offset 提交逻辑,则存在因程序错误或其他因素未能成功完成提交的风险[^3]。 #### 解决方案概述 针对上述提到的原因以及具体业务需求,可以从多个角度出发来设计一套有效的防重机制: 1. **幂等性设计** 幂等是指对于同一操作多次请求能够得到相同的结果而影响最终状态。通过实现服务端接口或者数据库层面的幂等写入功能,即使同一条记录被多次投递给下游系统也会造成数据一致等问题。例如利用唯一键值(如订单号)作为判断依据,在存储之前先查询是否存在该笔交易;若已存在则忽略此次更新动作[^2]。 2. **引入去重表/缓存结构** 可以考虑构建一张专门用于记录已经处理过的消息ID列表(即所谓的“去重表”),每当接收到新消息时便检查其是否存在于此集合当中。如果是第一次见到这条信息,则正常执行后续流程并将对应标识加入黑名单直至过期时间到达为止;反之直接丢弃无需进一步操作。Redis 等内存型NoSQL 数据库因其高性能特性非常适合充当此类角色。 3. **调整自动提交策略** 默认情况下,Kafka 客户端会在一定间隔周期之后才向集群汇报当前进度位置。为了降低潜在风险,可以关闭默认行为改为显式调用`commitSync()` 方法确保每次仅当确认无误后再推进offset 。此外还可以适当延长最大会话超时期限(max.poll.interval.ms),给予更多喘息空间给复杂耗时的任务完成整个生命周期[^4]。 4. **启用精确一次性语义 (Exactly Once Semantics ,EOS)** 自 Apache Kafka 版本0.11起支持 EOS 功能,允许开发者轻松达成跨分区事务协调目标——既保证生产者发送的数据会丢失亦杜绝任何可能性下的多余副本生成。过需要注意的是开启此项配置需额外付出性能代价,故应权衡利弊慎重决定何时何处适用[^1]。 ```java Properties props = new Properties(); props.put("enable.idempotence", true); // 开启幂等保障 props.put("transactional.id", "my-txn-id"); // 设置事务 ID // 创建 Producer 实例... producer.initTransactions(); // 初始化事务环境 try { producer.beginTransaction(); // 执行一系列 send 调用 ... producer.commitTransaction(); // 成功结束事务 } catch (ProducerFencedException | OutOfOrderSequenceException e) { producer.close(); // 可恢复错误立即终止连接 } finally { if (!isCommitted){ producer.abortTransaction(); // 失败回滚更改 } } ``` 以上列举了几种常见的应对措施供参考选用,当然实际情况往往更加错综复杂,还需要结合项目具体情况灵活运用这些技巧组合出击才能达到理想效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值