4、消息队列核心原理:Kafka分区机制、消息可靠性保证(ACK、ISR)、消费模型(At-least-once/Exactly-once)

消息队列这东西,说白了就是系统的「缓冲带」。我做了这么多年实时数据架构,见过太多系统因为没用好消息队列,要么丢数据,要么重复消费,要么直接撑爆。今天咱们就聊聊 Kafka 最核心的三个点:分区怎么玩、消息怎么不丢、消费怎么保证不重复。

一句话总结:Kafka 的可靠性,全靠分区、副本、ACK 和 ISR 这四兄弟撑着。你只要搞懂它们怎么配合,就能在性能和可靠性之间找到平衡点。

4.1 分区机制:Kafka 的并行基石

Kafka 为什么快?分区。每个 Topic 可以拆成多个 Partition,每个 Partition 就是一个有序的日志文件。我刚开始用 Kafka 时犯过一个错——Topic 只设了 1 个分区,结果消费者一多,全在抢同一个分区,吞吐量惨不忍睹。

分区的作用其实就三个:

  • 水平扩展:分区越多,并行度越高。每个分区只能被一个消费者线程消费,所以分区数决定了消费端的最大并行能力。
  • 顺序保证:同一个分区内,消息是有序的。但跨分区,顺序就别想了。如果你需要全局有序,那就只能用一个分区——代价是吞吐量暴跌。
  • 数据冗余:每个分区可以有多个副本,分布在不同的 Broker 上。副本数决定了你能容忍几台机器挂掉。

分区数怎么定?我有个经验公式:

分区数 = max(生产者吞吐量 / 单个分区写入速度, 消费者吞吐量 / 单个分区消费速度) × 1.5

嗯,这里要注意:分区数不是越多越好。每个分区都会占用文件句柄和内存,分区太多反而会拖慢 ZooKeeper 的元数据管理。我见过有人把分区设到 1000 个,结果集群重启花了半小时。

我的习惯:生产环境一般把分区数控制在 50-200 之间。如果业务量特别大,可以按 Topic 维度单独评估,别一刀切。

4.2 消息可靠性保证:ACK 与 ISR

消息会不会丢?这是实时系统最要命的问题。Kafka 的可靠性,核心靠两个东西:ACK 机制和 ISR 列表。

4.2.1 ACK 参数:你选性能还是选安全?

生产者发消息时,可以设置 acks 参数。这个参数决定了「多少个副本确认写入后,才算发送成功」。有三种选择:

ACK 值 含义 可靠性 性能 适用场景
acks=0 发出去就不管了 极低 最高 日志、监控等可丢数据
acks=1 Leader 写入成功就算成功 中等 较高 大部分业务场景
acks=all 所有 ISR 副本都写入才算成功 最高 最低 金融、订单等关键数据

我曾经在金融项目中用过 acks=all,配合 min.insync.replicas=2,保证即使一个 Broker 挂了,数据也不会丢。但代价是写入延迟从 5ms 涨到了 20ms。你想想看,这 15ms 的差距,在实时风控场景下可能就是一笔交易的风险。

避坑指南:我曾经遇到过一个问题——acks=all 但 min.insync.replicas 设成了 1。结果 Leader 挂了,ISR 里只剩一个副本,写入直接报错。后来我学乖了,min.insync.replicas 至少设成 2,副本数至少 3。

4.2.2 ISR:谁才是真正的「可靠副本」?

ISR(In-Sync Replicas)就是「跟 Leader 保持同步的副本列表」。不是所有副本都能进 ISR,只有那些数据跟 Leader 差距不大的副本才行。

Kafka 怎么判断一个副本是否「同步」?看两个参数:

  • replica.lag.time.max.ms:默认 30 秒。如果副本超过这个时间没向 Leader 拉取数据,就被踢出 ISR。
  • replica.lag.max.messages:这个参数在 0.9 版本后已经废弃了,现在只看时间。

ISR 的作用很关键:

  • 只有 ISR 里的副本才有资格成为新的 Leader。
  • 只有 ISR 里的副本都确认写入,acks=all 才算成功。
  • 如果 ISR 为空,那整个分区就不可用了。

我建议你把 ISR 的监控做起来。一旦发现 ISR 频繁收缩,说明集群有问题——要么网络抖动,要么磁盘 I/O 瓶颈,要么 GC 太频繁。

4.3 消费模型:At-least-once 与 Exactly-once

消费模型解决的是「消息会不会重复处理」的问题。你想想看,如果一条订单消息被重复消费,那用户可能被扣两次钱。这谁受得了?

4.3.1 At-least-once:至少一次

这是 Kafka 默认的消费语义。消费者先处理消息,再提交偏移量。如果处理完但提交前挂了,重启后会重新消费这条消息。

优点:不会丢消息。
缺点:可能重复消费。

我一般在日志采集、监控告警这类场景用 At-least-once。重复几条日志无所谓,但丢了就查不到问题了。

4.3.2 Exactly-once:精确一次

这是最理想的语义,但实现起来最复杂。Kafka 在 0.11 版本后引入了幂等性生产和事务性消费,才真正支持 Exactly-once。

实现 Exactly-once 需要三管齐下:

  1. 幂等性生产者:设置 enable.idempotence=true,Kafka 会自动去重。
  2. 事务性 API:使用 beginTransaction()、commitTransaction() 包裹生产和消费。
  3. 原子性提交:消费偏移量和处理结果在同一个事务里提交。

代码示例:

// 生产者配置
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("enable.idempotence", true);
props.put("transactional.id", "my-transactional-id");

KafkaProducer<String, String> producer = new KafkaProducer<>(props);
producer.initTransactions();

try {
    producer.beginTransaction();
    producer.send(new ProducerRecord<>("orders", "key1", "value1"));
    producer.send(new ProducerRecord<>("orders", "key2", "value2"));
    producer.commitTransaction();
} catch (Exception e) {
    producer.abortTransaction();
}

注意:Exactly-once 是有代价的。事务性 API 会引入额外的网络开销和延迟。我建议只在金融交易、订单处理等场景使用,别滥用。

4.3.3 At-most-once:最多一次

这个用得少,但偶尔也有用。消费者先提交偏移量,再处理消息。如果处理时挂了,偏移量已经提交了,消息就丢了。

优点:不会重复消费。
缺点:可能丢消息。

我只有在「丢了也无所谓」的场景才用,比如一些非关键的统计指标。

4.4 核心逻辑总览

下面这张图把 Kafka 的核心流程串起来了。从生产者发消息,到分区分配、副本同步、消费者消费,每一步都有对应的可靠性机制。

Kafka 消息流转与可靠性机制 生产者 Producer acks 参数 0 / 1 / all 决定写入可靠性 Topic 分区 分区特性 有序 + 并行 分区数 = 并行度 副本组 Leader(读写) Follower(同步) Follower(同步) ISR 列表 同步副本集合 消费者 消费语义 At-least-once Exactly-once At-most-once 核心总结 分区 = 并行基石 | 副本 = 冗余保障 | ACK = 写入确认 | ISR = 同步标准 消费语义选择: At-least-once(不丢但可能重复)→ 日志监控 Exactly-once(不丢不重复)→ 金融交易

4.5 实战建议

说了这么多理论,最后给几条我踩过坑后的经验:

  • 分区数别乱设:先压测,再根据吞吐量反推。我一般留 50% 的余量。
  • ACK 别盲目用 all:如果业务允许丢少量数据,acks=1 就够了。性能差好几倍。
  • ISR 监控不能少:一旦 ISR 频繁变化,赶紧查集群健康状态。
  • Exactly-once 慎用:事务性 API 会降低吞吐量。我只有在扣费、订单这类场景才用。
  • 消费端做好幂等:就算用 At-least-once,只要消费端做好幂等,重复消费也没事。

我的习惯:生产环境默认用 acks=1 + 幂等性生产者。如果业务要求严格不丢,再升级到 acks=all + min.insync.replicas=2。别一上来就追求最高可靠性,性能扛不住。