异步与消息
pub/sub、队列、competing consumers、choreography。
异步与消息
这一课你将能做到
- 区分队列(点对点)与发布/订阅语义,说清 Kafka 的 Topic、Partition、Consumer Group 如何协作
- 为本站判题链路做削峰计算:峰值积压多少条、需要多少分区和消费者
- 写出至少一次投递下的幂等消费者代码(手动 ack + 唯一键 + 重试 + 死信队列)
为什么要学
同步调用链一长,延迟和故障就互相传染:判题要 10 秒,如果提交接口同步等判题,高峰期线程池瞬间被占满,连题目列表页都打不开。消息队列把「必须立刻完成」改成「最终完成」,是系统设计里最常用的解耦、削峰、异步化三合一工具。
面试必考:「为什么用消息队列?」「消息丢了怎么办?重复了怎么办?」「怎么保证顺序?」「积压了怎么处理?」——这四问就是本课的骨架。
人话:同步像是去食堂打饭非要盯着师傅做完你的菜才走;异步像是拿号坐下等叫号。Kafka 就是大屏幕取号系统——后厨(消费者)按自己速度出餐,高峰期单子排队(削峰),且同一张单子只给一个窗口(消费者组内竞争消费)。
先建立图像
判题链路的消息化设计:
提交 API 消费者组 judge-worker
│ ① 写 MySQL (status=PENDING) ┌─────────────┐
│ ② 发 Kafka │ JudgeWorker-1│──┐
├──────────────▶ Topic: judge-task│ JudgeWorker-2│──┤ Docker 沙箱判题
│ partition 0..23 │ JudgeWorker-N│──┘
│ ③ 立即返回「排队中」 └─────────────┘
▼ │ 判完写回 MySQL
用户轮询 GET /submissions/{id} ◀─────────┘ (或 WebSocket 推送)
Kafka 三个核心概念一句话:
Topic:消息的逻辑分类(judge-task)
Partition:Topic 的物理分片,每个分区内有序、是并行度的最小单位
Consumer Group:组内每条消息只被一个消费者处理(竞争),跨组全量广播(发布/订阅)
削峰算一笔账(这是面试的量化加分项):
比赛开始瞬间:提交 500 QPS,持续 60 秒 → 3 万条消息涌入
判题能力:20 个 worker × 5 QPS = 100 QPS
积压峰值:500×60 − 100×60 = 2.4 万条
消化时间:24000 ÷ 100 = 240 秒 ≈ 4 分钟排队上限
产品对策:前端展示「当前排队第 N 位」(N = Kafka lag),体验可控
队列深度可控 + 消费能力明确 = 削峰的本质是把不可控的瞬时压力变成可观测的排队延迟。
跟着做
步骤 1:建 Topic 并理解分区数怎么定
# 24 分区、3 副本
kafka-topics.sh --create --topic judge-task \
--partitions 24 --replication-factor 3 \
--bootstrap-server kafka-1:9092
# 观察消费延迟(lag)——判题队列最重要的监控指标
kafka-consumer-groups.sh --bootstrap-server kafka-1:9092 \
--describe --group judge-worker
分区数推导:消费总能力 = 分区数 × 单分区消费速率。目标 100 QPS 判题、单 worker 5 QPS → 20 个消费者 → 至少 20 分区,留扩容余量取 24。分区数只能增不能减,起步宁多勿少。
步骤 2:生产者——提交接口发消息
spring:
kafka:
bootstrap-servers: kafka-1:9092,kafka-2:9092,kafka-3:9092
producer:
acks: all # 全部同步副本确认,防丢
properties:
enable.idempotence: true # 生产者幂等,防 broker 端重复
retries: 10
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer