消息队列的消费语义和投递语义

本文详细探讨了消息队列的消费语义和投递语义,包括最多、至少和恰好投递一次的实现方式。通过Kafka举例,解释了如何配置参数以达到不同语义,并介绍了消息的幂等性和消费确认机制,以确保消息的正确处理。同时,文章也提及了消息最多、至少和恰好消费一次的场景及其实现策略。

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

引言

正文

1.所谓的消费语义,指的就是如下三种情况

我们先做如下约定

Producer代表生产者
Consumer代表消费者
Message Queue代表消息队列
投递语义
我们先从投递语义开始讲起,因为要先把这个概念讲明白了,才能讲消费语义。恰巧,kafka实现了这三种语义,我们以kafka来说明。

* 如何保证消息最多投递一次?

简单,就是我已经投出去了,收没收到不管了,会存在消息丢失。
我们在初始化Producer时可以通过配置request.required.acks不同的值,来实现不同的发送模式。
这里将request.required.acks设为0,意思就是Producer不等待Leader确认,只管发出即可;最可能丢失消息。如果丢了消息,就是投递0次。如果没丢,就是投递1次。符合最多投递一次的含义。

* 如何保证消息至少投递一次?

这里将request.required.acks设为-1。ProducerkafkaLeader(主)节点发送消息后,会等follower(从)节点同步完数据以后,再给Producer返回ACK确认消息。
但是这里是有几率出现重复消费的问题的。
例如,kafka保存消息后,发送ACK前宕机,Producer认为消息未发送成功并重试,造成数据重复!
那么,在这种情况下,就会出现大于1次的投递情况,符合至少投递一次的含义。

* 如何保证消息恰好投递一次?

kafka在0.11.0.0版本之后支持恰好投递一次的语义。
我们将enable.idempotence设置为ture,此时就会默认把request.required.acks设为-1,可以达到恰好投递一次的语义。

如何做到的?

为了实现Producer的幂等语义,Kafka引入了Producer ID(即PID)和Sequence Number。
kafka为每个Producer分配一个pid,作为该Producer的唯一标识。
Producer会为每一个<topic,partition>维护一个单调递增的seq。
类似的,Message Queue也会为每个<pid,topic,partition>记录下最新的seq。
当req_seq == message_seq+1时,Message Queue才会接受该消息。因为:

(1)消息的seq比`Message Queue`的seq大一以上,说明中间有数据还没写入,即乱序了。
(2)消息的seq比`Message Queue`的seq小,那么说明该消息已被保存。
消费语义

这里我们还是做一个定义如下所示

consumer.poll()表示消费者获取消息内容
processMsg(message)表示下游系统进行消费消息
consumer.commit()表示消费者往消息队列提交确认信息,消息队列接到确认消息,删除该消息。
注意了,我是以processMsg函数,即处理消息的过程,定义为消费消息。

2.其实类似还有一个投递语义

* 如何保证消息最多消费一次?

Producer:满足最多投递一次的语义即可,即只管发消息,不需要等待消息队列返回确认消息。
Message Queue:接到消息后往内存中一放就行,不用持久化存储。
Consumer:拉取到消息以后,直接给消息队列返回确认消息即可。至于后续消费消息成功与否,无所谓的。即按照以下顺序执行

consumer.poll();
consumer.commit();
processMsg(message);
* 如何保证消息至少消费一次?

Producer:满足至少投递一次语义即可,即发送消息后,需要等待消息队列返回确认消息。如果超时没收到确认消息,则重发。
Message Queue:接到消息后,进行持久化存储,而后返回生产者确认消息。
Consumer:拉取到消息后,进行消费,消费成功后,再返回确认消息。即按照如下顺序执行

consumer.poll();
processMsg(message);
consumer.commit();

由于这里Producer满足的是至少投递一次语义,因此消息队列中是有重复消息的。所以我们的Consumer会出现重复消费的情形!

* 如何保证消息恰好消费一次?

在保证至少消费一次的基础上,processMsg满足幂等性操作即可。

### 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、付费专栏及课程。

余额充值