主题
界面
主题
从本节起进入分布式与中间件。第一个问题是:为什么要在系统里塞一个"消息队列"?
它解决三件事:异步(别让用户等)、解耦(别让服务互相认识)、削峰(别被瞬时流量打死)★ 本节的实测会把第一件事量化出来 ✓:
① 同步处理 3000 单(用户等待全过程): 905 ms ✗✗
② 入队(用户只等到这里): 12 ms ✓✓
后台消费(用户不感知): 908 ms
→ 用户等待时间从 905 ms 降到 12 ms,**改善 75 倍** ✓✓先看一个"同步链路被拖长"的真实场景 ✗:
@Transactional
public String createOrder(OrderRequest request) {
Long orderId = orderRepository.save(request); // 10 ms
inventoryService.deduct(request); // 远程调用 50 ms
couponService.use(request); // 远程调用 50 ms
pointsService.add(request); // 远程调用 50 ms
smsService.notify(request); // 第三方接口 300 ms ✗✗
return "下单成功";
}★ 用户为了收到那条短信,要多等 300 ms ✗ ——而这 300 ms 里他什么也做不了 ✓
★ 本节的实测就是这个场景的量化(905 ms → 12 ms)✓
| 问题 | 没有 MQ | 有 MQ |
|---|---|---|
| 异步 ★★★ | 用户等完整链路 | 用户只等到"入队" ✓✓(实测 75 倍) |
| 解耦 ★★ | 下单接口要认识积分/短信/埋点服务 | 业务只发一个事件,谁要谁订阅 ✓✓ |
| 削峰 ★★★ | 秒杀流量直接打到数据库 | 流量先堆在队列里,后端按自己的速度消费 ✓✓ |
★★ 但必须说清一件事 ✓✓:
MQ 不会让总工作量变少 ✓
它只是把"用户的等待"搬成了"后台的处理" ✓
本节的实测就是证据:入队 12 ms,而后台仍然要花 908 ms 才处理完 ✓
★ 所以它有一个硬前提 ✓:
用户能接受"最终一致" ——他拿到的是"受理成功",不是"处理完成" ✓
「下单成功后立刻查订单,却发现查不到」就是没想清这一点 ✗
这是容量规划里最容易踩的坑 ✓✓ ——本节的实测:
分区数 1 消费者 1 → 耗时 526 ms
分区数 4 消费者 1 → 耗时 491 ms
分区数 4 消费者 4 → 耗时 128 ms ← 并行度 = 4,提速约 4 倍 ✓
分区数 4 消费者 8 → 耗时 132 ms ← **加了一倍消费者,一点没快** ✗✗
分区数 8 消费者 8 → 耗时 69 ms ← 加速比接近 8 ✓
分区数 8 消费者 16 → 耗时 70 ms ← **同样毫无收益** ✗✗★ 规律 ✓✓:
一个分区同一时刻只能给「组内一个消费者」 ✓
所以并行度上限 = 分区数 ——加到超过分区数,多出来的消费者完全空闲 ✗✗
反过来:分区数为 1 的 topic,无论加多少消费者都是串行 ✗✗
★ 由此推出两条实践规则 ✓✓:
| 规则 | 说明 |
|---|---|
| 建 topic 时按"未来峰值并行度 × 2"定分区数 ★★★ | 因为 分区数只能增不能减(减分区会打乱 key 的分区归属)⚠️ |
| 监控「消费 Lag」而不是「消费者数量」 ★★ | 消费者多不代表消费得动,Lag 才是真实水位 ✓ |
★ 顺带一个"坑里的坑" ✗:
增大分区数会打乱「同 key 同分区」的假设 ✗ —— 因为
hash(key) % 分区数里的除数变了,原来落 P0 的 key 可能跑到 P3 ✓所以「加分区」不是无痛的,要在业务低峰做,并接受一段时间的顺序错乱 ⚠️
实测(各 10 万条):
逐条发送 100000 条: 55 ms(181 万条/秒)
攒批发送 100000 条: 45 ms(222 万条/秒)★ 注意本模型里两者差距很小 ✗ ——因为这里没有网络往返 ✓
★ 真实 MQ 的批量收益来自两处 ✓✓:
| 来源 | 说明 |
|---|---|
| 把 N 次 RTT 合并成 1 次 ★★★ | 呼应 《时间复杂度和空间复杂度》:「RTT 比命令数更重要」✓ |
| 小消息攒成大包再压缩 | 压缩率随体积提升 ✓ |
★ 它的代价 ✓:
吞吐上升,但单条延迟上升 ——因为要等"攒批时间" ✗
这就是 Kafka 的
linger.ms(等多久攒批)与batch.size的意义 ✓✓
★ 一条调参经验 ✓:
| 场景 | linger.ms |
|---|---|
| 吞吐优先(日志、埋点)✓ | 大一些(如 20~100 ms) |
| 延迟优先(交易、通知)✓ | 小一些(如 0~5 ms) ✓ |
实测输出里的对照表 ✓:
| 产品 | 最强的地方 | 最弱的地方 | 典型场景 |
|---|---|---|---|
| Kafka ★★★ | 吞吐(百万级/秒) | 延迟不低、无优先级 | 日志、埋点、流处理 |
| RabbitMQ ★★ | 路由灵活、延迟低 | 吞吐一般 | 业务解耦、延迟任务 |
| RocketMQ ★★ | 事务消息、顺序消息 | 生态不如 Kafka | 电商交易链路 |
| Pulsar ★ | 存储计算分离、多租户 | 运维复杂 | 大规模多租户 |
★ 三条比"背参数"更有用的判断 ✓✓:
| # | 先问什么 | 为什么 |
|---|---|---|
| ① | 要不要顺序? | 要顺序就得用分区,还要考虑"热点 key 压单分区"(《消息可靠性、幂等与顺序》)✗ |
| ② | 能不能丢? | 不能丢就要确认 + 持久化 + 幂等消费(《消息可靠性、幂等与顺序》)✓ |
| ③ | 团队会运维哪个? | MQ 的运维成本远高于接入成本 ⚠️ |
★ 一条容易被忽略的事实 ✓✓:
引入 MQ = 引入一个必须 7x24 可用的中间件 ✓
它挂了,下单接口可能就发不出消息了 ✗
★ 所以要提前回答三个问题 ✓:
| 问题 | 典型答案 |
|---|---|
| MQ 挂了,业务能降级吗? ★★★ | 本地消息表 + 定时补偿(《Seata 分布式事务》)✓ |
| 消息发重复了会怎样? ★★★ | 消费端幂等(《消息可靠性、幂等与顺序》)✓ |
| 消息积压了怎么办? ★★ | 扩消费者(受分区数限制)+ 临时降级 |
★ 这一节比"怎么用"更重要 ✓✓:
| 需求 | 更好的选择 |
|---|---|
| 只是"异步执行" ★★ | 线程池就够了(《操作系统:进程与线程》)✓ |
| 只是"解耦"且同进程 ★★ | Spring 事件就够了(《设计模式在 Spring 中的应用》)✓ |
| 只是"定时任务" | @Scheduled / 调度平台 ✓ |
| 只是想"削峰"但量不大 | 限流 + 排队(《Sentinel 限流与熔断》)✓ |
★ 真正需要 MQ 的信号 ✓✓:
① 跨进程 / 跨团队 —— 生产者和消费者不在一个应用里 ✓
② 要削峰 —— 瞬时流量远超下游处理能力 ✓
③ 要重放 —— 消息要能被"重新消费"(如补数据、修 bug 后重跑)✓✓
④ 要多个消费方各自消费 —— 且互不影响 ✓★ 四条都满足才值得上 MQ,只满足一两条要慎重 ⚠️
// 生产者:按 key 发送(同 key 会进同一分区,从而保证有序)
producer.send(new ProducerRecord<>("order.created", orderId.toString(), payload));
// 消费者:订阅并处理
consumer.subscribe(List.of("order.created"));
while (true) {
for (ConsumerRecord<String, String> record : consumer.poll(Duration.ofMillis(500))) {
handle(record);
}
}@Transactional
public String createOrder(OrderRequest request) {
Long orderId = orderRepository.save(request); // 核心:落库
// ★ 其余全部异步:发一条"订单已创建"消息,谁关心谁订阅
mqProducer.send("order.created", orderId.toString(), buildEvent(orderId, request));
return "下单成功"; // 立刻返回 ✓
}// ✗ 同一个应用里"异步写日志"用 MQ:多了一个中间件、一次网络往返、一套运维
mqProducer.send("app.log", json);
// ✓ 用 Spring 事件 + @Async 就够了
applicationEventPublisher.publishEvent(new LogEvent(json));代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/mq/ —— MiniBroker(最小消息队列:Topic / Partition / Offset / 消费组)+ MqBasicsLesson
运行:
cd labs
.\mvnw.cmd -pl distributed-lab compile
java "-Dstdout.encoding=UTF-8" -cp distributed-lab/target/classes cn.kb.learn.dist.mq.MqBasicsLesson实测输出(关键部分):
===== 1. 削峰填谷:同步处理 vs 入队后异步处理 =====
① 同步处理 3000 单(用户等待全过程): 905 ms ✗✗
② 入队(用户只等到这里): 12 ms ✓✓
后台消费(用户不感知): 908 ms
→ 用户等待时间从 905 ms 降到 12 ms,**改善 75 倍** ✓✓
===== 3. 消费并行度:消费者数超过分区数会怎样 =====
分区数 4 消费者 4 → 耗时 128 ms
分区数 4 消费者 8 → 耗时 132 ms ← 有消费者分不到分区,纯属浪费 ✗
分区数 8 消费者 8 → 耗时 69 ms
分区数 8 消费者 16 → 耗时 70 ms ← 有消费者分不到分区,纯属浪费 ✗★ 三条可落地的结论 ✓:
| # | 结论 | 依据 |
|---|---|---|
| ① | MQ 的收益是"改善用户等待",不是"减少总工作量" | 入队 12 ms,后台仍要 908 ms ✓ |
| ② | 分区数决定并行度上限 | 4 分区加 8 消费者毫无收益 ✗ |
| ③ | 分区数只能增不能减,要一次规划好 | 减分区会打乱 key 归属 ⚠️ |
| # | 错误 | 说明 |
|---|---|---|
| 1 | 以为 MQ 能"加快"处理 ✗✗ | 它只是搬家(5.1)✓ |
| 2 | 分区数定太小 ✗✗ | 并行度封顶,扩消费者无效 ✓✓ |
| 3 | 以为"加消费者就能提升消费速度" ✗✗ | 上限是分区数 ✓ |
| 4 | 上线后再改分区数 ⚠️ | 会打乱 key 的顺序保证 ✓ |
| 5 | 用 MQ 替代线程池/Spring 事件 ✗✗ | 过度设计(5.6)✓ |
| 6 | 没考虑"MQ 挂了会怎样" ✗✗ | 缺降级方案 ✓ |
| 7 | 把"入队成功"当"处理成功" ✗✗ | 用户查不到订单 ✓ |
目标:把"跨服务调用"改成"发消息",量出用户等待时间的改善。
步骤:
MqBasicsLesson,确认 905 ms → 12 ms 的改善 ✓;验收方式:能说出"你这个系统里,哪条链路该用 MQ、为什么、分区数定几"。
扩展任务:设计一个"秒杀下单"的消息队列方案:
需求:用户注册后要发欢迎邮件、加新人积分、通知运营。
三个动作互不依赖,且失败不影响注册。该用 MQ 还是 Spring 事件?为什么? 5. (设计)给"订单导出"设计异步方案:导出 10 万条要 30 秒,前端要立刻返回。
hash(key) % 分区数 ✗ —— 除数变了,原来落 P0 的 key 可能跑到 P3 → 顺序保证被打破 ✓★《RabbitMQ 核心概念与工作模式》:Exchange、Binding 与四种路由行为。
无,可直接开始