1、什么是Kafka?它的核心特点是什么
-
分布式流处理平台,高吞吐、持久化、水平扩展、实时处理。
- 流处理 (Stream Processing)指对连续、无界的数据流 进行实时处理,与传统的批处理 (有界、数据量固定、离线处理)形成对比。
- 分布式:Kafka 集群由多个节点(Broker)组成,数据在节点间自动分区和复制
-
核心特点:消息持久化(存储到磁盘)、水平扩展/可伸缩性(Broker/Partition)、高吞吐(批量发送+压缩)、实时性(消费者拉取)。
持久化
-
持久化:数据以日志形式持久化存储,通过时间、大小、日志段保留机制控制数据的持久化时长和存储空间。
- 基于时间 :
log.retention.hours
(默认 168 小时,即 7 天)。 - 基于大小 :
log.retention.bytes
(单个 Partition 的最大存储量,默认未启用)。 - 基于日志段 :当分段文件达到
segment.bytes
(默认 1GB)或segment.ms
(默认 7 天)时,触发新分段创建。
- 基于时间 :
-
示例场景
-
假设配置如下:
segment.bytes=1GB
segment.ms=7天
log.retention.hours=168
(7天)
流程 :
- 分段 A 写入到 1GB 或 7 天后,触发新分段 B 的创建。
- 分段 A 被关闭,进入保留倒计时。
- 分段 A 的最后一条消息时间超过 7 天后,Kafka 的后台线程(Log Cleaner)会删除该分段文件。
-
水平扩展
场景 | 扩展方式 | 效果 |
---|---|---|
写入吞吐量不足 | 增加 Broker + 扩展 Topic Partition 数(修改 Topic 配置) | 提升生产者的并发写入能力。 |
消费积压 | 增加 Consumer 实例(需足够 Partition,每个 Consumer Group 内的 Consumer 实例数 ≤ Partition 数) | 加速消费处理,减少延迟,加强消费能力。 |
存储容量不足 | 增加 Broker + 分布 Partition 副本 | 扩展总存储空间,避免磁盘满载。 |
跨地域容灾 | 增加异地 Broker + 副本跨地域分布 | 提升可用性,支持多活架构。 |
高吞吐
Kafka 的高吞吐量 是其核心设计目标之一,能够实现每秒处理百万级消息 ,主要得益于以下关键设计:
1. 顺序写入与磁盘 I/O 优化
-
顺序写入磁盘 :
Kafka 将消息以追加日志(Append-Only Log) 的形式顺序写入磁盘,避免了随机写入的寻址开销。
- 磁盘顺序写性能接近内存速度(现代磁盘可达 100+ MB/s),远高于随机写。
-
分段存储(Segmented Log) :
每个 Partition 的日志被拆分为多个分段文件(Segment),支持快速删除旧数据(直接删除整个文件),减少碎片化。
2. 批量处理(Batching)
- 生产端批量发送 :
生产者将消息批量压缩 后发送(通过batch.size
和linger.ms
参数控制),减少网络请求次数。 - 消费端批量拉取 :
消费者以批量(如 1MB 或 500 条消息)拉取数据,降低 I/O 开销。
3. 零拷贝技术(Zero-Copy)
-
绕过用户态内存拷贝 :
Kafka 使用操作系统的
sendfile
系统调用,直接将磁盘文件数据通过 DMA(Direct Memory Access)传输到网络缓冲区,避免数据在内核态与用户态之间的多次拷贝。- 传统方式:磁盘 → 内核缓冲区 → 用户缓冲区 → 内核 Socket 缓冲区 → 网络。
- 零拷贝:磁盘 → 内核缓冲区 → 网络(省去 2 次拷贝和上下文切换)。
4. 分区与并行处理(重点)
- 水平扩展能力 :
- Kafka支持将一个主题的数据分散至多个分区,不同分区位于多个broker节点上,实现了集群负载均衡,从而提高了写入和读取的性能,避免单点瓶颈。
- 生产者可并行写入多个 Partition,消费者组可并行消费多个 Partition,吞吐量随 Partition 数量线性增长。
5. 异步刷盘
Kafka支持异步刷盘,即将消息写入日志后,不会立即将数据从内存刷入磁盘,而是会缓存一段时间再批量写入磁盘,减少了磁盘I/O的次数,提高了写入性能。
2、Kafka的业务场景
消息队列(解耦系统)
日志聚合(ELK)
事件溯源
流处理(Kafka Streams)
运营指标监控。
1. 消息队列(Message Queue)
- 场景 :作为系统间异步通信的桥梁,解耦生产者和消费者。
- 案例 :
- 电商平台中,订单服务将订单消息写入 Kafka,库存、支付、物流等服务异步消费消息,避免同步调用的性能瓶颈。
- 缓冲突发流量(如秒杀活动),防止下游系统过载
- 作用:解耦系统、异步处理、流量削峰和缓冲
2. 日志聚合与分析
- 场景 :集中收集、存储和分析分布式系统的日志。
- 案例 :
- 收集服务器、应用、移动端日志,通过 Kafka 传输到 ELK(Elasticsearch、Logstash、Kibana)或 Hadoop 生态系统进行分析。
- 实时监控日志异常(如错误率激增)。
3、为什么选择 Kafka?
- 高吞吐 :支持每秒百万级消息。
- 持久化 :消息持久化到磁盘,避免数据丢失。
- 水平扩展 :通过分区(Partition)和集群实现无缝扩容。
- 实时性 :毫秒级延迟,支持流式处理。
- 生态整合 :与 Hadoop、Spark、Flink 等大数据工具无缝对接。
4、Kafka的架构组件有哪些?

- Broker :Kafka服务器节点,负责存储消息。
- Topic :消息的逻辑分类,类似数据库的表。
- Partition :Topic的分片,支持并行读写和水平扩展。
- Producer :消息生产者,负责发送消息。
- Consumer :消息消费者,订阅Topic处理消息。
- Consumer Group :消费者组,每个消费者组可以消费一个或多个主题的消息,实现负载均衡和容错。
- ZooKeeper :协调集群管理(新版本逐步移除依赖)。
Kafka主题与分区
参考

当我们发布者向某个主题发送消息时,其就会被“分发”到某一个分区里
存储核心规则
- 每条消息只能存在于一个分区中:
生产者发送消息时,根据分区策略(轮询、粘性、Key哈希等)将消息分配到 Topic 的某一个分区。
例如:一个 Topic 有 3 个分区(P0, P1, P2),消息A分配到 P1,消息B分配到 P2,依此类推。 - 分区是消息存储的最小单位:
每个分区是一个有序的、不可变的消息序列。分区内的消息按顺序存储(偏移量递增),但不同分区的消息顺序不保证。

分区方式
指定分区:如果生产者自己决定要发送到哪个分区,可以在发送消息时指定消息要发送到的分区编号。此时,如果指定的分区编号存在,则消息会被发送到该分区;如果指定的分区编号不存在,则会抛出异常。
自动分区:如果使用默认的分区分配算法,Kafka提供了多种分配算法,例如轮询(Round-Robin)、随机(Random)、哈希(Hash)等。
场景 | 策略 | 目标 |
---|---|---|
消息有 Key | Murmur2 哈希分区 | 保证相同 Key 的消息顺序性 |
消息无 Key(Kafka 2.4+) | 粘性分区 | 提升吞吐量,兼顾负载均衡 |
消息无 Key(旧版本) | 轮询分区 | 均匀分配消息到所有分区 |
public ListenableFuture<SendResult<K, V>> send(String topic, Integer partition, K key, @Nullable V data) {
ProducerRecord<K, V> producerRecord = new ProducerRecord(topic, partition, key, data);
return this.doSend(producerRecord);
}
1、轮询分区
核心机制
- 无 Key 的轮询:生产者严格按顺序将消息依次发送到所有分区。例如,如果有 3 个分区(P0、P1、P2),消息会按
P0 → P1 → P2 → P0 → P1 → P2...
的顺序分配。 - 有 Key 的覆盖:如果消息指定 Key,则通过哈希算法(如
murmur2
)计算 Key 的哈希值,再取模分区数,确保相同 Key 的消息分配到同一分区。此时轮询策略失效。
示例场景
假设 Kafka 主题有 3 个分区(P0, P1, P2),生产者发送 6 条消息,其中:
- 3 条无 Key 的消息(Message1, Message2, Message3)。
- 3 条有 Key 的消息(Key=“UserA”, Key=“UserB”, Key=“UserA”)。
分配结果:
消息 | 分区分配逻辑 | 实际分区 |
---|---|---|
Message1 | 无 Key → 轮询 P0 | P0 |
Message2 | 无 Key → 轮询 P1 | P1 |
Message3 | 无 Key → 轮询 P2 | P2 |
Key=“UserA” | 哈希(UserA) % 3 → 假设为 P1 | P1 |
Key=“UserB” | 哈希(UserB) % 3 → 假设为 P2 | P2 |
Key=“UserA” | 哈希(UserA) % 3 → 仍为 P1 | P1 |
特点:
- 无 Key 的消息严格轮询,负载均衡。
- 有 Key 的消息固定到同一分区,但不同 Key 可能分布不均(例如某些分区可能负载更高)。
2. 粘性分区(Sticky Partitioning)
核心机制
- 批次优化:生产者会尝试将同一批次的消息“粘”到同一个分区,直到满足批次条件(如
batch.size
或linger.ms
),再切换到下一个分区。 - 动态均衡:长期来看,消息仍然会均匀分布到所有分区,但短期内可能集中在某一分区。
- Key 的优先级:如果消息有 Key,依然通过哈希分配到固定分区(与轮询相同)。
示例场景
假设相同的主题(3 个分区),生产者发送 6 条无 Key 的消息,批次大小为 2 条消息:
-
生产者首先将消息1和消息2粘到 P0(批次1)。
-
批次1满足条件后,将消息3和消息4粘到 P1(批次2)。
-
批次2满足条件后,将消息5和消息6粘到 P2(批次3)。
-
分配结果:
消息 批次 分区分配逻辑 实际分区 Message1 批次1 粘性选择 P0(例如随机选择起始分区) P0 Message2 批次1 同一批次 → 继续 P0 P0 Message3 批次2 切换到下一个分区 → P1 P1 Message4 批次2 同一批次 → 继续 P1 P1 Message5 批次3 切换到下一个分区 → P2 P2 Message6 批次3 同一批次 → 继续 P2 P2
特点:
- 同一批次的消息集中在同一分区,减少网络请求次数。
- 长期来看,所有分区被均匀使用(批次1→P0,批次2→P1,批次3→P2)。
详细对比分析
场景1:无 Key 的高吞吐场景
- 轮询分区:
- 每条消息独立分配分区,导致每个批次可能发送到不同 Broker。
- 频繁的跨 Broker 网络请求,吞吐量较低。
- 粘性分区:
- 同一批次的消息集中发送到一个 Broker,减少网络开销。
- 吞吐量显著提升(例如合并多个小请求为一个大请求)。
场景2:有 Key 的消息顺序性
- 两种策略的相同点:
- 如果消息指定 Key,无论轮询还是粘性策略,都会通过哈希分配到固定分区。
- 例如订单消息(Key=订单ID)必须分配到同一分区,保证顺序性。
如何选择分区策略?
- 需要严格顺序性:
- 必须使用 Key(如订单ID),此时分区策略由 Key 的哈希决定。
- 高吞吐无 Key 场景:
- 优先使用粘性分区(默认策略),优化批次效率。
- 严格负载均衡:
- 如果接受较低的吞吐量,可以选择轮询分区。
分区内数据的存储
1、消息的存储
- 每个分区被组织为一组日志段(Log Segment),其中每个日志段都包含了一个连续的消息序列
- 当一个日志段被写满后,它将被关闭并分配一个更高的编号,新的消息将被追加到一个新的日志段中。
- 而日志段的核心又由两个部分组成:索引文件(index file)和数据文件(data file)

数据文件: 也叫日志文件,数据文件是消息分区的核心部分,它是以追加方式写入的文件。当有新的消息写入分区时,Kafka会根据协议、消息头、消息体等信息将消息封装成字节流,然后追加写入数据文件。
索引文件:索引其实就像字典的目录,是帮助大家快速找到某条消息的工具(磁盘中)
2、消息的读取
消费偏移量的存储
每个消费者负责需要消费分配给它的分区上的消息,并记录自己在每个分区上消费的最新偏移量。Kafka将给定消费者组的所有偏移存储在一个叫做组协调器(group coordinator)的组件。
Kafka的可靠性分析
可靠性的两个角度
1、消息不会意外丢失
2、消息不会重复传递
分区与副本
- Kafka 的每个 Topic 分为多个 分区(Partition),每个分区可以有多个副本(Replica)。
- 副本数(Replication Factor):例如设置为 3,表示每个分区有 3 个副本(1 个 Leader + 2 个 Follower)。
特性 | Leader | Follower |
---|---|---|
读写权限 | 处理所有读写请求 | 仅同步数据,不处理读写请求 |
数据权威性 | 数据唯一权威副本 | 数据副本,依赖 Leader 同步 |
故障恢复 | 宕机后由 Follower 接替 | 可升级为 Leader(需在 ISR 中) |
网络负载 | 高(直接服务生产者和消费者) | 低(仅与 Leader 通信) |
Leader 的职责
- 处理生产者请求:
- 接收生产者发送的消息,写入本地日志(Log)。
- 同步数据给所有 Follower(通过 ISR 机制)。
- 处理消费者请求:
- 直接响应消费者的数据读取请求。
- 维护数据一致性:
- 确保所有 Follower 副本的数据同步进度(通过
High Watermark
机制)。 - 简单讲就是Follower 定期向 Leader 发送心跳,若 Follower 的数据延迟超过阈值(由
replica.lag.time.max.ms
控制,默认 30 秒),会被移出 ISR。
- 确保所有 Follower 副本的数据同步进度(通过
Follower 的职责
- 从 Leader 拉取数据:
- 定期向 Leader 发送
FETCH
请求,拉取新消息并写入本地日志。
- 定期向 Leader 发送
- 参与 Leader 选举:
- 当 Leader 宕机时,Follower 可能被选举为新的 Leader(需属于
ISR
列表)。
- 当 Leader 宕机时,Follower 可能被选举为新的 Leader(需属于
- 提供数据冗余:
- 存储与 Leader 相同的数据,防止数据丢失。
ISR(In-Sync Replicas)

- 定义:
同步副本集合(In-Sync Replicas) 是一组与 Leader 数据保持同步的副本(包括 Leader 自身),同时也是机制。 - 维护规则:
- Follower 定期向 Leader 发送心跳。
- 若 Follower 的数据延迟超过阈值(由
replica.lag.time.max.ms
控制,默认 30 秒),会被移出 ISR。
- 作用:
- 确保故障时,新 Leader 的数据是最新的。
- 生产者发送消息时,可配置
acks=all
,要求所有 ISR 副本确认写入。
数据同步机制
生产者写入流程
- 生产者发送消息到 Leader。
- Leader 将消息写入本地日志。
- Leader 等待 ISR 中的所有 Follower 确认已同步消息(取决于
acks
配置)。 - Leader 向生产者返回确认。
Follower 同步流程
- Follower 定期向 Leader 发送
FETCH
请求。 - Leader 返回新消息。
- Follower 写入本地日志后,更新同步状态。
High Watermark 机制
确保所有 Follower 副本的数据同步进度
High Watermark(高水位线) 是分布式消息系统(如 Apache Kafka)中用于确保副本数据一致性的核心机制,它定义了消息的==提交边界==,表示所有副本已成功复制的消息位置。以下是其核心要点:
High Watermark 的作用
- 定义提交消息:所有位置(偏移量)低于 High Watermark 的消息被认为是**已提交(committed)**的,即这些消息已被所有同步副本(ISR,In-Sync Replicas)成功复制,即使 Leader 副本故障,这些消息也不会丢失。
- 控制可见性:消费者只能读取到 High Watermark 之前的消息,确保不会消费到未提交(可能丢失)的数据。
- 协调副本同步:Leader 副本通过 High Watermark 追踪 Follower 副本的同步进度,避免数据不一致。
High Watermark 的更新逻辑
- Leader 副本维护:Leader 副本负责计算和更新 High Watermark。
- 依赖 Follower 副本反馈:
- Follower 副本定期向 Leader 发送心跳和拉取请求,并汇报自己的最新已复制偏移量(Log End Offset, LEO)。
- Leader 根据所有 ISR 副本的 LEO 最小值更新 High Watermark(例如:ISR 中有 3 个副本,LEO 分别为 10、9、12,则 High Watermark = 9)。
- 推进条件:只有当消息被所有 ISR 副本复制后,High Watermark 才会向前推进。
High Watermark 的保障机制
- 防止数据丢失:只有超过 High Watermark 的消息才会对消费者可见,确保故障恢复时不丢失已提交数据。
- 副本滞后处理:
- 若 Follower 副本长时间未同步(落后于 Leader),会被移出 ISR 列表,High Watermark 的计算将忽略该副本。
- 当副本重新追上进度后,重新加入 ISR 列表。
实际应用场景
- 生产者确认机制:通过
acks=all
参数,生产者会等待消息被所有 ISR 副本确认(即 High Watermark 推进后才返回成功)。 - 消费者读取边界:消费者只能读取到 High Watermark 之前的消息,避免读取未提交数据。
- 故障恢复:新 Leader 选举时,会以 High Watermark 为基准截断不一致的日志,确保数据一致性。
ACKS设置
如果说备份机制是保障消息不会在Kafka服务器丢失,那么消息丢失的另一个重要原因就是消息在发送中丢失。
这种场景下,可以通过设置ACK的值,来决定同步的情况:
acks=0
- 行为 :生产者不等待Broker的任何确认,消息发送后立即视为成功。
- 特点 :延迟最低,但可能丢失数据(如消息未到达Broker或写入失败)。
- 适用场景 :对数据可靠性要求低(如日志采集)。
acks=1
(默认值)- 行为 :仅等待Leader副本确认写入,不等待其他副本同步。
- 特点 :平衡性能与可靠性,但若Leader在同步前宕机,可能导致数据丢失。
- 适用场景 :普通业务场景。
acks=all
(或-1
,能确保消息不丢失)- 行为 :等待所有ISR(In-Sync Replicas,同步副本)确认写入。
- 特点 :最高可靠性,但延迟最高(需等待多个副本同步)。
- 适用场景 :金融、交易等强一致性场景。
重试机制
发消息不可能万无一失,当Kafka在发送或接收消息时发生错误时,可以通过重试
来解决这些问题。
生产者重试
生产者在发送消息时,如果遇到可恢复的错误(如网络抖动、Broker临时不可用),会自动重试发送消息。
1. 核心配置
retries
- 作用 :设置生产者重试次数(默认为
0
,即不重试)。 - 示例 :
retries=3
表示最多重试3次。 - 注意 :需结合
retry.backoff.ms
(重试间隔,默认100ms)使用。
- 作用 :设置生产者重试次数(默认为
acks
- 若
acks=all
或acks=1
,生产者会在未收到Broker确认时触发重试。
- 若
2. 重试场景
- Broker无响应或返回可重试错误(如
NotEnoughReplicasException
)。 - 网络中断或超时(
SocketTimeoutException
)。 - 消息大小超过Broker限制(
RecordTooLargeException
,需调整配置)。
3. 风险与解决方案
- 重复消息 :重试可能导致消息重复(如Broker已写入但未及时响应)。
- 解决方案 :启用幂等性生产者 (
enable.idempotence=true
)或事务 (transactional.id
)。
- 解决方案 :启用幂等性生产者 (
消费者重试
消费者处理消息失败时,可通过重试机制重新消费消息,但需结合偏移量提交策略 。
1. 自动提交偏移量(enable.auto.commit=true
)
-
问题
:自动提交可能导致消息丢失或重复。
- 消息丢失 :消息处理失败但偏移量已提交。
- 消息重复 :消息处理成功但提交偏移量失败,下次会重复消费。
2. 手动提交偏移量(enable.auto.commit=false
)
-
推荐方式 :在业务逻辑处理完成后手动提交偏移量(
commitSync()
或commitAsync()
)。 -
重试逻辑 :使用Spring Retry库,结合Kafka的事务管理,结合DLQ
-
@Service public class KafkaConsumerService { private final KafkaTemplate<String, String> dlqKafkaTemplate; public KafkaConsumerService(KafkaTemplate<String, String> dlqKafkaTemplate) { this.dlqKafkaTemplate = dlqKafkaTemplate; } /** * 监听主Topic,启用重试 */ @KafkaListener(topics = "my-topic", groupId = "my-group") @Retryable( maxAttempts = 3, // 最大重试次数 backoff = @Backoff(delay = 1000) // 重试间隔(指数退避) ) public void consumeMessage(ConsumerRecord<String, String> record, Acknowledgment ack) { try { // 模拟业务逻辑(可能抛出异常) processMessage(record.value()); // 处理成功,提交偏移量 ack.acknowledge(); } catch (Exception e) { // 异常会触发重试,无需手动处理 throw new RuntimeException("Message processing failed", e); } } /** * 重试失败后的恢复方法(发送到DLQ) */ @Recover public void recover(RuntimeException e, ConsumerRecord<String, String> record, Acknowledgment ack) { // 发送到死信队列 dlqKafkaTemplate.send("dlq-topic", record.value()); // 提交偏移量(确保消息不会再次消费) ack.acknowledge(); System.out.println("Message sent to DLQ: " + record.value()); } private void processMessage(String message) { // 模拟处理失败 if (message.contains("fail")) { throw new RuntimeException("Simulated processing failure"); } System.out.println("Processed message: " + message); } }
3. 消费者重试策略
- 固定次数重试 :如重试3次后放弃,记录日志或发送到死信队列(DLQ)。
- 指数退避 :每次重试间隔逐渐增加(如1s、2s、4s)。
- 死信队列(DLQ) :将多次失败的消息发送到专用Topic,后续人工处理。
Kafka的幂等性
Kafka 的 幂等性(Idempotence) 是 Kafka 0.11 版本引入的核心特性,旨在解决生产者重试导致的消息重复 问题。它通过确保单个生产者在单个会话内 发送的消息不会重复写入 Kafka,从而实现“精确一次(Exactly-Once) ”语义。
1. 什么是幂等性?
在分布式系统中,幂等性指同一操作多次执行与一次执行的效果相同。
在 Kafka 中,幂等性特指:
- 生产者重复发送同一批消息时 (如网络重试或 Broker暂时性故障),Broker 仅持久化一次。
- 消费者重复拉取消息时 ,业务逻辑需自行处理重复(需结合幂等性消费)
2. 为什么需要幂等性?
问题场景
- 生产者重试 :当生产者发送消息后未收到 Broker 确认(如网络抖动),会触发重试,导致 Broker 收到重复消息。
- Broker 故障 :Leader 副本切换时,生产者可能重复发送已写入但未确认的消息。
传统解决方案的缺陷
- 关闭重试 :可能导致消息丢失。
- 消费者去重 :增加业务复杂度(需维护唯一ID、数据库去重等)。
3. Kafka 幂等性实现原理
Kafka 通过 PID(Producer ID) 和 序列号(Sequence Number) 实现生产端幂等性:
3.1 核心机制
- PID 分配
- 每个生产者启动时,Kafka Broker 会为其分配一个唯一的
PID
。 PID
通过InitProducerIdRequest
协议动态分配,无需手动配置。
- 每个生产者启动时,Kafka Broker 会为其分配一个唯一的
- 序列号追踪
- 生产者为每个分区维护一个递增的序列号(
Sequence Number
)。 - 每条消息携带 PID和 Sequence Number,Broker 根据这两个值判断是否重复:
- 如果新消息的
Sequence Number
大于 当前记录的值:接受并持久化。 - 如果 小于或等于 :判定为重复消息,丢弃。
- 如果新消息的
- 生产者为每个分区维护一个递增的序列号(
- 会话保证
- 幂等性仅在单个生产者会话内有效。生产者重启后,
PID
会变化,但序列号会重置。
- 幂等性仅在单个生产者会话内有效。生产者重启后,
4.如何启用幂等性
启用幂等性要求
①max.in.flight.requests.per.connection小于或等于5,
- 控制生产者在单个连接上能发送的未响应请求的最大数量。也就是说,生产者可以同时发送多少个消息请求而不需要等待Broker的响应。
- 幂等性需要通过序列号保证消息不重复,而过多的并发请求可能导致序列号冲突。
- 当
enable.idempotence=true
时,max.in.flight.requests.per.connection
必须 ≤5 (否则会抛出异常)
②retries重试次数大于0,
③acks必须为“all”。
spring:
kafka:
producer:
enable-idempotence: true # 启用幂等性
acks: all # 必须设置为all
retries: 3 # 重试次数
properties:
max.in.flight.requests.per.connection: 5 # 最大飞行请求数
5. 幂等性的局限性
-
仅限单会话
- 生产者重启后,
PID
变化,无法识别之前的序列号。
- 生产者重启后,
-
不跨分区
- 幂等性仅在单个分区有效,无法保证跨分区的消息去重。
-
消费者端需自行处理重复
- 幂等性仅解决生产端重复,消费者仍需处理网络重传或业务逻辑中的重复。
6. 幂等性与事务的区别
特性 | 幂等性 | 事务 |
---|---|---|
作用范围 | 单生产者、单会话、单分区 | 跨分区、跨会话、跨生产者操作 |
目标 | 避免生产端重复消息 | 保证多操作原子性(如消息发送+数据库更新) |
性能开销 | 较低 | 较高(需维护事务日志) |
典型场景 | 普通消息去重 | 跨系统数据一致性(如订单+库存) |
总结
Kafka 的幂等性通过 PID
和 Sequence Number
机制,解决了生产端因重试导致的消息重复问题,是构建高可靠消息系统的基石。它与事务、ACK 机制、重试策略结合使用,可实现端到端的 Exactly-Once 语义。在实际应用中,需根据业务需求权衡配置,并在消费者端补充幂等逻辑。
可靠性不足分析
-
尽管我们在上面讲了一些,Kafka为了实现可靠性而做的设计,一般情况下,这种程度的可靠性足以应付了。但在实际应用过程中,Kafka仍然可能会面临以下几个可靠性问题:
-
生产者重复发送:尽管开启了幂等性,但不要忘记幂等性设置仅表示生产者对同一个分区的消息的写入是有序的、幂等的,如果producer挂了,重启之后,producer会重新生成producerid,此时幂等性校验就不准了。
-
消费者重复消费:如果消费者在提交偏移量前宕机了,将导致Kafka认为该消息没有被消费,在消费者重启后,又会消费该消息,导致重复消费。
-
这些情况,Kafka自身已经无法解决。我们的解决策略只能契合在我们的业务处理上,目前一个通用的方案是全局性ID+生产/消费两端校验
好差评中是维护业务类型_评价id这个全局ID,如GAEVAL_123xxxx,同时维护一个生产者日志和一个消费者日志。
具体示例:订单系统重复下单
业务场景:用户下单时,生产者发送订单消息到 Kafka,消费者处理消息生成订单。
(跨会话导致的问题)
- 第一次会话 :
- 生产者发送订单消息
OrderId=123
(PID=100, Seq=1)。 - 消息成功写入 Kafka,但生产者未收到响应(网络问题)。
- 生产者触发重试,但此时服务崩溃。
- 生产者发送订单消息
- 第二次会话 :
- 服务重启后,生产者重新初始化(PID=200, Seq=1)。
- 重试发送
OrderId=123
,Kafka 认为是新消息(不同 PID)。
- 消费者处理 :
- 消费者先后收到两条
OrderId=123
,导致重复创建订单。
- 消费者先后收到两条
为什么幂等性无法解决跨会话重复?
- Kafka 幂等性机制 :通过
PID + Seq
确保单个会话内消息不重复。例如,如果同一会话中发送的Seq=1
消息已存在,Kafka 会拒绝后续的Seq=1
。 - 跨会话的局限性 :不同会话的
PID
不同,Kafka 无法关联新旧会话的Seq
,导致重复。
解决方案
消息表 + 数据库幂等性
生产消息时,生成唯一ID(hcp中由消息类型_评价ID组成),写入生产者消息表mq_produce_log
,消费消息时写入消费者消息表mq_consume_log
,通过数据库的唯一索引来保证消息的不重复
核心思想
- 生产端 :将消息和业务数据写入同一数据库事务。
- 消费端 :通过数据库的唯一约束和幂等性设计避免重复处理。