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 需要三管齐下:
- 幂等性生产者:设置
enable.idempotence=true,Kafka 会自动去重。 - 事务性 API:使用
beginTransaction()、commitTransaction()包裹生产和消费。 - 原子性提交:消费偏移量和处理结果在同一个事务里提交。
代码示例:
// 生产者配置
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 的核心流程串起来了。从生产者发消息,到分区分配、副本同步、消费者消费,每一步都有对应的可靠性机制。
4.5 实战建议
说了这么多理论,最后给几条我踩过坑后的经验:
- 分区数别乱设:先压测,再根据吞吐量反推。我一般留 50% 的余量。
- ACK 别盲目用 all:如果业务允许丢少量数据,
acks=1就够了。性能差好几倍。 - ISR 监控不能少:一旦 ISR 频繁变化,赶紧查集群健康状态。
- Exactly-once 慎用:事务性 API 会降低吞吐量。我只有在扣费、订单这类场景才用。
- 消费端做好幂等:就算用 At-least-once,只要消费端做好幂等,重复消费也没事。
我的习惯:生产环境默认用 acks=1 + 幂等性生产者。如果业务要求严格不丢,再升级到 acks=all + min.insync.replicas=2。别一上来就追求最高可靠性,性能扛不住。