Kafka / MQ 核心原理:可靠性、幂等与积压治理
从分区副本、生产消费可靠性到业务幂等、事务边界和消息积压,系统理解 Kafka。
一、Kafka 的核心模型
Kafka 的逻辑层次是:
Topic
↓
Partition(有序追加日志)
↓
Replica(Leader + Followers)
生产者向分区 Leader 写入,消费者从分区拉取。消费者组内,一个分区同一时刻最多分配给一个消费者实例,因此:
消费组内的有效并行度受分区数上限约束。
不同消费者组各自维护消费进度,可以独立消费同一 Topic。
二、为什么 Kafka 吞吐高
- 顺序追加日志,减少随机 I/O;
- 批量发送和压缩,提高网络与磁盘利用率;
- 页缓存充分利用操作系统缓存;
- 分区允许横向并行;
- 拉取模型让消费者按自身能力批量消费;
- 网络协议和数据传输路径针对批处理优化。
吞吐高不代表单条消息一定延迟最低,也不代表可靠性配置可以省略。linger.ms、batch、压缩和 ACK 都是在延迟、吞吐与可靠性之间取舍。
三、生产端如何保证可靠
ACK
acks=0:不等待确认,吞吐高但可能静默丢失;acks=1:Leader 写入即确认,Leader 故障时未同步副本的数据可能丢失;acks=all:等待 ISR 中满足要求的副本确认,可靠性更高。
通常还应配合:
- 合理的
min.insync.replicas; - 禁止不受控的非同步副本选主;
- 发送失败重试和回调告警;
- 业务级超时与降级。
幂等生产者
幂等生产者使用 Producer ID、Epoch 和分区内序列号识别重试产生的重复写入,使单个生产者会话在单分区内的重试不会重复落日志。
它解决的是生产者到 Kafka 的重复写入,不等于解决消费者重复执行业务,也不等于跨系统 exactly-once。
四、Broker 如何保证副本可靠
每个分区由 Leader 接收读写,Follower 从 Leader 同步。ISR 表示当前与 Leader 保持同步的副本集合。
可靠性需要组合配置:
副本数 >= 3
acks = all
min.insync.replicas >= 2
合理的选主策略
如果只设置 acks=all,但 ISR 中只剩 Leader,一个副本确认仍可能满足要求;所以必须同时理解 min.insync.replicas。
副本复制通常是异步推进的,Kafka 在可用性、吞吐和数据安全之间做工程权衡,并非跨机房强一致数据库。
五、消费端为什么常见“至少一次”
典型安全顺序是:
拉取消息
↓
执行业务
↓
业务成功后提交 offset
如果业务成功后、offset 提交前进程崩溃,重启后会再次消费,因此是至少一次语义。
如果先提交 offset 再执行业务,进程随后崩溃,则消息可能永久跳过。因此关键业务通常选择“先处理再提交 + 业务幂等”。
Kafka 消息有没有业务 messageId
Kafka 原生定位一条日志记录的身份通常是:
topic + partition + offset
Kafka 不会自动为业务生成通用 messageId 或 eventId。如果需要跨 Topic、重试 Topic、数据库和日志统一追踪,应由业务在消息信封中生成:
{
"eventId": "uuid",
"eventType": "BILL_CONFIRMED",
"aggregateId": "bill-1001",
"occurredAt": "2026-07-16T10:00:00+08:00",
"payload": {}
}
六、消费幂等怎么做
常见方案:
唯一键或幂等表
以 eventId 或业务请求号建立唯一索引,在同一个数据库事务中记录消费和更新业务数据。重复消息触发唯一键冲突后返回已处理结果。
业务状态机
使用条件更新限制合法状态迁移:
UPDATE bill
SET status = 'PAID'
WHERE id = :id
AND status = 'PAYING';
重复执行时受影响行数为 0,不会重复推进。
Redis 去重
适合可容忍最终一致、丢失风险可控的非核心场景。资金和结算不能只依赖有 TTL 的 Redis Key 作为最终幂等依据。
七、Kafka 事务与 exactly-once 的边界
Kafka 事务可以把多分区写入和消费 offset 提交放进同一个 Kafka 事务,典型用于:
消费 Kafka A
↓
处理
↓
写入 Kafka B
+
提交 A 的 offset
下游使用 read_committed 时只读取已提交事务消息。
但这类 exactly-once 主要覆盖 Kafka 内部的“读—处理—写”。如果处理中还写 MySQL、调用支付接口或发送 HTTP 请求,就已经跨出 Kafka 事务边界,需要数据库幂等、Outbox、CDC 或业务补偿。
Kafka 事务也不要与 RocketMQ 的“事务消息/半消息”机制混为一谈,它们解决问题的路径不同。
八、数据库与 Kafka 如何保持一致
错误的直觉方案
更新数据库
→ 发送 Kafka
数据库提交后发送失败,会丢事件。反过来先发送再提交,数据库失败时又会出现无效事件。
Transactional Outbox
在同一个数据库事务中写业务表和 Outbox 表:
数据库事务:
更新业务数据
插入 Outbox 事件
↓
提交
↓
后台任务或 CDC 发布到 Kafka
发布过程可能重复,因此消费者仍要幂等。Outbox 提供的是可恢复的最终一致性,不是让数据库和 Kafka 共享一个全局事务。
CDC
通过 binlog 捕获数据库已提交变更并发布事件,可降低业务代码双写复杂度。需要治理表结构变化、事件语义、重放和重复投递。
九、顺序消息
Kafka 只保证单分区内有序。要保证同一订单、账户或账单的事件顺序,应使用稳定 Key 让它们进入同一分区:
key = accountId
全局有序通常意味着只使用一个分区,会牺牲并行度。多数业务真正需要的是聚合维度有序,而不是全局有序。
即使分区有序,也要处理重试、异步线程池和下游并行处理带来的业务乱序,可结合版本号和状态机拒绝旧事件。
十、重试与死信
Kafka Broker 原生保存日志,并不会自动替业务创建“重试队列”和“死信队列”。工程中通常创建普通 Topic:
business-topic
business-retry-1m
business-retry-10m
business-dlq
由应用或框架负责:
- 记录重试次数和下一次执行时间;
- 区分可重试异常与永久异常;
- 超过阈值进入 DLQ;
- 提供人工查看、修复和重放能力。
不要无限立即重试,否则会阻塞正常消息并放大下游故障。
十一、消息积压怎么处理
先判断瓶颈:
生产突然增加?
消费者实例不足?
分区数不足?
单条业务处理太慢?
数据库、缓存或远程接口变慢?
频繁 Rebalance?
毒消息反复失败?
治理步骤
- 看 Consumer Lag、消费速率、分区分布和失败率;
- 优化单条处理,使用批量查询、批量写和连接池;
- 在原消费者组内增加消费者,直到接近分区数;
- 并行度仍不足时评估扩分区,并检查顺序和 Key 分布;
- 把慢外部依赖隔离、限流或异步化;
- 将毒消息转入重试/DLQ,避免卡住主链路;
- 恢复后控制追赶速度,避免把数据库二次打垮。
不能简单创建一个临时消费者组“帮原组消费”,因为不同组拥有独立 offset;临时组读完不会推进原组的消费进度,还可能重复执行业务。真正协助应加入同一个消费者组,或设计受控的数据转移方案。
十二、Rebalance
消费者实例加入、退出,订阅变化或分区变化都可能触发 Rebalance。期间分区重新分配,可能出现短暂停顿。
治理方向包括:
- 避免消费线程被长时间阻塞导致心跳异常;
- 合理配置 poll 批次与最大处理间隔;
- 使用静态成员或增量协作式分配减少抖动;
- 把不可控慢调用移出核心 poll 循环,但要正确管理 offset 和背压。
十三、速记结论
生产可靠看 ACK、ISR 和幂等生产者;消费可靠看处理与 offset 的顺序;业务不重复靠幂等键和状态机;Kafka exactly-once 主要覆盖 Kafka 内部链路,跨数据库仍要 Outbox/CDC 和幂等;积压先找瓶颈,扩容必须尊重分区并行度和消费者组语义。
评论与讨论
回复 :
留下你的想法