📖 引言:快递员的噩梦
想象一下,你是一个快递公司的老板 👔,客户最关心的是什么?
- 快?✅ 重要!
- 便宜?✅ 也重要!
- 不丢件?✅✅✅ 最重要!
如果快递总丢,客户早就跑光了!😱
Kafka也一样! 作为一个消息队列系统,最核心的能力就是:保证消息不丢失!
今天我们就来揭秘Kafka的三重保险机制!🔒
🎯 消息丢失的三个"作案现场"
在Kafka的消息传递链路中,有三个地方可能丢消息:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 生产者 │ 发送 │ Kafka │ 拉取 │ 消费者 │
│ Producer │────────>│ Broker │────────>│ Consumer │
└──────────┘ └──────────┘ └──────────┘
💀① 💀② 💀③
可能丢 可能丢 可能丢
💀 案发现场①:生产者端丢失
场景:消息还没发到Kafka就丢了
原因:
- 网络故障 📡❌
- 发送缓冲区满了 💾❌
- 生产者没等Broker确认就继续发下一条 🏃💨
- 程序异常退出 💥
💀 案发现场②:Broker端丢失
场景:消息到了Broker但没保存好
原因:
- 消息在内存中,还没来得及刷盘,Broker挂了 💀
- 只有Leader有数据,Follower还没同步,Leader就挂了 ⚰️
- 磁盘坏了 💿❌
💀 案发现场③:消费者端丢失
场景:消费者拿到消息但没处理好
原因:
- 先提交offset,再处理消息,结果处理失败 😵
- 自动提交offset,但消息还没处理完就宕机了 💀
- 异常没捕获,消息被跳过了 🐛
🛡️ 三重保险机制:让消息安全到达
🔒 保险一:生产者端保障
1️⃣ ACK确认机制 ⭐⭐⭐⭐⭐
acks参数 = 消息的"安全级别"
props.put("acks", "???");
| acks值 | 含义 | 安全性 | 性能 | 适用场景 |
|---|---|---|---|---|
| 0 | 不等确认,发完就算成功 | 💀 最低 | ⚡ 最快 | 日志收集、不重要数据 |
| 1 | 等Leader确认 | 😐 中等 | 😊 较快 | 一般业务 |
| all/-1 | 等Leader+所有ISR副本确认 | 🛡️ 最高 | 😴 最慢 | 金融、订单等重要数据 |
图解:
acks=0("裸奔模式")🏃💨
Producer: "我要发消息了!"
Producer: 直接发送... 💨
Producer: "好了,我认为发送成功了!"(实际上不知道)
Broker: "什么?我还没收到呢!" 💀
生活比喻:
你往朋友家扔纸飞机,扔完就走,也不管飞机有没有进窗户 🪟
acks=1("签收模式")📝
Producer: "我要发消息!"
Producer: 发送... 📤
Leader: "收到!我写下来了!" ✅
Producer: "好的,那我放心了!"
Follower: "等等,我还没同步呢..." 😰
(如果此时Leader挂了,消息还是会丢!)
生活比喻:
你发快递,快递员签收了,但还没送到目的地,车就出事故了 🚚💥
acks=all("保险柜模式")🔐⭐
Producer: "我要发消息!"
Producer: 发送... 📤
Leader: "收到!我写下来了!"
Follower-1: "我也同步好了!" ✅
Follower-2: "我也同步好了!" ✅
Leader: "所有副本都同步完成!"
Producer: "好的,现在我真正放心了!" 😌
生活比喻:
你发贵重物品,必须所有仓库都保存好了,才算发送成功 💎🏦
2️⃣ 重试机制 🔄
配置重试参数:
// 最大重试次数
props.put("retries", Integer.MAX_VALUE); // 无限重试(Kafka 2.1+默认)
// 重试间隔(毫秒)
props.put("retry.backoff.ms", 100);
// 请求超时时间
props.put("request.timeout.ms", 30000); // 30秒
// 发送超时时间
props.put("delivery.timeout.ms", 120000); // 120秒(总超时)
重试流程:
第1次发送 → 失败 ❌ → 等100ms → 第2次发送 → 失败 ❌
→ 等100ms → 第3次发送 → 成功 ✅
⚠️ 注意幂等性! 重试可能导致重复发送,需要配合幂等性配置:
// 开启幂等性(Kafka 0.11+)
props.put("enable.idempotence", true);
幂等性原理:
每条消息有唯一ID(PID + Sequence Number)
第1次发送: PID=100, Seq=1
重试发送: PID=100, Seq=1 ← Broker检测到重复,直接返回成功,不再保存
生活比喻:
你发微信,网络不好重复点了几次,但对方只收到一条(微信帮你去重了)✅
3️⃣ 同步发送 vs 异步发送
异步发送(默认,高性能):
// 异步发送 - 不等待结果
producer.send(record);
producer.send(record);
producer.send(record);
问题:如果程序突然退出,缓冲区的消息会丢失!💀
异步发送 + 回调(推荐):
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception != null) {
// 发送失败,记录日志或重试
log.error("消息发送失败: {}", record, exception);
// 可以保存到数据库,后续重发
saveToDb(record);
} else {
// 发送成功
log.info("消息发送成功: partition={}, offset={}",
metadata.partition(), metadata.offset());
}
}
});
同步发送(最安全,低性能):
try {
// 同步发送 - 等待结果
RecordMetadata metadata = producer.send(record).get();
System.out.println("发送成功!offset=" + metadata.offset());
} catch (Exception e) {
// 发送失败
System.err.println("发送失败:" + e.getMessage());
// 重试或保存到数据库
}
对比:
异步发送:发3条消息耗时 1ms ⚡⚡⚡
同步发送:发3条消息耗时 100ms 🐌🐌🐌
但同步发送更安全!根据业务选择!
4️⃣ 生产者端完整配置(最安全版本)⭐⭐⭐⭐⭐
Properties props = new Properties();
// 基础配置
props.put("bootstrap.servers", "kafka1:9092,kafka2:9092,kafka3:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// ⭐ 可靠性配置
props.put("acks", "all"); // 等待所有ISR副本确认
props.put("retries", Integer.MAX_VALUE); // 无限重试
props.put("max.in.flight.requests.per.connection", 5); // 最多5个未确认请求
props.put("enable.idempotence", true); // 开启幂等性
props.put("compression.type", "lz4"); // 压缩(可选,节省带宽)
// 超时配置
props.put("request.timeout.ms", 30000); // 请求超时30秒
props.put("delivery.timeout.ms", 120000); // 发送超时120秒
props.put("retry.backoff.ms", 100); // 重试间隔100ms
// 缓冲配置
props.put("buffer.memory", 33554432); // 32MB缓冲区
props.put("batch.size", 16384); // 16KB批量大小
props.put("linger.ms", 10); // 等待10ms积攒批量
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
🔒 保险二:Broker端保障
1️⃣ 副本机制(Replication)🏦
核心思想:数据要有备份!不能只存一份!
Topic: orders
Replication Factor: 3 ← 每个分区有3个副本
Partition 0:
├─ Leader (Broker-1) 📝 主副本,处理读写
├─ Follower (Broker-2) 📋 备份,同步数据
└─ Follower (Broker-3) 📋 备份,同步数据
创建Topic时指定副本数:
kafka-topics.sh --create \
--bootstrap-server localhost:9092 \
--topic orders \
--partitions 3 \
--replication-factor 3 # ⭐ 3个副本
生活比喻:
重要文件打印3份,分别存在家里、公司、银行保险柜 🏠🏢🏦
2️⃣ ISR机制(In-Sync Replicas)⭐⭐⭐⭐⭐
什么是ISR?
ISR = "同步中的副本集合" = 跟Leader同步进度差不多的副本
所有副本 = [Leader, Follower-1, Follower-2, Follower-3]
ISR(同步副本)= [Leader, Follower-1, Follower-2] ✅ 数据同步快
OSR(落后副本)= [Follower-3] ❌ 数据同步太慢,被踢出ISR
为什么需要ISR?
如果acks=all要等所有副本确认:
- Follower-3很慢(网络差/机器慢/磁盘慢)
- Producer一直等,等到天荒地老 😴
- 整个系统卡死!
有了ISR:
acks=all只需等ISR中的副本确认- 慢的副本被踢出ISR,不影响整体性能
- 既保证可靠性,又保证性能 🎯
3️⃣ min.insync.replicas(最小同步副本数)🔐
配置说明:
# Topic级别配置(推荐)
min.insync.replicas=2
含义:
- ISR中至少要有2个副本,才允许写入
- 如果ISR副本数 < 2,拒绝写入,抛异常 ❌
场景分析:
Replication Factor: 3 (总共3个副本)
min.insync.replicas: 2 (至少2个副本在ISR中)
正常情况:
ISR = [Leader, Follower-1, Follower-2] (3个) ✅ 可以写入
Follower-2挂了:
ISR = [Leader, Follower-1] (2个) ✅ 还可以写入
Follower-1也挂了:
ISR = [Leader] (1个) ❌ 拒绝写入!
Producer会收到异常:NotEnoughReplicasException
为什么这样设计?
场景:Leader突然挂了
ISR = [Leader] (只有Leader自己)
如果允许写入:
1. Producer写入消息到Leader ✅
2. Leader确认成功 ✅
3. Leader突然挂了 💀
4. 选举Follower为新Leader
5. 新Leader没有刚才的消息!💀💀💀
有了min.insync.replicas=2:
ISR = [Leader] (只有1个,小于2)
Producer尝试写入:
❌ 拒绝!抛异常:NotEnoughReplicasException
Producer可以:
- 重试(等Follower恢复)
- 告警(人工介入)
- 降级处理(写到其他地方)
生活比喻:
银行规定:大额转账必须两个工作人员同时确认,如果只有一个人在,拒绝办理!🏦👨👨
4️⃣ 刷盘机制
Linux的页缓存(Page Cache):
Producer → Kafka → 写入内存(Page Cache)→ 操作系统刷盘 → 磁盘
↑
很快!(微秒级) ↓
慢!(毫秒级)
Kafka默认策略:
- 写入Page Cache后,立即返回成功 ✅
- 由操作系统决定何时刷盘(异步刷盘)
- 如果机器突然断电,Page Cache中的数据会丢失 💀
如何保证不丢?
答案:靠副本!
只要ISR中有多个副本,即使Leader断电丢失Page Cache的数据,
Follower上还有数据,不会真正丢失!🎯
配置刷盘参数(可选):
# 每N条消息强制刷盘
log.flush.interval.messages=10000
# 每N毫秒强制刷盘
log.flush.interval.ms=1000
⚠️ 注意:一般不需要配置,让操作系统自己决定更高效!
5️⃣ Broker端最佳配置⭐⭐⭐⭐⭐
# ========== 副本配置 ==========
# 默认副本数(建议3)
default.replication.factor=3
# 最小同步副本数(建议2)
min.insync.replicas=2
# 不允许unclean leader选举(重要!)
unclean.leader.election.enable=false # ⭐⭐⭐
# ========== ISR配置 ==========
# 副本落后多久会被踢出ISR
replica.lag.time.max.ms=10000 # 10秒
# ========== 消息保留 ==========
# 消息保留时间(7天)
log.retention.hours=168
# 消息保留大小(100GB)
log.retention.bytes=107374182400
⭐ unclean.leader.election.enable=false 很重要!
场景:
ISR = [Leader] (只有Leader)
OSR = [Follower-1, Follower-2] (落后的副本)
Leader挂了!需要选举新Leader!
如果 unclean.leader.election.enable=true:
→ 允许从OSR中选举Leader
→ Follower-1被选为新Leader
→ 但是它的数据是旧的!💀
→ 最新的消息丢失了!
如果 unclean.leader.election.enable=false(推荐):
→ 不允许从OSR中选举
→ 分区进入离线状态,拒绝服务 ❌
→ 等Leader恢复,或者人工介入
→ 数据不会丢失!✅
权衡:
true:可用性优先(服务不中断,但可能丢数据)false:一致性优先(宁可服务中断,也不丢数据)⭐
金融、订单等场景,必须选false!
🔒 保险三:消费者端保障
1️⃣ 手动提交Offset ⭐⭐⭐⭐⭐
错误做法❌:自动提交
// 自动提交offset(默认)
props.put("enable.auto.commit", true);
props.put("auto.commit.interval.ms", 5000); // 每5秒自动提交
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
// 处理消息
processMessage(record); // 如果这里抛异常...
}
// 5秒后,offset自动提交了
// 但如果上面的processMessage失败,消息丢失!💀
}
问题:
时间线:
0秒:拉取消息 offset=100-109
1秒:处理到offset=105
2秒:处理offset=106时抛异常 💥
3秒:程序重启
5秒:自动提交了offset=110(自动提交是定时的)
重启后:
从offset=110继续消费
offset=106-109的消息永远丢失了!💀💀💀
正确做法✅:手动提交
// 关闭自动提交
props.put("enable.auto.commit", false);
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
try {
// 处理消息
processMessage(record);
// ⭐ 处理成功后,手动提交offset
consumer.commitSync(); // 同步提交(阻塞)
} catch (Exception e) {
log.error("处理消息失败: {}", record, e);
// 不提交offset,下次重新消费这条消息
break; // 退出循环,重新poll
}
}
}
同步提交 vs 异步提交:
// 同步提交(阻塞,慢,但可靠)
consumer.commitSync();
// 异步提交(不阻塞,快,但可能失败)
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets,
Exception exception) {
if (exception != null) {
log.error("提交offset失败", exception);
}
}
});
最佳实践(混合使用):
try {
while (running) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
processMessage(record);
}
// 正常情况:异步提交(快)
consumer.commitAsync();
}
} finally {
try {
// 关闭前:同步提交(保证提交成功)
consumer.commitSync();
} finally {
consumer.close();
}
}
2️⃣ 精确控制Offset
场景:批量处理
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
processMessage(record);
// ⭐ 每处理一条,就提交一次
Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
offsets.put(
new TopicPartition(record.topic(), record.partition()),
new OffsetAndMetadata(record.offset() + 1) // 下次从这个offset+1开始
);
consumer.commitSync(offsets);
}
注意:offset + 1!
当前消费:offset=100
提交:offset=101 ← 下次从101开始消费
为什么+1?
因为提交的offset表示"下次要消费的offset"
3️⃣ 消费者端异常处理
完整的异常处理流程:
public class ReliableConsumer {
private static final int MAX_RETRY = 3; // 最大重试次数
public void consume() {
KafkaConsumer<String, String> consumer = createConsumer();
try {
while (running) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
boolean success = processWithRetry(record);
if (success) {
// 成功:提交offset
commitOffset(consumer, record);
} else {
// 失败:发送到死信队列
sendToDeadLetterQueue(record);
// 继续提交offset(跳过这条消息)
commitOffset(consumer, record);
}
}
}
} finally {
consumer.close();
}
}
/**
* 处理消息,带重试
*/
private boolean processWithRetry(ConsumerRecord<String, String> record) {
for (int i = 0; i < MAX_RETRY; i++) {
try {
processMessage(record);
return true; // 成功
} catch (Exception e) {
log.warn("处理消息失败,第{}次重试", i + 1, e);
if (i < MAX_RETRY - 1) {
// 等待一段时间后重试
sleep(1000 * (i + 1)); // 递增等待时间
}
}
}
log.error("处理消息失败,已重试{}次", MAX_RETRY);
return false; // 失败
}
/**
* 提交offset
*/
private void commitOffset(KafkaConsumer<String, String> consumer,
ConsumerRecord<String, String> record) {
Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
offsets.put(
new TopicPartition(record.topic(), record.partition()),
new OffsetAndMetadata(record.offset() + 1)
);
consumer.commitSync(offsets);
}
/**
* 发送到死信队列
*/
private void sendToDeadLetterQueue(ConsumerRecord<String, String> record) {
// 保存到数据库或发送到另一个Topic
try {
deadLetterProducer.send(
new ProducerRecord<>("orders-dead-letter",
record.key(),
record.value())
);
log.info("消息已发送到死信队列: {}", record);
} catch (Exception e) {
log.error("发送到死信队列失败", e);
}
}
}
4️⃣ 消费者端最佳配置⭐⭐⭐⭐⭐
Properties props = new Properties();
// 基础配置
props.put("bootstrap.servers", "kafka1:9092,kafka2:9092,kafka3:9092");
props.put("group.id", "order-consumer-group");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// ⭐ 可靠性配置
props.put("enable.auto.commit", false); // 关闭自动提交
// 从最早的offset开始消费(如果没有已提交的offset)
props.put("auto.offset.reset", "earliest");
// ⭐ 隔离级别(如果使用了Kafka事务)
props.put("isolation.level", "read_committed"); // 只读已提交的消息
// 性能配置
props.put("fetch.min.bytes", 1024); // 最少拉取1KB
props.put("fetch.max.wait.ms", 500); // 最多等待500ms
props.put("max.poll.records", 500); // 单次最多拉取500条
// 会话配置
props.put("session.timeout.ms", 30000); // 30秒
props.put("heartbeat.interval.ms", 3000); // 3秒
props.put("max.poll.interval.ms", 300000); // 5分钟
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
🎯 终极方案:事务消息(Exactly Once)
Kafka 0.11+支持事务!
什么是事务消息?
保证:
- 原子性:多条消息要么全部发送成功,要么全部失败
- Exactly Once:消息有且仅有一次,不重复,不丢失
场景:
转账业务:
- 从A账户扣款 → 发送消息1
- 给B账户加款 → 发送消息2
要求:两条消息要么都成功,要么都失败!
如果只成功了一条,就出事了!💀
生产者事务代码
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// ⭐ 事务配置
props.put("transactional.id", "my-transactional-id"); // 事务ID(必须唯一)
props.put("enable.idempotence", true); // 必须开启幂等性
props.put("acks", "all"); // 必须等所有ISR副本确认
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
// ⭐ 初始化事务
producer.initTransactions();
try {
// ⭐ 开启事务
producer.beginTransaction();
// 发送多条消息
producer.send(new ProducerRecord<>("topic1", "key1", "value1"));
producer.send(new ProducerRecord<>("topic2", "key2", "value2"));
producer.send(new ProducerRecord<>("topic3", "key3", "value3"));
// 业务逻辑...
doBusinessLogic();
// ⭐ 提交事务(所有消息一起生效)
producer.commitTransaction();
} catch (Exception e) {
// ⭐ 回滚事务(所有消息都不生效)
producer.abortTransaction();
}
producer.close();
消费者事务代码
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "my-group");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// ⭐ 只读已提交的消息(忽略未提交/已回滚的消息)
props.put("isolation.level", "read_committed");
props.put("enable.auto.commit", false);
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("topic1", "topic2", "topic3"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
// 只能读到已提交的消息
System.out.println("消费: " + record.value());
}
consumer.commitSync();
}
事务流程图
┌─────────────────────────────────────────────────────┐
│ Producer │
│ │
│ beginTransaction() │
│ ↓ │
│ send(msg1) ─┐ │
│ send(msg2) ─┼→ 写入Broker(标记为"未提交") │
│ send(msg3) ─┘ │
│ ↓ │
│ commitTransaction() │
│ ↓ │
│ Broker标记所有消息为"已提交" ✅ │
│ │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Consumer │
│ │
│ isolation.level = read_committed │
│ ↓ │
│ 只读"已提交"的消息 │
│ 忽略"未提交"或"已回滚"的消息 ✅ │
│ │
└─────────────────────────────────────────────────────┘
好处:
- ✅ 完全避免重复消费(Exactly Once)
- ✅ 原子性保证(多条消息要么全成功,要么全失败)
- ✅ 消费者永远不会读到一半的事务
代价:
- ❌ 性能略有下降
- ❌ 配置稍微复杂
📊 消息不丢失完整配置总结
生产者端 ⭐⭐⭐⭐⭐
Properties props = new Properties();
// ============ 可靠性核心配置 ============
props.put("acks", "all"); // 最重要!
props.put("retries", Integer.MAX_VALUE);
props.put("max.in.flight.requests.per.connection", 5);
props.put("enable.idempotence", true);
// ============ 超时配置 ============
props.put("request.timeout.ms", 30000);
props.put("delivery.timeout.ms", 120000);
// ============ 事务配置(可选,最高级别保证)============
props.put("transactional.id", "unique-transactional-id");
Broker端 ⭐⭐⭐⭐⭐
# ============ 副本配置 ============
default.replication.factor=3 # 最重要!
min.insync.replicas=2 # 最重要!
# ============ Leader选举 ============
unclean.leader.election.enable=false # 最重要!
# ============ ISR配置 ============
replica.lag.time.max.ms=10000
消费者端 ⭐⭐⭐⭐⭐
Properties props = new Properties();
// ============ 可靠性核心配置 ============
props.put("enable.auto.commit", false); // 最重要!手动提交
// ============ 隔离级别(如果用了事务)============
props.put("isolation.level", "read_committed");
// ============ 重置策略 ============
props.put("auto.offset.reset", "earliest");
代码模式:
while (running) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
try {
// 处理消息
processMessage(record);
// ⭐ 处理成功后,立即手动提交
commitOffset(consumer, record);
} catch (Exception e) {
// 记录日志,发送告警
log.error("处理消息失败", e);
// 不提交offset,下次重新消费
break;
}
}
}
🎓 面试题速答
Q1: Kafka如何保证消息不丢失?
A: 三个层面保证!
- 生产者端:
acks=all+ 重试 + 幂等性 - Broker端:副本机制(3副本)+ ISR +
min.insync.replicas=2 - 消费者端:手动提交offset
Q2: acks有哪些取值,分别是什么意思?
A: 三个取值!
acks=0:发完就算成功,最快但最不安全 💀acks=1:Leader确认即可,中等安全 😐acks=all/-1:所有ISR副本确认,最安全 ✅(推荐)
Q3: 什么是ISR?
A: In-Sync Replicas(同步副本集合)
- 和Leader同步进度差不多的副本
acks=all只需要ISR中的副本确认,不需要所有副本- 慢的副本会被踢出ISR,避免拖慢整体性能
Q4: min.insync.replicas是什么?
A: 最小同步副本数
- 规定ISR中至少要有几个副本,才允许写入
- 常见配置:
min.insync.replicas=2 - 如果ISR副本数 < 2,拒绝写入,保证数据安全
示例:
replication-factor=3, min.insync.replicas=2
ISR=[Leader, Follower1, Follower2] ✅ 可以写
ISR=[Leader, Follower1] ✅ 可以写
ISR=[Leader] ❌ 拒绝写入!
Q5: 消费者如何保证不丢消息?
A: 手动提交offset!
错误做法:
自动提交 → 拉取消息 → 处理失败 → offset已自动提交 → 消息丢失 💀
正确做法:
手动提交 → 拉取消息 → 处理成功 → 手动提交offset ✅
→ 处理失败 → 不提交offset,下次重新消费 ✅
Q6: unclean.leader.election.enable是什么?
A: 是否允许"不干净"的Leader选举
-
true:允许从OSR(落后的副本)中选Leader- 优点:可用性高,服务不中断
- 缺点:可能丢数据!💀
-
false:不允许从OSR中选Leader(推荐)✅- 优点:不丢数据
- 缺点:分区可能暂时不可用
重要数据必须设为false!
Q7: Kafka事务是怎么回事?
A: Exactly Once语义!
普通模式:
- At Least Once:至少一次(可能重复)
- At Most Once:最多一次(可能丢失)
事务模式:
- Exactly Once:有且仅有一次(不重复,不丢失)✅
实现:
- Producer:
transactional.id+beginTransaction()+commitTransaction() - Consumer:
isolation.level=read_committed - 多条消息原子性提交
🎯 生产环境检查清单
Producer检查 ✅
□ acks=all
□ enable.idempotence=true
□ retries=Integer.MAX_VALUE
□ 异步发送配合回调
□ 重要数据使用同步发送
□ 考虑使用事务(如果需要Exactly Once)
Broker检查 ✅
□ replication.factor=3
□ min.insync.replicas=2
□ unclean.leader.election.enable=false
□ 监控ISR副本数
□ 监控Leader切换
□ 定期检查磁盘空间
Consumer检查 ✅
□ enable.auto.commit=false
□ 手动提交offset
□ 异常处理完善
□ 有重试机制
□ 有死信队列
□ 监控消费延迟
🎬 总结:一张图看懂Kafka消息不丢失
Kafka消息不丢失全景图
┌──────────────────────────────────────────────────────────┐
│ Producer │
│ │
│ ✅ acks=all(等所有ISR副本确认) │
│ ✅ enable.idempotence=true(幂等性,避免重复) │
│ ✅ retries=MAX(失败重试) │
│ ✅ 异步发送+回调(或同步发送) │
│ ✅ 事务(Exactly Once,可选) │
│ │
└──────────────────────────────────────────────────────────┘
↓ 发送消息
┌──────────────────────────────────────────────────────────┐
│ Kafka Broker Cluster │
│ │
│ Partition 0: │
│ ├─ Leader (Broker-1) 📝 │
│ ├─ Follower (Broker-2) 📋 ISR │
│ └─ Follower (Broker-3) 📋 ISR │
│ │
│ ✅ replication.factor=3(3个副本) │
│ ✅ min.insync.replicas=2(至少2个副本) │
│ ✅ unclean.leader.election.enable=false(不丢数据) │
│ │
└──────────────────────────────────────────────────────────┘
↓ 拉取消息
┌──────────────────────────────────────────────────────────┐
│ Consumer │
│ │
│ ✅ enable.auto.commit=false(手动提交offset) │
│ ✅ 处理消息 → 成功 → 提交offset │
│ ✅ 处理消息 → 失败 → 不提交,重新消费 │
│ ✅ 异常处理 + 重试 + 死信队列 │
│ ✅ isolation.level=read_committed(事务场景) │
│ │
└──────────────────────────────────────────────────────────┘
三重保险,层层保障,消息绝不丢失!🛡️
🎉 恭喜你!
你已经完全掌握了Kafka如何保证消息不丢失!🎊
记住核心三点:
- Producer:
acks=all+ 幂等性 + 重试 - Broker: 3副本 + ISR +
min.insync.replicas=2 - Consumer: 手动提交offset
下次面试,这样回答:
"Kafka通过三个层面保证消息不丢失:
生产者端,配置acks=all,确保所有ISR副本都确认,开启幂等性避免重复,配置重试机制。
Broker端,配置3个副本,min.insync.replicas=2,保证至少2个副本同步,unclean.leader.election.enable=false,防止脏数据成为新Leader。
消费者端,关闭自动提交,手动提交offset,只有处理成功才提交,失败则重新消费。
如果需要更高级别保证,可以使用Kafka事务,实现Exactly Once语义。"
面试官:👍 "完美!你对Kafka理解很深刻!"
📚 推荐阅读
🎈 表情包时间 🎈
学完Kafka消息不丢失后的你:
之前:
😰 "消息会不会丢啊..."
现在:
😎 "acks=all + 3副本 + 手动提交,稳!"
面试官:
😲 "这小子可以啊!"
本文完 🎬
记得点赞👍 收藏⭐ 分享🔗
上一篇: 182-Kafka的分区策略和消费者组负载均衡.md
下一篇: 184-Kafka的消息积压如何处理.md
作者注:写完这篇,我觉得Kafka可以改名叫"保险箱"了!🔐
如果这篇文章对你有帮助,请给我一个Star⭐!版权声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。