Kafka系统设计与编码
🏠 README · ⬅ 02 调优与运维 · ➡ 04 源码与OS底层
- 包含章节:Ch13 业务场景设计 / Ch14 手撕题 / Ch15 加分项 & 反问
- 难度:⭐⭐ 资深岗分水岭
- 建议 :第 3 周 完成
- Ch13 准备 4~5 个白板可画的业务场景
- Ch14 手撕题必须动手敲熟,肌肉记忆
- Ch15 加分项 + 反问,面试前 1 天回顾
📑 本模块目录
- [13. 业务场景设计题](#13. 业务场景设计题 "#ch13") ⭐⭐
- [14. 手撕题 / 编码题](#14. 手撕题 / 编码题 "#ch14") ⭐⭐
- [15. 面试加分项 & 反问环节](#15. 面试加分项 & 反问环节 "#ch15") ⭐
模块导引
目标:从"会用 Kafka"上升到"能用 Kafka 解决业务难题"。资深岗 30~60 分钟系统设计题就考这一模块。
检验标准:
- 能在白板上画出"秒杀 / CDC / 延迟队列 / DLQ"四种典型场景的 Kafka 用法
- 现场写出可靠的 Producer/Consumer 包装类(含幂等、优雅停机、Rebalance Listener)
- 能针对"基于 Kafka 实现延迟队列"提出至少 2 种方案并讲清取舍
共 3 章(Ch13~Ch15)。重点是 Ch14 手撕题------很多公司会让你现场敲,必须熟练到肌肉记忆。
13. 业务场景设计题
本章定位 :资深岗、架构师面试的「白板题」核心。每题按 场景 → 架构图 → 关键决策 → 陷阱 → 完整代码 结构展开。
答题套路:先讲清需求(QPS / 一致性 / 延迟 SLA) → 画架构图 → 解释每个组件的选型理由 → 主动暴露 2~3 个陷阱并给出方案。
13.1 设计一个秒杀系统中的 Kafka 用法
13.1.1 业务需求
- 峰值 QPS:100 万 / 秒(如双 11 / 演唱会抢票)
- 库存数量:1 万件
- 订单不可超卖 + 不可重复下单 + 同一用户多次点击不重复扣款
- 响应时间:< 200ms
13.1.2 端到端架构
令牌桶限流 + 验证码] Gateway --> Front["前置校验服务
1) 用户黑名单
2) 请求合法性"] Front -->|放行 30%| Redis["(Redis 集群
预扣库存
Lua 原子脚本)"] Front -.->|拒绝| Reject1[返回 太挤了] Redis -->|stock--| Success[预扣成功 1%] Redis -.->|stock=0| Reject2[返回 已售罄] Success --> Kafka{{Kafka
seckill-topic
key=skuId}} Kafka --> C1[Consumer 1
校验 + 落单] Kafka --> C2[Consumer 2] Kafka --> Cn[Consumer N] C1 --> DB[(MySQL
订单表 + 唯一索引)] C2 --> DB Cn --> DB DB --> Notify[订单确认通知
Push / 短信] style User fill:#dbeafe style Kafka fill:#fee2e2 style Redis fill:#fef9c3 style DB fill:#dcfce7
13.1.3 关键决策点
层层削减流量:
erlang
1M QPS(用户)
↓ CDN + WAF:拦截爬虫和明显作弊(-50%)
500K QPS(合法用户)
↓ 网关令牌桶限流 + 验证码(-94%)
30K QPS(前置校验通过)
↓ Redis 预扣库存(仅 1 万件 → 99% 拒绝)
10K QPS(实际成功)
↓ Kafka 异步削峰
100~500 QPS(数据库写入)
Kafka 配置:
properties
# Producer
acks=all # 不丢
enable.idempotence=true # 不重
linger.ms=10 # 攒批
batch.size=131072 # 128KB
compression.type=lz4
max.in.flight.requests.per.connection=5
# Topic
seckill-topic
partitions=64 # 64 个 partition 支撑高并发
replication.factor=3
min.insync.replicas=2
# Producer 路由:key=skuId
# → 同一 SKU 顺序进同一 Partition
# → 避免库存争抢,便于业务侧顺序消费
消费者幂等:
sql
CREATE TABLE seckill_order (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL,
-- 业务幂等键:用户 + 活动 + 商品
UNIQUE KEY uniq_user_activity_sku (user_id, activity_id, sku_id),
created_at DATETIME
) ENGINE=InnoDB;
java
try {
orderMapper.insert(order);
} catch (DuplicateKeyException e) {
// 幂等命中,不算失败
log.info("Order already exists: {}", order.getId());
}
13.1.4 陷阱与方案
陷阱 1:Redis 预扣后服务崩溃 → 库存"假性占用"
→ 方案:Redis 设置 5 分钟 TTL;MySQL 超时未支付订单定时回补 Redis 库存。
陷阱 2:Kafka 写入失败 → Redis 已扣库存丢失
→ 方案:Producer 同步等 ack;失败则 Lua 脚本回滚 Redis 库存(两阶段补偿)。
陷阱 3:Hot Partition(爆款 SKU)
→ 方案:单个爆款 SKU 单独一个 Topic + 加大 Partition 数;或用 skuId + (userId % 10) 散列到 10 个 Partition,业务侧汇总。
13.2 基于 Kafka 实现延迟队列
13.2.1 业务需求
- 订单 30 分钟未支付自动取消
- 优惠券到期前 1 小时提醒
- 精度要求:秒级
- 支持任意延迟时长:从 1 秒到 30 天
13.2.2 三种方案对比
| 方案 | 实现 | 精度 | 复杂度 | 适用 |
|---|---|---|---|---|
| A. 多级 Topic 分桶 | 5s / 1min / 5min / 30min / 1h / 1d 多个 Topic | 桶级(最坏到 1h) | 低 | 业务可容忍粗粒度 |
| B. 单 Topic + 时间轮服务 | 自研 KafkaDelayService 中转 | 秒级 | 中 | 通用场景 |
| C. 换 RocketMQ 5.0 | 内置任意精度延迟 | 秒级 | 低 | 允许换技术栈 |
13.2.3 方案 B 详细设计(推荐)
带 deliverAt 时间戳] --> DT[delay-topic
所有延迟消息入口] DT --> DS[KafkaDelayService
消费者] DS --> TW[本地时间轮
HashedWheelTimer] TW --> Trigger[到期触发] Trigger --> TT{目标 Topic
order-event} TT --> CG[业务 Consumer Group] DS -.持久化.-> SS["状态存储
RocksDB(offset + 待触发任务)"] DS -.checkpoint.-> Meta[(MySQL
关键消息备份)] style DT fill:#dbeafe style TW fill:#fef9c3 style TT fill:#fee2e2
消息格式:
java
public class DelayMessage {
String targetTopic; // 到期后投递的目标 Topic
long deliverAtMs; // 到期时间戳(绝对时间)
String key; // 业务 key(路由用)
byte[] payload; // 业务消息体
Map<String, String> headers; // 透传 header
}
时间轮服务核心逻辑:
java
public class KafkaDelayService {
private final HashedWheelTimer timer = new HashedWheelTimer(1, TimeUnit.SECONDS, 512);
private final KafkaProducer<String, byte[]> producer;
private final KafkaConsumer<String, DelayMessage> consumer;
public void run() {
consumer.subscribe(List.of("delay-topic"));
while (running) {
ConsumerRecords<String, DelayMessage> records = consumer.poll(Duration.ofMillis(500));
for (ConsumerRecord<String, DelayMessage> r : records) {
DelayMessage msg = r.value();
long delay = msg.deliverAtMs - System.currentTimeMillis();
if (delay <= 0) {
// 已过期,立即投递
forward(msg);
} else if (delay > MAX_DELAY) {
// 超长延迟分级处理(避免内存堆积)
putToColdStorage(msg);
} else {
timer.newTimeout(t -> forward(msg), delay, TimeUnit.MILLISECONDS);
}
}
consumer.commitSync();
}
}
private void forward(DelayMessage msg) {
producer.send(new ProducerRecord<>(msg.targetTopic, msg.key, msg.payload));
}
}
13.2.4 关键陷阱
陷阱 1:服务重启 → 时间轮内存丢失
→ 方案:消息先 commit 到 delay-topic(持久化),重启后从 earliest 重新消费 + 重建时间轮。
陷阱 2:超长延迟(30 天)占内存
→ 方案:分级时间轮 + 冷热分离:
- < 1h:内存时间轮
- 1h ~ 24h:RocksDB 落盘
-
24h:写到独立的 long-delay-topic + 定时调度器
陷阱 3:服务多实例水平扩展,消息分配不均
→ 方案:delay-topic 多分区 + 消费者按 partition 分配。每个实例只处理自己负责的 partition。
陷阱 4:高峰期投递放大流量
→ 方案:消费 delay-topic 时主动限流,避免到期消息瞬间打爆下游。
13.3 数据库 CDC 接 Kafka 实时数仓(高频)
13.3.1 业务需求
- MySQL 业务库变更实时同步到数据湖 / OLAP
- 端到端延迟 < 30 秒
- 保证 update / delete 顺序
- 支持 Schema 演进
- 不丢不重
13.3.2 完整架构
读 binlog] end CD1 -->|每表一 Topic
key=主键| K1{{"Kafka
cdc.order_db.orders
cdc.order_db.users"}} K1 --> SR[Schema Registry
Avro/Protobuf 演进] K1 --> Flink[Flink Streaming
清洗 / 关联 / 聚合] Flink --> ODS[(ODS 层
Iceberg / Hudi)] Flink --> DWD[(DWD 明细层)] Flink --> DWS[(DWS 汇总层)] DWS --> Doris[(Doris / StarRocks
实时查询)] DWS --> ES[(ElasticSearch
全文检索)] DWS --> Tableau[BI 报表] style CD1 fill:#fee2e2 style K1 fill:#dbeafe style Flink fill:#fef9c3
13.3.3 核心配置
Topic 设计:
css
按表分 Topic(推荐):
cdc.order_db.orders ← 主键 key 路由
cdc.order_db.users
cdc.order_db.order_items
按库分 Topic(备选):
cdc.order_db ← table 在 header 里
→ 优势:Topic 少;劣势:Schema 多样难处理
Producer 配置(Debezium):
properties
# 同主键路由同分区 → 保证 UPDATE/DELETE 顺序
key.converter=io.confluent.connect.avro.AvroConverter
key.converter.schema.registry.url=http://schema-registry:8081
# 用 Compact Topic 保留每行最新状态(变成流表)
log.cleanup.policy=compact
# Tombstone 处理(删除标记)
delete.handling.mode=rewrite
# 大事务保护
max.batch.size=2048
producer.delivery.timeout.ms=300000
13.3.4 端到端 EOS 实现
(Kafka Consumer) participant Op as Flink 算子
(状态计算) participant Sink as Flink Sink
(Kafka Producer) participant Kafka Source->>Op: 读取消息
(offset 在 barrier 中) Op->>Op: 处理 + 更新状态 Op->>Sink: 输出 Note over Source,Sink: ============ Checkpoint Barrier ============ Source->>Op: barrier Op->>Sink: barrier Sink->>Kafka: producer.flush()
事务 PREPARE Op->>Op: 状态快照到远端存储 Note over Source,Sink: ============ Checkpoint Complete ============ Sink->>Kafka: producer.commitTransaction()
写入 COMMIT Marker Note over Kafka: read_committed Consumer 才能读到 Note over Source,Sink: ============ 故障恢复 ============ Source->>Source: 从 checkpoint 恢复 offset Sink->>Kafka: 老事务自动 abort(Producer Epoch fence)
13.3.5 陷阱与方案
陷阱 1:Schema 演进破坏下游
→ Schema Registry + 兼容性策略(推荐 BACKWARD):
- BACKWARD :新 schema 能读老消息(只允许加可选字段 / 删字段)。
- FORWARD:老 schema 能读新消息(只允许加字段 / 删可选字段)。
- FULL:双向兼容。
陷阱 2:DDL 变更(加列、改类型)后下游 Flink 报错
→ 方案:DDL 变更前先升级 Sink Schema → 数据库变更 → Source 自动感知。或者用 Schema-On-Read 的格式(Iceberg / Delta Lake)。
陷阱 3:MySQL 大事务(10 万行)→ Kafka 单分区瞬时压力
→ 方案:Debezium 配置 max.batch.size 拆批 + 分区数充足。
陷阱 4:DELETE 后又 INSERT 同主键 → 顺序错乱
→ 用主键 hash 路由保证同 partition;Compact Topic 自动处理。
13.4 死信与重试机制完整设计
13.4.1 业务需求
- 消费失败时有限次重试,避免永远卡住
- 不同失败类型采用不同重试策略
- 反复失败的消息进入死信队列,人工介入
- 死信可视化 + 重投能力
13.4.2 重试 Topic 链架构
主 Topic] M --> CM[主消费者] CM -->|失败| R1[order-event.retry.30s] R1 -->|消费时延迟到时刻| CM CM -->|再失败| R2[order-event.retry.5min] R2 -->|延迟| CM CM -->|继续失败| R3[order-event.retry.30min] R3 --> CM CM -->|超过 N 次| DLQ[order-event.DLQ] DLQ --> Admin[运营管理后台
查看 / 重投 / 删除] DLQ --> Alert[告警 + 人工介入] style M fill:#dbeafe style R1 fill:#fef9c3 style R2 fill:#fef9c3 style R3 fill:#fef9c3 style DLQ fill:#fee2e2
13.4.3 实现关键
消息 Header 标记:
ini
Header keys:
retry.count = 3 # 当前重试次数
retry.original-topic = order-event # 原始 Topic
retry.first-failed-at = 2026-05-07T10:00:00.000Z # 首次失败时间
retry.last-error = "DB connection timeout" # 最近一次错误
消费者处理逻辑:
java
@KafkaListener(topics = "order-event", groupId = "order-svc")
public void handleOrder(ConsumerRecord<String, Order> record, Acknowledgment ack) {
try {
orderService.process(record.value());
ack.acknowledge();
} catch (Exception e) {
Headers headers = record.headers();
int retryCount = getRetryCount(headers);
if (retryCount >= MAX_RETRIES) {
sendToDLQ(record, e);
} else {
sendToRetryTopic(record, retryCount + 1, e);
}
ack.acknowledge(); // 主 Topic 消费完毕
}
}
private void sendToRetryTopic(ConsumerRecord<String, Order> record, int newCount, Exception e) {
String retryTopic = pickRetryTopic(newCount); // 30s / 5m / 30m
Headers newHeaders = record.headers().add("retry.count", String.valueOf(newCount).getBytes())
.add("retry.last-error", e.getMessage().getBytes());
producer.send(new ProducerRecord<>(retryTopic, null, record.timestamp(), record.key(), record.value(), newHeaders));
}
重试 Topic 消费者(带延迟):
java
@KafkaListener(topics = {"order-event.retry.30s", "order-event.retry.5min", "order-event.retry.30min"})
public void handleRetry(ConsumerRecord<String, Order> record, Acknowledgment ack) {
// 延迟到 deliverAt 才处理
long deliverAt = record.timestamp() + getDelayMs(record.topic());
long sleepMs = deliverAt - System.currentTimeMillis();
if (sleepMs > 0) {
Thread.sleep(sleepMs);
}
// 重新触发主消费者逻辑
handleOrder(record, ack);
}
13.4.4 DLQ 运营平台
最低实现:
sql
CREATE TABLE dead_letter (
id BIGSERIAL PRIMARY KEY,
topic VARCHAR(128),
partition INT,
offset BIGINT,
key VARCHAR(256),
payload TEXT,
headers JSONB,
last_error TEXT,
created_at TIMESTAMP,
handled_at TIMESTAMP NULL,
handled_by VARCHAR(64) NULL,
handle_action VARCHAR(32) NULL -- REPLAY / DELETE / IGNORE
);
DLQ 消费者把消息写入 DB → 运营后台 GUI 操作 → REPLAY 时重新投回主 Topic。
13.4.5 陷阱
- 重试导致顺序错乱 :失败消息延迟后投递,新消息已处理 → 业务必须幂等或接受最终一致。
- 重试 Topic 消费 sleep 占用 Consumer 线程 :用 Spring Kafka 的
RetryableTopic或自研异步定时器。 - DLQ 黑洞 :无人值守的 DLQ 等于丢消息,必须告警 + 自动监控 DLQ Lag。
13.5 跨地域数据同步(异地多活前置)
13.5.1 业务需求
- 业务部署在两个机房(杭州 + 上海)
- 任一机房故障,业务不中断
- 数据双向同步,最大可容忍 RPO < 1 分钟
13.5.2 方案对比
| 方案 | 同步方向 | 延迟 | 冲突处理 | 适用 |
|---|---|---|---|---|
| MM2 单向 | 主 → 备 | 秒级 | 无(备只读) | 灾备 |
| MM2 双向 + 防回环 | 双向 | 秒级 | 业务侧幂等 | 双活 |
| Stretched Cluster | 同集群跨机房 | RPO=0 | 无(强一致) | 同城双活 |
| 业务侧路由 + Kafka 单元化 | 用户分流 | 秒级 | 单元内闭环 | 蚂蚁体系 |
13.5.3 MM2 双向同步详细配置
topic: order-event}} P1[Producer HZ] --> K1 K1 --> C1[Consumer HZ] end subgraph DC2["上海机房"] K2{{Kafka SH
topic: order-event}} P2[Producer SH] --> K2 K2 --> C2[Consumer SH] end K1 -.MM2 单向.-> SH1["topic: HZ.order-event
(避免回环)"] K2 -.MM2 单向.-> HZ1["topic: SH.order-event
(避免回环)"] SH1 -.consumer.-> C2 HZ1 -.consumer.-> C1 style K1 fill:#dbeafe style K2 fill:#fee2e2
MM2 配置要点:
properties
# MM2 connector
clusters=HZ,SH
HZ.bootstrap.servers=kafka-hz:9092
SH.bootstrap.servers=kafka-sh:9092
# 双向同步
HZ->SH.enabled=true
SH->HZ.enabled=true
# 防回环:同步过来的 Topic 加前缀,本地 Consumer 同时订阅 order-event 和 SH.order-event
replication.policy.class=org.apache.kafka.connect.mirror.DefaultReplicationPolicy
# 同步 ACL / Quota / __consumer_offsets
emit.heartbeats.enabled=true
sync.topic.acls.enabled=true
sync.group.offsets.enabled=true
# 限速
HZ->SH.replication.factor=3
HZ->SH.tasks.max=8
HZ->SH.replication.policy.separator=.
13.5.4 陷阱
- 跨机房 RTT 高 → acks=all 性能下降 :本地写本地集群,跨机房异步 MM2,业务不要直连远端 Kafka。
- 回环检测:MM2 用 cluster alias 前缀(HZ.xxx)防回环;自研同步必须维护源 cluster ID 的 header。
- Consumer offset 不一致:MM2 维护映射表(每条消息源 offset → 目标 offset),切换时按映射表 reset。
- Schema Registry 也要双活:Confluent 的 Schema Linking 或自研同步。
13.6 消息广播(Pub/Sub Fanout)的多种实现
13.6.1 场景
业务变更需要通知多个不同业务系统,比如订单创建后:
- 财务系统:记账
- 风控系统:实时分析
- 推送系统:消息通知用户
- 数据仓库:实时入仓
13.6.2 三种实现对比
| 方案 | 实现 | 优点 | 缺点 |
|---|---|---|---|
| A. 单 Topic + 多 Consumer Group | 每个下游一个独立 group | 简单,Kafka 原生支持 | Topic 写流量与读流量不对称 |
| B. 一个主 Topic + Fanout 服务转发 | 主 Topic + 业务 Topic | 业务下游解耦 | 多一次转发延迟 |
| C. 主 Topic + Connect Sink | Kafka Connect 推到 ES/HDFS/MySQL | 不需要业务消费者 | 灵活性差 |
13.6.3 推荐方案:单 Topic + 多 Consumer Group
财务系统] T --> G2[Consumer Group
风控系统] T --> G3[Consumer Group
推送系统] T --> G4[Consumer Group
数仓 ETL] style T fill:#dbeafe style G1 fill:#dcfce7 style G2 fill:#fef9c3 style G3 fill:#fee2e2 style G4 fill:#f3e8ff
关键点:
- Topic 设计:业务事件 型,不是命令型(
OrderCreated而不是SendNotification)。 - 每个下游有独立 Group → offset 独立 → 互不影响。
- 部分下游有"过滤"需求(只关心 VIP 订单)→ 业务侧消费时过滤即可。
13.6.4 容量估算
bash
入流量:1 GB/s
出流量:1 GB/s × 4 (4 个下游) = 4 GB/s
+ 副本同步:1 GB/s × (R-1) = 2 GB/s
总出流量:6 GB/s = 48 Gbps
→ 必须 25Gbps × 2 网卡 bond
→ 多个下游消费时网络是瓶颈,不是磁盘
13.7 高频追问
Q:Kafka 能不能做请求-响应模式(RPC)?
A:理论可以但不推荐。常见做法:
- Producer 发请求消息,Header 带 correlationId + replyTopic。
- Consumer 处理后,把响应发到 replyTopic(或 caller 订阅的临时 Topic)。
- Producer 端订阅 replyTopic,按 correlationId 匹配响应。
缺点:
- 延迟高(一次往返 + Kafka 写盘)。
- 资源浪费(Topic / Consumer 持续运行)。
- 失败处理复杂(超时、重试、幂等)。
正确选择:
- 同步 RPC → gRPC / Dubbo / HSF。
- 异步通知 → Kafka 单向消息 + Webhook。
Q:Kafka 适合做"任务队列"吗?
A:不适合做精细任务调度。原因:
- 消费并发受 Partition 数限制,无法动态扩缩。
- 失败重试机制简陋,需要业务自己造轮子。
- 没有任务优先级。
应该用:
- Celery(Python)/ XXL-Job / ElasticJob → 任务调度。
- RocketMQ 顺序消息 + 重试机制 → 业务消息任务。
Kafka 适合的是 "事件流" 场景:日志、CDC、流处理上游。
Q:你们公司 Kafka Topic 命名规范是怎样的?
A(参考阿里 / 美团 / 字节通用规范):
vbnet
{environment}.{domain}.{event-type}.{version}
例:
prod.order.created.v1
prod.payment.refunded.v1
prod.user.profile-updated.v2
test.cdc.mysql.order_db.orders
要点:
- 环境前缀(prod/test/staging)--- 物理集群往往不同。
- 业务域分组(order / payment / user)--- 便于权限管理。
- 事件名(过去式)--- 表示已发生事实。
- 版本号(v1/v2)--- Schema 演进时切新 Topic(避免 in-place 兼容性破坏)。
14. 手撕题 / 编码题
14.1 写一个可靠的 Producer 包装类
java
public class ReliableKafkaProducer<K, V> implements Closeable {
private final KafkaProducer<K, V> producer;
public ReliableKafkaProducer(Map<String, Object> userProps) {
Map<String, Object> props = new HashMap<>(userProps);
props.put(ProducerConfig.ACKS_CONFIG, "all");
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5);
props.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, 120_000);
props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4");
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 128 * 1024);
props.put(ProducerConfig.LINGER_MS_CONFIG, 20);
this.producer = new KafkaProducer<>(props);
}
public CompletableFuture<RecordMetadata> sendAsync(ProducerRecord<K, V> record) {
CompletableFuture<RecordMetadata> future = new CompletableFuture<>();
producer.send(record, (md, ex) -> {
if (ex != null) future.completeExceptionally(ex);
else future.complete(md);
});
return future;
}
@Override public void close() { producer.flush(); producer.close(Duration.ofSeconds(30)); }
}
14.2 写一个支持优雅停机 + 手动提交 + 业务线程池的 Consumer
java
public class ManagedConsumer implements Runnable, Closeable {
private final KafkaConsumer<String, String> consumer;
private final ExecutorService workerPool = Executors.newFixedThreadPool(16);
private final Map<TopicPartition, OffsetAndMetadata> pendingOffsets = new ConcurrentHashMap<>();
private volatile boolean running = true;
public ManagedConsumer(String groupId, List<String> topics) {
Properties p = new Properties();
p.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka:9092");
p.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
p.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
p.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 200);
p.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 600_000);
p.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
CooperativeStickyAssignor.class.getName());
p.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
p.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
this.consumer = new KafkaConsumer<>(p);
consumer.subscribe(topics, new RebalanceListener());
}
@Override
public void run() {
try {
while (running) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
if (records.isEmpty()) { commitIfNeeded(); continue; }
CountDownLatch latch = new CountDownLatch(records.count());
for (ConsumerRecord<String, String> r : records) {
workerPool.submit(() -> {
try { handle(r); }
finally {
pendingOffsets.merge(
new TopicPartition(r.topic(), r.partition()),
new OffsetAndMetadata(r.offset() + 1),
(oldV, newV) -> newV.offset() > oldV.offset() ? newV : oldV);
latch.countDown();
}
});
}
latch.await();
commitIfNeeded();
}
} catch (WakeupException ignored) {
} catch (Exception e) {
// log & alarm
} finally {
try { consumer.commitSync(pendingOffsets); } catch (Exception ignored) {}
consumer.close();
}
}
private void commitIfNeeded() {
if (pendingOffsets.isEmpty()) return;
Map<TopicPartition, OffsetAndMetadata> snapshot = new HashMap<>(pendingOffsets);
consumer.commitAsync(snapshot, (m, e) -> {
if (e == null) snapshot.forEach((k, v) -> pendingOffsets.remove(k, v));
});
}
private void handle(ConsumerRecord<String, String> r) { /* 业务处理(必须幂等) */ }
private class RebalanceListener implements ConsumerRebalanceListener {
@Override public void onPartitionsRevoked(Collection<TopicPartition> parts) {
try { consumer.commitSync(pendingOffsets); } catch (Exception ignored) {}
parts.forEach(pendingOffsets::remove);
}
@Override public void onPartitionsAssigned(Collection<TopicPartition> parts) { }
}
@Override public void close() {
running = false;
consumer.wakeup();
workerPool.shutdown();
}
}
加分点:
- 引入 per-partition 单线程(按 partition 路由到固定 worker)保证局部顺序。
- onPartitionsRevoked 中 commit 已完成 offset 防止丢失/重复。
14.3 写一个本地内存的批量幂等组件
java
public class IdempotentExecutor {
private final Cache<String, Boolean> seen = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1_000_000).build();
public boolean executeOnce(String bizId, Runnable task) {
return seen.get(bizId, k -> { task.run(); return true; });
}
}
注意:本地内存只适合短时窗内防重,最终幂等仍要靠 DB 唯一索引。
15. 面试加分项 & 反问环节
本章定位 :给同样答得不错的候选人之间制造差异化优势 。资深岗终面或交叉面,会不会反问 / 会不会延伸是否有体系,往往是定级的依据。
15.1 技术加分项 ── 拉开普通候选人的差距
15.1.1 体系性表达(必备)
| 能力 | 具体表现 | 含金量 |
|---|---|---|
| 能徒手画架构图 | 在白板上 5 分钟画出 Producer → 3 副本 → Consumer Group → Coordinator 全链路 | ⭐⭐⭐ |
| 能源码定位 | "这个错误对应 Sender#completeBatch 中的 OutOfOrderSequenceException" |
⭐⭐⭐ |
| 能讲清协议流程 | "事务 commit 是 2PC:先 PREPARE_COMMIT 写 __transaction_state,再 WriteTxnMarker 写各分区" |
⭐⭐⭐ |
| 能给出量化数据 | "我们集群 8 broker,日均 100 亿消息,P99 写入 25ms,PageCache 命中率 96%" | ⭐⭐⭐ |
| 能对比技术方案 | "ISR 是 PacificA 协议变种,与 Raft Quorum 的核心区别是 N 副本全同步 vs 多数派" | ⭐⭐⭐ |
15.1.2 深度技术点(高频被追问)
15.1.3 数据驱动的项目讲法(金句模板)
不要说:"我们用 Kafka 做了订单消息处理"
推荐说法(先数据、再三条可验证动作):
我们订单中心每天处理约 12 亿消息,峰值约 5 万 QPS;Kafka 集群 8 个 broker,单集群日均约 200TB 流量。 核心优化我做了三块:一是把
acks=1改成all并配上min.insync.replicas=2、Producer 幂等与业务唯一键,把丢失率从约 1/10 万压到 0 ;二是引入 Cooperative Sticky Assignor ,把日均 Rebalance 从 50+ 降到 5 次以内 ;三是完成 KRaft 后,Controller 切换从分钟级降到秒级。
讲述模板(STAR + 数据):
diff
S (Situation): 数据规模 + 业务背景
T (Task): 具体责任 / 难点
A (Action): 做了什么(参数 / 架构 / 工具)
R (Result): 量化指标提升(百分比、绝对值、SLA)
强化:
+ 失败案例(曾经踩坑后修复)
+ 业务影响(带来什么业务价值)
+ 持续改进(后续还有什么计划)
15.1.4 跨领域知识(资深岗 +)
- JVM:能讲 G1 vs ZGC vs Shenandoah 的取舍(参考 Ch9.4),关键是"Kafka 不需要 ZGC"。
- Linux 内核:PageCache、epoll、TCP 调优。
- 网络:ENI / RDMA / kTLS / TCP_NODELAY 与 Kafka 的关系。
- 数据库 / 数据湖:CDC / Iceberg / Hudi / Doris / StarRocks 与 Kafka 的协同。
- 流处理:Flink Checkpoint / Watermark / 反压如何影响 Kafka 消费。
- 可观测性:OpenTelemetry / 链路追踪 traceparent header 透传到 Kafka。
15.1.5 行业前沿(紧跟社区)
| 主题 | 关键词 |
|---|---|
| KIP-405 Tiered Storage | 冷热分层、S3 卸载 |
| KIP-848 Next-Gen Consumer | 服务端 rebalance、弃用 Eager 协议 |
| KIP-500 KRaft 模式 GA | 4.0 完全去 ZK |
| KIP-996 Pre-vote | 减少 KRaft 选举抖动 |
| WarpStream / Bufstream | 基于 S3 的 0 本地盘 Kafka 替代品 |
| Confluent KORA | Kafka 云原生重写引擎 |
| AutoMQ | 国内云原生 Kafka 替代 |
主动谈一两个最新动向(比如 WarpStream 基于 S3 的 0 本地盘架构、AutoMQ 在阿里云的实践),能立刻让面试官感受到候选人还在持续学习。
15.2 软实力加分项
15.2.1 面试官评判候选人的隐性维度
| 维度 | 优秀表现 | 反例 |
|---|---|---|
| 结构化思维 | 答题先讲背景 → 选项 → 决策 → 结果 | 想到啥讲啥,跳跃 |
| 诚实 | 不会的就说不会,承认局限 | 编造或瞎猜 |
| 沟通 | 5 分钟内讲清复杂概念 | 啰嗦或不抓重点 |
| 学习能力 | 主动谈最近读的 KIP / 论文 | 一年没看技术 |
| 业务理解 | 能讲技术决策对业务的影响 | 只关心技术指标 |
| 协作 | 提到与产品 / 测试 / 运维的协作 | 只讲个人英雄主义 |
15.2.2 高情商应答模板
面对不会的题:
❌ "这个我不太了解......"
✅ "这个具体细节我不确定,但根据 XXX 原理推测应该是 YYY,方便的话我面试完查证后回复邮件给您。"
面对失败案例:
❌ "其实那次故障不是我的锅。"
✅ "那次故障我作为主要排查人,第一时间确认了影响范围(XXX 业务受影响 XXX 时长),止血后做了根因分析(XXX),最终我们的改进是 XXX,现在线上运行 XXX 没再复发。"
面对压力问:
❌ "我也是这么想的,但我们老大说这样做......"
✅ "这个权衡当时确实有讨论,从 ABC 三个维度看 XXX 方案更合适,但我也很理解您说的考虑点。如果让我重新设计,我会再加 XXX 来兼顾。"
15.3 反问环节 ── 你问面试官的问题
15.3.1 反问的战略意义
反问的目的:
- 判断公司 / 团队是否值得加入;
- 体现技术深度(好问题胜过好答案);
- 转换攻防(让面试官回答你,建立对话感);
- 收集信息为后续面试 / 谈薪做准备。
避坑:
- ❌ 不要问公开资料能查到的东西("贵公司主营业务是什么")。
- ❌ 不要问很功利的("几号发工资?")--- 留到 HR 面或 offer 谈判。
- ❌ 不要问你已经清楚不会满意的(除非测试面试官真诚度)。
15.3.2 推荐反问清单(按面试阶段分)
一面(技术/直接经理)
具体问题示例:
- "贵团队的 Kafka 集群规模大概是多少?日均消息量在什么量级?"
- "目前在用 ZK 还是 KRaft?如果还在 ZK,是否有升级计划?"
- "团队最近半年最大的一次稳定性挑战是什么?怎么解决的?"
- "Kafka 平台化做到什么程度了?是否有自助申请、容量评估、自动监控?"
- "团队对 Kafka 的二次开发深度如何?是直接用社区版还是有内部 fork?"
二面(部门 leader / 架构师)
具体问题示例:
- "团队对 Kafka 的未来规划,是更倾向跟随社区,还是有内部演进路线?"
- "整个数据链路(Kafka → Flink → 数据湖)的端到端 SLA 是怎么定的?"
- "您觉得团队当前最缺哪种能力?我加入后如何补足?"
- "未来 1~2 年团队最大的技术挑战是什么?"
三面(HRBP / 大老板 / 跨部门)
- "公司对 Kafka 这类基础设施团队的投入和定位是什么?"
- "如果我加入,前 3 个月、6 个月、1 年的合理预期是什么?"
- "您对一个资深工程师区别于普通工程师的核心要求是什么?"
HR 面
- "贵司晋升周期一般多久?跨部门转岗是否容易?"
- "团队的工作节奏 / 加班情况 / 弹性工作?"
- "员工的成长支持(培训、外部会议、开源贡献)?"
15.3.3 高质量反问示例(按场景)
场景 A:你想去做技术深度 → 问反映技术含量的题
"我看您们公司在用 KRaft 模式了。能聊聊从 ZK 迁移到 KRaft 的实践吗?特别是迁移过程中遇到的最大坑?"
效果:体现你了解技术演进 + 关心实战经验。
场景 B:你想去做业务影响 → 问业务相关题
"我注意到您们最近在做实时风控。从消息系统的视角看,这个业务对 Kafka 的延迟、丢失、有序性的诉求是什么?团队是怎么权衡的?"
效果:体现你能从业务反推技术。
场景 C:你担心团队稳定性 → 问"故障"
"团队最近半年最严重的一次 Kafka 故障是什么?事后是怎么复盘的?有没有沉淀文档?"
效果:好的团队会大方分享 + 体现"复盘文化"。
场景 D:你担心是否被压榨 → 问值班 / 节奏
"团队的值班机制是怎样的?月度 / 季度的节奏是什么样?"
效果:直接但合理,HR / 直接经理都该回答。
15.4 一句话压箱底(资深面试金句库)
把这些金句背熟、改成自己的口吻,面试时随手丢出立刻提级:
关于设计
- "Kafka 是为吞吐而生,RocketMQ 是为业务消息而生。"
- "PageCache 是 Kafka 高性能的灵魂,堆内存只是配菜。"
- "ISR 不是 Raft,是 PacificA 协议的变种 ── 全副本同步换更少的副本数。"
- "acks=all + min.insync.replicas=2 + 副本 3,是金融级不丢的最低标配。"
- "Kafka 的 EOS = 至少一次 + 业务幂等,所有 MQ 本质都这样。"
关于权衡
- "Partition 数不是越多越好 ── 单 Broker 超过 4000 就开始受 Controller 选举与 PageCache 命中率影响。"
- "
acks=all + min.insync.replicas=1等价于acks=1,这是最隐蔽的丢消息陷阱。" - "为什么不用 ZGC?因为 Kafka 堆只有 6~8GB,G1 已经够用,ZGC 反而要付读屏障开销。"
- "Kafka 不主动 fsync 不是 bug 是 feature,依赖多副本而非单机磁盘是它快的根本。"
- "分区数能加不能减,是 Kafka 设计的原罪之一。"
关于运维
- "网络往往先于磁盘成为瓶颈,3 副本入 1GB 出可能 5GB。"
- "reassign-partitions 不带 throttle 等于自杀,限速是必备而不是可选。"
- "监控 Lag 一个指标是不够的,要监控端到端时延 + 业务正确性。"
- "Rebalance 风暴的根因往往是 GC,不是 Kafka 本身。"
关于业务
- "幂等是消息系统的最后一道防线,业务写入必须有唯一键。"
- "消息回溯是 Kafka 相比传统 MQ 最强的能力,要善用而不是怕。"
- "Outbox 模式是解决'DB 提交了消息没发'的标准答案。"
🧭 章节导航
⬅️ 上一模块:02-调优与运维.md | ➡️ 下一模块:04-源码与OS底层.md
| 模块 | 文件 |
|---|---|
| 🔵 模块一·基础原理 | 01-基础与原理.md |
| 🟢 模块二·调优运维 | 02-调优与运维.md |
| 🟡 模块三·设计编码(当前) | 03-设计与编码.md |
| 🔴 模块四·源码 OS | 04-源码与OS底层.md |
| 🟣 模块五·分布式理论 | 05-分布式理论与大厂设计.md |
| 📎 附录 | 99-附录.md |