🛡️ Kafka如何保证消息不丢失:数据安全的三重保险!

25 阅读17分钟

📖 引言:快递员的噩梦

想象一下,你是一个快递公司的老板 👔,客户最关心的是什么?

  • ?✅ 重要!
  • 便宜?✅ 也重要!
  • 不丢件?✅✅✅ 最重要!

如果快递总丢,客户早就跑光了!😱

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+支持事务!

什么是事务消息?

保证

  1. 原子性:多条消息要么全部发送成功,要么全部失败
  2. 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: 三个层面保证!

  1. 生产者端acks=all + 重试 + 幂等性
  2. Broker端:副本机制(3副本)+ ISR + min.insync.replicas=2
  3. 消费者端:手动提交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:有且仅有一次(不重复,不丢失)✅

实现

  1. Producer:transactional.id + beginTransaction() + commitTransaction()
  2. Consumer:isolation.level=read_committed
  3. 多条消息原子性提交

🎯 生产环境检查清单

Producer检查 ✅

acks=all
□ enable.idempotence=trueretries=Integer.MAX_VALUE
□ 异步发送配合回调
□ 重要数据使用同步发送
□ 考虑使用事务(如果需要Exactly Once)

Broker检查 ✅

replication.factor=3min.insync.replicas=2unclean.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=33个副本)                      │
│  ✅ min.insync.replicas=2(至少2个副本)                 │
│  ✅ unclean.leader.election.enable=false(不丢数据)     │
│                                                          │
└──────────────────────────────────────────────────────────┘
                         ↓  拉取消息
┌──────────────────────────────────────────────────────────┐
│                      Consumer                            │
│                                                          │
│  ✅ enable.auto.commit=false(手动提交offset)           │
│  ✅ 处理消息 → 成功 → 提交offset                         │
│  ✅ 处理消息 → 失败 → 不提交,重新消费                   │
│  ✅ 异常处理 + 重试 + 死信队列                           │
│  ✅ isolation.level=read_committed(事务场景)           │
│                                                          │
└──────────────────────────────────────────────────────────┘

         三重保险,层层保障,消息绝不丢失!🛡️

🎉 恭喜你!

你已经完全掌握了Kafka如何保证消息不丢失!🎊

记住核心三点

  1. Producer: acks=all + 幂等性 + 重试
  2. Broker: 3副本 + ISR + min.insync.replicas=2
  3. 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 许可协议,转载请注明出处。