综合练习
订单事件 Topic + Spring 消费端。
综合练习
这一课你将能做到
- 独立搭起「API 写 MySQL → 发
OrderCreated → 双 Consumer Group 各自处理」的完整 lab 链路
- 为通知与审计两条消费链路实现幂等(去重表/唯一索引),并用 CLI 验证 lag 回落
- 用 README 写清本站边界(生产主路径 MySQL+Redis,Kafka 为实验),并逐项完成验收清单
为什么要学
前面 11 课每个都是零件:概念、安装、生产、消费、Spring、格式、投递、Streams、监控、选型。零件各自能跑 ≠ 系统能跑——真实工作里你交付的从来不是「会用某个 API」,而是一条能演示、能抗重启、能说清边界的链路。
本课做一个结营作业:完整的订单事件实验。做完它,你就有了能写进简历的实战叙述:「我用 Spring Boot + Kafka 搭过订单事件链路,双消费组订阅、去重表幂等、CLI 验证过 lag」——这句话的每个词你都亲手敲过。
不要求接入生产 API;独立 lab 表 + 本地 Kafka 即可,大胆试错。
人话:结营作业——能跑通、能讲清、能写边界,才算入门闭环。
先建立图像
POST /lab/orders
└─ @Transactional 写 lab_orders(权威)
└─ afterCommit → KafkaTemplate.send
Topic: coderoadmap.order-events(3 分区,key=orderId)
│
┌───────┴────────┐
▼ ▼
group notify group audit
记通知日志 INSERT lab_audit_log
(幂等) (UNIQUE event_id)
│ │
└────────┬───────┘
▼
kafka-consumer-groups --describe → 两组 LAG ≈ 0
| 元素 | 值 |
|---|
| Topic | coderoadmap.order-events |
| 事件 | OrderCreated(必做)、OrderPaid(选做) |
| Key | orderId |
| 消费组 | coderoadmap-notify、coderoadmap-audit |
| 权威库 | lab 自己的 lab_orders 表(非生产 cf_*) |
跟着做
步骤 1:起 Broker 与 Topic
docker compose up -d
docker exec kafka-lab /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic coderoadmap.order-events --partitions 3 --replication-factor 1
docker exec kafka-lab /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic coderoadmap.order-events
应看到 PartitionCount: 3。
步骤 2:建表
-- 订单真相(lab)
CREATE TABLE lab_orders (
id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
amount BIGINT NOT NULL, -- 金额存分,不用浮点(消息格式课的纪律)
status VARCHAR(32) NOT NULL,
created_at TIMESTAMP NOT NULL
);
-- 审计投影:event_id 唯一索引 = 幂等闸门
CREATE TABLE lab_audit_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
event_id VARCHAR(64) NOT NULL UNIQUE,
order_id VARCHAR(64) NOT NULL,
payload JSON NOT NULL,
created_at TIMESTAMP NOT NULL
);
-- 通知侧去重表
CREATE TABLE lab_processed_events (
event_id VARCHAR(64) PRIMARY KEY,
processor VARCHAR(32) NOT NULL,
processed_at TIMESTAMP NOT NULL
);
步骤 3:API + 事务后发送
@RestController
@RequiredArgsConstructor
public class LabOrderController {
private final LabOrderService orderService;
private final OrderEventPublisher publisher;
private final ObjectMapper objectMapper;
@PostMapping("/lab/orders")
public LabOrder create(@RequestBody CreateLabOrderCmd cmd) throws Exception {
LabOrder order = orderService.create(cmd); // @Transactional 写 lab_orders
OrderCreatedEvent evt = OrderCreatedEvent.from(order);
publisher.publishCreated(evt.orderId(), objectMapper.writeValueAsString(evt));
return order;
}
}
// 事务后发送(生产者课 + Spring 课的合并纪律)
@Service
@RequiredArgsConstructor
public class OrderEventPublisher {
private final KafkaTemplate<String, String> kafkaTemplate;
public void publishCreated(String orderId, String json) {
kafkaTemplate.send("coderoadmap.order-events", orderId, json)
.whenComplete((r, ex) -> {
if (ex != null) { /* 记日志/告警,进补偿 */ }
});
}
}
LabOrderService.create 内部用 TransactionSynchronizationManager.registerSynchronization 的 afterCommit 调 publisher,或直接在 Controller 层「service 事务方法返回后再发」——两种都行,但必须写注释说明选的是哪种、风险窗口是什么。
步骤 4:双监听 + 各自幂等
@Component
@RequiredArgsConstructor
public class LabOrderListeners {
private final NotifyFacade notifyFacade;
private final AuditFacade auditFacade;
private final ObjectMapper objectMapper;
@KafkaListener(topics = "coderoadmap.order-events",
groupId = "coderoadmap-notify", concurrency = "3")
public void notify(String payload) throws Exception {
OrderCreatedEvent evt = objectMapper.readValue(payload, OrderCreatedEvent.class);
notifyFacade.onOrderCreated(evt); // 内部查 lab_processed_events,命中即跳过
}
@KafkaListener(topics = "coderoadmap.order-events",
groupId = "coderoadmap-audit")
public void audit(String payload) throws Exception {
OrderCreatedEvent evt = objectMapper.readValue(payload, OrderCreatedEvent.class);
auditFacade.onOrderCreated(evt); // INSERT lab_audit_log,撞 UNIQUE(event_id) 则忽略
}
}
两个 groupId 是硬性要求:验证「多订阅者各自全量」这一核心模型。审计侧 onOrderCreated 用 INSERT IGNORE 或捕获 DuplicateKeyException 静默跳过,都算幂等。
步骤 5:验收操作
- 连续
POST /lab/orders 5 笔。
- 查库:
lab_orders 5 行;lab_audit_log 5 行;通知日志 5 次(三处都对上)。
- 重启 notify 应用(或重启整个 Spring Boot),对同一批消息重新投递(可用
kafka-consumer-groups --reset-offsets --to-datetime 或重发同 eventId 的消息):通知不应重复执行——日志应出现「skip eventId=...」。
- 验证同订单事件有序:同一 orderId 再发
OrderPaid,确认消费者日志里 Created 恒在 Paid 之前、且两者 partition 相同。
- CLI 看两组 lag:
docker exec kafka-lab /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group coderoadmap-notify
docker exec kafka-lab /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group coderoadmap-audit
步骤 6:写 README 边界(必做)
- 何时 Redis 删缓存 vs 发 Kafka 事件:举本站各一例(课程缓存失效广播 / 订单多下游处理)
- 本 lab 与生产主站的关系:独立
/lab/** 与 lab 表,不碰 cf_* 权威路径
- 已知简化与下一步:如未上 Outbox(事务后直接发送的崩溃窗口)、未配死信 Topic、未接监控
这三节是交付物的一部分,不是空话——面试官问「你这个项目的边界和风险」,答案就在这里。
讲清楚要点
- 因为双 group 才是多订阅者模型,所以验收必须看到 notify 与 audit 各自 5 条,而不是两组抢 5 条——这是本 lab 最核心的验证点。
- 因为at-least-once 必然可能重复,所以「重启后不重复副作用」要靠幂等证明(skip 日志),不能靠运气。
- 因为key=orderId,所以同一订单的 Created/Paid 同分区有序;换任何其他 key 策略前先想清楚顺序需求。
- 因为LAG 反映追赶情况,所以验收清单包含 describe,而不仅是「日志有了」——位点与业务进度是两回事。
- 因为实验库与生产库物理隔离,所以可以放心制造积压、杀进程、清 Topic,把故障场景都演一遍。
- 因为主站可以完全无 Kafka 运行,所以 README 里的边界句是工程判断力的证明:知道什么时候该用什么,比什么都用上更难。
你可能会懵的地方
-
「两个 Listener 能共用一个 groupId 吗?」
不能(会互抢分区,各自收不全)。本练习硬性要求两个 groupId;这正是概念课「组内互斥、组间全量」的实操。
-
「审计 5 行但通知只有 3?」
分组排查:--describe 看两组各自的 LAG 与成员;看应用错误日志(大概率是反序列化或 DB 异常被吞);用 console consumer 确认 Topic 内确有 5 条,把问题定位到消费侧还是生产侧。
-
「幂等了为什么还要关心位点提交?」
位点不前进会反复 poll 同一段消息,浪费资源且 lag 永远不归零。幂等是安全网,位点是进度,两者都要正确。
-
「必须用 Streams 吗?」
不要求。本练习聚焦生产/消费/幂等/lag 四件事;Streams 是独立的进阶方向,适合作为课后扩展。
-
「事件丢了怎么发现?」
对账视角:定时任务比对「lab_orders 行数」与「lab_audit_log 行数」(或 outbox 缺口)。本 lab 可手查;生产靠对账任务 + 告警——这也是 afterCommit 方案风险窗口的标准兜底。
错法对照
| 错法 | 问题 | 正解 |
|---|
notify/audit 共用 groupId | 组内互斥,互相抢消息收不全 | 两个 group,各收全量 |
无 eventId,靠「别重启」防重复 | 重启/rebalance 立刻露馅 | 去重表/唯一索引兜底 |
| 只验日志,不验 LAG 与表行数 | 「看起来在跑」≠ 数据一致 | 验收五项逐条勾 |
| 把 lab 直接接到生产订单 API | 实验故障污染真实业务 | 独立 /lab/** 路由与 lab 表 |
| 事务内随手 send | 回滚后下游处理幽灵订单 | afterCommit 或 Outbox,注释写明风险 |
卡点急救
- 监听器一条消息都没收到:按链路查——Topic 存在?(
--list)发送回调是否报错?groupId 拼写?组是否已停在 latest(换新组试)?console consumer 先确认消息真的进了 Topic。
- 重启后重复副作用:检查幂等是否真的生效——去重表有没有插上(
SELECT * FROM lab_processed_events)、唯一索引建了没有、检查与副作用是否在同一事务/同一方法内。
- LAG 居高不下:回看监控课三板斧——处理耗时(是不是 sleep 忘删了)、实例数 vs 分区数、下游(lab 的 DB 写入)是否被拖慢;逐层定位而不是重启碰运气。
面试常问
-
介绍一下你做过的 Kafka 项目(用本 lab 回答)
背结构:背景(订单创建后要通知、审计,多下游解耦)→ 方案(Topic 3 分区、key=orderId 保序、双消费组订阅)→ 难点(at-least-once 重复 → eventId 去重表幂等;事务边界 → afterCommit 发送)→ 验证(重启幂等验证、CLI 看 lag、分组 describe)→ 边界(未上 Outbox 的风险窗口与对账兜底)。这个五段式适用于任何项目介绍。
-
如果消息量涨 10 倍,你的链路哪里先崩?
背思路而不是背答案:①先看消费并行度——3 分区 3 并发已满,加实例无效,需扩分区;②再看待性——DB 写入是瓶颈则审计表分库分表或换存储;③再看保留与磁盘——retention 与容量规划;④最后看生产端——afterCommit 同步发送会拖慢 API,考虑异步化/批量。能答「先崩在哪」说明真的理解链路。
自测三问
- 验收清单五项能否全部现场演示?缺哪项补哪项。
- README:何时 Redis 删缓存 vs 发 Kafka 事件?各举本站一例。
- 回顾本 track 12 课,用三句话串起「日志 → 组 → 幂等」这条主线。
验收清单
小结一句话
订单 Topic + 双消费组 + 幂等 + 会看 lag = Kafka 入门闭环;生产权威仍在 MySQL/REST,Kafka 是你事件驱动能力的扩展起点。
延伸
- Apache Kafka 官方文档
- 下一步方向:把 lab 能力带进真实项目——给 Spring Boot 主站加一条真实事件链路(如课程更新通知搜索索引),或继续进阶 Spring Cloud 事件总线与生产部署(多 Broker、SASL/SSL、Prometheus 监控)。学完本 track,你已经具备读懂任何 Kafka 生产架构图的底子。