本文从核心概念讲起,梳理 Kafka 的特性与典型应用场景,再用一个电商订单系统串起架构、消息流和业务逻辑的实践,最后简单对比 RabbitMQ、RocketMQ、Pulsar 等常见方案。
Apache Kafka 最初由 LinkedIn 开发,后来捐给 Apache 基金会。它常被叫做“消息队列”,但更准确的定位是:分布式事件流平台(Distributed Event Streaming Platform)。
它做三件事:
理解 Kafka 的关键,是把它想成一个分布式的、只追加写入的提交日志(Commit Log)。消息写进去之后不会因为被消费就消失,而是按保留策略(时间或大小)保存,消费者只是记录“我读到哪了”。这一点是它与传统消息队列最本质的区别。
| 概念 | 说明 |
|---|---|
| Producer | 生产者,向 Kafka 发送消息 |
| Consumer | 消费者,从 Kafka 拉取消息 |
| Broker | Kafka 服务节点,多个 Broker 组成集群 |
| Topic | 主题,消息的逻辑分类,如 order-created |
| Partition | 分区,Topic 的物理分片,是并行度和有序性的基本单位 |
| Offset | 分区内每条消息的唯一递增序号 |
| Consumer Group | 消费者组,组内成员共同分担一个 Topic 的分区 |
| Replica | 副本,分区在多个 Broker 上的拷贝,用于高可用 |
一个 Topic 被切成若干 Partition,每个 Partition 是一个独立的、有序的、只追加的日志文件:
Topic: order-events
Partition 0: [0][1][2][3][4][5] →
Partition 1: [0][1][2][3] →
Partition 2: [0][1][2][3][4] →
几个要点:
所以 Kafka 同时支持两种模式:组内竞争消费(类似队列)和组间广播(类似发布订阅)。比如“库存服务”和“数据分析服务”各用一个消费者组,就都能完整收到订单事件。
每个分区有一个 Leader 和若干 Follower。读写都走 Leader,Follower 异步(准确说是拉取)同步数据。与 Leader 保持同步的副本集合叫 ISR(In-Sync Replicas)。
关键参数组合:
replication.factor=3:每个分区 3 个副本;min.insync.replicas=2:至少 2 个副本确认才算写入成功;acks=all:等待 ISR 全部确认。三者配合,可以在单个 Broker 宕机时既不丢数据,又不中断服务。
早期 Kafka 依赖 ZooKeeper 管理集群元数据。新版本引入了基于 Raft 的 KRaft 模式,由 Kafka 自己管理元数据;从 4.0 开始已经完全移除了 ZooKeeper。新项目直接使用 KRaft 即可,部署和运维都更简单。
Kafka 单集群可以轻松达到每秒百万级消息。主要得益于:
sendfile 直接把磁盘数据送到网卡;消息按保留策略落盘,消费完不会删除。这带来两个很实用的能力:
分区可以分布在不同 Broker 上,加机器即可扩容;消费者组在成员变化时会触发 Rebalance,自动重新分配分区。
enable.idempotence=true)+ 事务实现,主要在“Kafka 读 → 处理 → Kafka 写”的链路内成立。需要注意:Exactly once 只在 Kafka 内部闭环成立,一旦涉及外部系统(数据库、HTTP 调用),仍然需要业务层做幂等。这一点后面实践部分会展开。
下面以一个电商订单系统为例,说明在真实项目里怎么用 Kafka。示例做了简化,但思路是通用的。
┌───────────────┐
用户请求 ─→ 订单服务 ──┤ Kafka 集群 ├──→ 库存服务(组:inventory)
│ │ ├──→ 支付服务(组:payment)
▼ │ order-created ├──→ 通知服务(组:notification)
订单数据库 │ order-paid ├──→ 数据分析(组:analytics)
(含 Outbox 表) │ order-dlq │
│ └───────────────┘
└── Outbox 转发器(轮询 / CDC) ──→ Kafka
设计思路:
Topic 命名:领域.实体.事件,例如 order.order.created、order.order.paid。团队统一规范,后期治理会轻松很多。
消息结构(以 JSON 为例,生产环境推荐 Avro / Protobuf + Schema Registry):
{
"eventId": "7c1f2a9e-5b34-4a8e-9a77-0e1c1b2d3f40",
"eventType": "OrderCreated",
"occurredAt": "2026-10-26T08:30:00Z",
"version": 1,
"data": {
"orderId": "ORD-20261026-0001",
"userId": "U10086",
"items": [{ "sku": "SKU-001", "qty": 2 }],
"amount": 199.00
}
}
几个经验:
eventId(用于消费端去重)和 version(用于格式演进);分区数:按“目标吞吐 ÷ 单分区吞吐”估算,并为未来增长留余量。分区只能增加不能减少,且增加分区会改变 Key 到分区的映射,所以初期要规划好。
完整的一次下单流转:
order.order.created;inventory.stock.reserved;order.order.paid;order.order.paid,分别发短信和写入报表;inventory.stock.released),订单服务据此更新状态。这种“事件接力 + 补偿”的方式,本质上是 Saga 模式(编排式 / 协同式),用来替代跨服务的分布式事务。
最常见的坑:先写数据库再发 Kafka,如果中间进程挂了,数据库有订单而消息没发出去;反过来则是消息发了但订单回滚。
解决方案是 Transactional Outbox:
这样得到的是 至少一次 的投递,重复问题交给消费端处理。
既然是至少一次,消费者就必须能容忍重复。常用手段:
eventId 建去重表(唯一索引),先插入再处理,插入失败说明已处理过;以 .NET 的 Confluent.Kafka 为例:
var config = new ProducerConfig
{
BootstrapServers = "kafka1:9092,kafka2:9092,kafka3:9092",
Acks = Acks.All, // 等待 ISR 全部确认
EnableIdempotence = true, // 幂等生产者,避免重试导致重复
CompressionType = CompressionType.Lz4,
LingerMs = 5, // 少量等待,换取更好的批量效果
MessageTimeoutMs = 30000
};
using var producer = new ProducerBuilder<string, string>(config).Build();
var result = await producer.ProduceAsync("order.order.created",
new Message<string, string>
{
Key = order.Id, // 同一订单进入同一分区
Value = JsonSerializer.Serialize(evt)
});
核心原则:处理成功之后再提交 Offset,也就是关闭自动提交,手动提交。
var config = new ConsumerConfig
{
BootstrapServers = "kafka1:9092,kafka2:9092,kafka3:9092",
GroupId = "inventory",
EnableAutoCommit = false,
AutoOffsetReset = AutoOffsetReset.Earliest
};
using var consumer = new ConsumerBuilder<string, string>(config).Build();
consumer.Subscribe("order.order.created");
while (!ct.IsCancellationRequested)
{
var cr = consumer.Consume(ct);
try
{
await handler.HandleAsync(cr.Message); // 内部包含幂等检查
consumer.Commit(cr); // 成功后再提交
}
catch (TransientException)
{
// 可重试错误:本地有限次重试 / 退避
}
catch (Exception ex)
{
await dlqProducer.ProduceAsync("order.order.created.dlq", cr.Message);
consumer.Commit(cr); // 进入死信后继续往前走
}
}
Node.js 项目里可以用 kafkajs 或 @confluentinc/kafka-javascript,思路完全一致。
重试策略建议分层:
.retry-5s、.retry-1m)做延迟重试,避免阻塞分区;max.in.flight.requests.per.connection(开启幂等时 ≤ 5 仍能保证有序);max.poll.interval.ms,被踢出组引发 Rebalance;retention.ms,需要长期保存的走归档(S3 + Connect);| 维度 | Kafka | RabbitMQ | RocketMQ | Pulsar |
|---|---|---|---|---|
| 定位 | 分布式事件流平台 | 传统消息代理 | 业务消息中间件 | 云原生流 / 消息平台 |
| 吞吐量 | 极高 | 中等 | 高 | 高 |
| 消息模型 | 分区日志,拉取 | 交换机 + 队列,推送为主 | Topic + Queue,拉取为主 | Topic + 订阅,多种订阅模式 |
| 消息保留 | 按时间 / 大小保留,可重放 | 消费后删除 | 可保留,可重放 | 可保留,分层存储 |
| 顺序性 | 分区内有序 | 单队列有序 | 队列内有序 | 分区内有序 |
| 延迟 / 定时消息 | 原生支持弱,需自行设计 | 插件 / 死信队列实现 | 原生支持较好 | 原生支持 |
| 事务消息 | 事务(Kafka 内部闭环) | 较弱 | 原生事务消息 | 支持事务 |
| 路由灵活性 | 较弱,主要靠 Topic 与 Key | 很强,多种 Exchange | 中等(Tag 过滤) | 中等 |
| 运维复杂度 | 中等(KRaft 后降低) | 较低 | 中等 | 较高(计算存储分离,组件多) |
| 生态 | 最丰富(Connect、Streams) | 成熟,协议多样 | 国内生态较好 | 持续成长 |
没有“最好”的消息中间件,只有“最适合当前场景”的。如果团队规模小、流量不大,只是想做个简单的异步任务队列,RabbitMQ 或云厂商的队列服务往往比自建 Kafka 更省心;一旦出现“多个系统要消费同一份数据”“需要回放历史”“数据量很大”这些信号,Kafka 的价值才会真正体现。
后续可以继续了解:Kafka 存储与索引机制、Rebalance 协议细节、Kafka Streams 与 Flink 的流处理实战、基于 Debezium 的 CDC 实践。