1 什么是kafka
kafka是开源流处理平台,也是一种高吞吐量的分布式发布订阅消息系统,可以收集并处理网站中所有的动作流数据和物联网设备的采样信息。
2 消息队列的工作模式
至多消费一次:一般消息队列中的消息仅允许消费一次。消息被确认消费掉之后,会被消息服务器删除。
没有限制:消息可以被多个消费者同时消费或者同一个消费者可以多次消费同一个消息。消息服务器可以长时间存储大量消息。
3 基础架构
kafka一般是以集群的方式存在。集群以topic的形式分类集群中的record,每个record都属于一个topic。每个topic的底层都会对应一组分区的日志,用于持久化topic中的record。同时在集群中,topic的每一个日志分区,都会有一个broker担当该分区的leader,其他broker担当分区的follower。leader负责分区数据的读写,follower负责同步改分区的数据。follower负责备份,即一个topic的数据不会仅存储在一个broker上。如果某个分区的leader挂了,其他的follower会重新选举一个新的leader。
leader的监控和topic的部分元数据,会存储在zk中。
同一个消息不会同时写入多个分区。消息会根据key的hash值对分区数取模,找到对应的分区(目标分区 =hash(key)% 分区数)。如果发送消息时不指定key,那么集群会以轮询的方式,将消息分配到各个分区,确保分区的负载均衡。

4 分区&日志
每个分区中都有一个有序且不可变的日志序列,每个Record都被分配了分配了唯一的序列编号offset。kafka集群会持久化所有发布到topic中的Record消息。持久化时间通过配置文件配置,默认168个小时。
注意kafka只能保证分区内部有序(通过offset实现),但是不同分区,同样的offset,不能判断哪个消息更新。
5 生产者组合消费者组
消费者在消费完一批消息之后,会将本次消费的偏移量提交费kafka集群,所以每个消费者都可以随意控制消费者的偏移量。多个消费者之间相互独立。
每个消费者都一定属于一个消费者组(consumer group)。如果一个consumer group有多个consumer,那么一个topic的消息,只会被group中的一个consumer消费掉。消息不会被多个consumer重复消费。
如果有多个consumer group,那么一个topic的消息会被广播到所有的group。
kafka会将topic下的分区均分给consumer group中的各个consumer。如果新加入一个consumer,那么它将接管,其他consumer负责的某些分区。如果某个consumer宕机,那么它负责分区,会被其他consumer接管。
注意:一般consumer group中consumer的数量不会大于topic的分区数。如果consumer数量大于分区数,那么会有consumer空闲,不会收到任何消息。
补充:rabbit mq的队列是一个实例,不存在分区。这是其和kafka的一个区别。
6 高性能技术
kafka高吞吐量,支持每秒上百万的写入。其原因是其采用了顺序写入和零拷贝技术。
6.1 顺序写入
相较于随机IO,顺序IO,可以节省带量的硬盘寻址和内存开销。
6.2 零拷贝

7 topic 管理API
参考:
https://blog.csdn.net/qq_41187124/article/details/155382010
https://kafka.github.net.cn/documentation#producerapi
7.1 幂等性:
Kafka 通过生产者 ID(Producer ID,PID)与序列号(Sequence Number)的组合机制,结合 Broker 端的缓存校验,实现单分区内消息的幂等性写入,确保重复消息仅处理一次。
- Producer ID(PID)
每个生产者实例在初始化时,会向 Kafka 集群申请一个全局唯一的 PID。该 PID 在生产者生命周期内保持不变,重启后会生成新的 PID(因此幂等性仅限单会话)。 - 序列号(Sequence Number)
对于每个 PID 和目标分区(Partition),生产者会为每条消息分配一个单调递增的序列号 。例如:- 向 Partition 0 发送的消息序列号:1, 2, 3, ...
- 向 Partition 1 发送的消息序列号:1, 2, 3, ...
Broker 为每个 PID 和分区维护一个缓存区域(如哈希表),存储最近接收到的序列号。当消息到达时:
- 检查序列号是否存在 :
- 若序列号等于缓存中记录的"期望值",则接受消息,并更新期望值(期望值 + 1)。
- 若序列号小于期望值,说明是重复消息,直接丢弃。
- 若序列号大于期望值 + 1,说明消息乱序或丢失,抛出异常(需结合事务机制处理)。
- 缓存更新与过期
缓存中的序列号会定期清理或限制大小,平衡内存使用与去重准确性。
开启幂等性:
在生产者配置中设置enable.idempotence=true,Kafka 会自动完成以下操作:
- 分配唯一的 PID。
- 为每条消息生成序列号。
- 强制将 acks设为 all(确保消息持久化到所有副本)。
例:
java
Properties props = new Properties();
props.put("bootstrap.servers", "broker:9092");
props.put("enable.idempotence", "true"); // 启用幂等性
props.put("acks", "all"); // 强制设置为 all
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
具体配置需要参考kafka文档:
https://kafka.github.net.cn/documentation#producerconfigs_enable.idempotence
7.2 事务
kafka事务可划分为生产者事务only、消费者&生产者事务。
生产者only:向n个分区发送了m个消息(多条消息作为一个整体提交),如果有任何一条消息发送失败,全部消息都回滚。
消费者&生产者事务:消费者消费失败,则回滚offset,使得这个消息能够再次被消费。
开启生产者事务 之后需要设置消费者事务隔离级别:消费者是否能够读到生产者未提交 的数据(事务提交,则消息提交)。默认读未提交,如果开启消费者事务隔离级别,那么必须设置为读已提交。参考:https://kafka.github.net.cn/documentation#consumerconfigs_isolation.level
配置案例:
java
Properties consumerProps = new Properties();
consumerProps.put("bootstrap.servers", "localhost:9092");
consumerProps.put("group.id", "group-1");
consumerProps.put("isolation.level", "read_committed"); // 设置为 read_committed
consumerProps.put("auto.offset.reset", "earliest"); // 其他必要配置
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps);
consumer.subscribe(Arrays.asList("topic1"));
开启生产者事务,每个生产者的transactional.id都需要不同。(参考:https://kafka.github.net.cn/documentation#producerconfigs_transactional.id)
配置案例:
java
Properties producerProps = new Properties();
producerProps.put("bootstrap.servers", "localhost:9092");
producerProps.put("transactional.id", "producer-1"); // 设置唯一的事务ID
producerProps.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
producerProps.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
KafkaProducer<String, String> producer = new KafkaProducer<>(producerProps);
producer.initTransactions(); // 初始化事务支持
try {
producer.beginTransaction(); // 开始事务
producer.send(new ProducerRecord<>("topic1", "key1", "value1"));
producer.send(new ProducerRecord<>("topic1", "key2", "value2"));
producer.commitTransaction(); // 提交事务
} catch (Exception e) {
producer.abortTransaction(); // 回滚事务
}
场景案例:
需求 :
订单服务(order-service)生成订单创建事件(OrderCreated)到主题orders,库存服务(inventory-service)消费事件并扣减库存,同时生成库存变更事件(InventoryUpdated)到主题inventory-changes。需保证:
- 订单创建与库存扣减原子性(若库存不足,订单需回滚)。
- 事件顺序和幂等性,避免重复扣减。
事务作用:
- 跨服务一致性:订单和库存操作作为一个事务,要么全部成功,要么全部失败。
- 端到端 Exactly-Once:消费者从 orders 读取的事件仅处理一次,且位移与 inventory-changes写入同步。
java
KafkaConsumer<String, OrderCreated> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Collections.singletonList("orders"));
KafkaProducer<String, InventoryUpdated> producer = new KafkaProducer<>(props);
producer.initTransactions();
while (true) {
ConsumerRecords<String, OrderCreated> records = consumer.poll(Duration.ofMillis(100));
Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
try {
producer.beginTransaction();
for (ConsumerRecord<String, OrderCreated> record : records) {
// 扣减库存
boolean success = inventoryService.decreaseStock(record.value().getProductId(), record.value().getQuantity());
if (!success) throw new RuntimeException("Insufficient stock");
// 生成库存变更事件
InventoryUpdated event = new InventoryUpdated(
record.value().getProductId(),
-record.value().getQuantity()
);
producer.send(new ProducerRecord<>("inventory-changes", record.key(), event));
offsets.put(new TopicPartition(record.topic(), record.partition()),
new OffsetAndMetadata(record.offset() + 1));
}
producer.sendOffsetsToTransaction(offsets, "inventory-group");
producer.commitTransaction();
} catch (Exception e) {
producer.abortTransaction();
log.error("Inventory update failed, order rolled back", e);
}
}
8 同步机制
kafka通过分区管理topic的数据。而分区是通过segment存储文件块。一个segment默认一个G。
8.1 高水位
在0.11版本之前来保证leader和follower之间的数据同步。但是高水位机制可能导致数据同步不一致或者乱序。
名词:
LEO:每个日志文件的结尾。标识每个分区中最后一条消息的下一个位置,分区的每一个副本都有自己的LEO。
HW:高水位线,所有HW之前的数据,都是已经备份的,当所有节点备份成功,leader会更新HW。
ISR:leader会定期维护一个处于同步的副本集合,如果在指定时间(可配置)中follower没有发送fetch请求,那么该follower会被leader踢除ISR列表。
通过高水位截断同步数据可能导致数据丢失或者数据不一致的情况,例:
一、数据丢失问题
场景示例:
- 初始状态:Broker B(Leader)的HW=0,Broker A(Follower)的HW=0,LEO(日志末端位移)均为0。
- 消息写入:Broker B写入消息m2,更新自身LEO=1,但未更新HW(因Follower未确认)。
- Follower同步:Broker A从Broker B拉取m2并写入本地,更新LEO=1,但Broker B返回的HW仍为0,Broker A被迫将自身HW更新为0(取min(本地LEO=1, Leader HW=0))。
- 宕机与截断 :
- Broker A重启后,发现自身HW=0,而LEO=1,根据HW规则截断日志,删除m2。
- Broker A尝试从Broker B同步数据时,Broker B宕机,Broker A被选举为新Leader。
- 结果:消息m2永久丢失。
根本原因:
- HW更新滞后性:Follower的HW更新依赖Leader的下一轮响应,若在此期间发生宕机,Follower可能错误截断已同步但未确认的消息。
- 依赖HW截断:Follower重启后以HW为基准清理日志,而非实际同步进度(LEO),导致数据被误删。
二、数据不一致问题
场景示例:
- 初始状态:Broker B(Leader)的HW=1,Broker A(Follower)宕机。
- 消息写入:Broker B写入消息`m2,其他Follower同步完成,Broker B更新HW=2。
- 双宕机与重启 :
- Broker A和Broker B同时宕机,Broker A先重启并成为新Leader,写入消息m3,更新HW=1(因其他Follower未同步)。
- Broker B重启后,发现自身HW=2,认为无需截断日志,保留m2。
- 结果 :
- Broker A的offset=1位置为m3,Broker B为m2,同一偏移量数据不一致。
根本原因:
- HW作为唯一截断标准:Follower重启后仅依赖HW判断是否需要清理日志,忽略实际同步进度(LEO),导致新旧数据混存。
- Leader切换时的HW继承:新Leader可能继承旧Leader的HW,而旧Leader恢复后未对齐新Leader的日志,引发冲突。
8.2 leader epoch
0.11版本之后kafka采用新的数据同步机制。
一、Leader Epoch的定义与组成
Leader Epoch可以认为是Leader的版本,它由两部分数据组成:
- Epoch:一个单调增加的版本号(整数)。每当副本领导权发生变更时,都会增加该版本号。小版本号的Leader被认定是过期Leader,不能再行使Leader权力。
- 起始位移(Start Offset):Leader副本在该Epoch值上写入的首条消息的位移。它标识了该代Leader写入消息的起始位置。
二、Leader Epoch的工作机制
- Leader成为时的操作 :
- 当副本成为Leader时(例如由于Leader切换),生产者有新消息发送过来时,它会尝试在内存中的Leader Epoch缓存中添加一个新的(epoch, start_offset)条目(其中start_offset等于当前LEO),并持久化到checkpoint文件中。
- Follower的同步过程 :
- Follower会向Leader发送一个LeaderEpochRequest请求,该请求包含Follower所知的最新Epoch。
- Leader收到请求后,会根据自己存储的Leader Epoch历史,返回一个LastOffset(最后偏移量):
- 如果Follower的最后Epoch与Leader的最后Epoch相同,则LastOffset = Leader LEO。
- 如果Follower的最后Epoch落后于Leader,则Leader会查找Follower的Epoch之后的第一代Leader的start_offset作为LastOffset。
- Follower根据Leader返回的LastOffset进行日志截断操作(如果需要的话),然后开始正常的Fetch流程,从Leader拉取消息。