消息队列设计
消息队列是系统解耦的核心组件。面试官想看你不只是会用,而是理解为什么需要消息队列,以及消息队列带来的新问题怎么解决。
面试考察点
面试官通过这道题想考察:
1. 你是否理解消息队列的核心价值
2. 你是否能处理顺序性、可靠性问题
3. 你是否理解幂等性的重要性
4. 你是否能根据场景选型
一、为什么需要消息队列?
1.1 不用消息队列的问题
面试官追问:"为什么需要消息队列?"
场景:用户下单后,需要:
1. 扣减库存
2. 发送短信通知
3. 增加用户积分
4. 通知物流系统
不用消息队列:
同步方式:
下单 → 扣库存 → 发短信 → 加积分 → 通知物流
↓
问题:
- 任何一个环节慢,整个流程都慢
- 任何环节失败,整个订单失败
- 紧耦合,修改一个影响全部
用消息队列:
异步方式:
下单 → 发消息到队列 → 立即返回"下单成功"
↓
库存服务:订阅消息,扣库存
短信服务:订阅消息,发通知
积分服务:订阅消息,加积分
物流服务:订阅消息,发货
优势:
- 异步:下单立即返回
- 解耦:各服务独立
- 削峰:高峰期消息堆积,平滑处理
1.2 消息队列的核心价值
┌─────────────────────────────────────────────────┐
│ 消息队列的核心价值 │
├─────────────────────────────────────────────────┤
│ │
│ 1. 异步:提升系统吞吐量 │
│ 用户不用等待所有操作完成 │
│ │
│ 2. 解耦:降低服务间依赖 │
│ 生产者和消费者独立演进 │
│ │
│ 3. 削峰:应对流量高峰 │
│ 消息堆积,慢慢处理 │
│ │
│ 4. 可靠:保证消息传递 │
│ 消息不丢失 │
│ │
└─────────────────────────────────────────────────┘
二、消息顺序性问题
2.1 为什么顺序会乱?
面试官追问:"消息顺序为什么会乱?怎么保证顺序?"
乱序的原因:
1. 并行消费
消息 1、2、3 按顺序发到队列
但消费者 A、B 并行处理
可能 B 先处理完 2,A 先处理完 1
导致执行顺序变成 2 → 1 → 3
2. 重试机制
消息 1 处理失败重试
消息 2、3 已经处理完
重试后顺序变成 2 → 3 → 1
解决思路:
1. 单一消费者
- 同一类型的消息只用一个消费者
- 但会损失并行度
2. 分区(Kafka)
- 相同 key 的消息到同一分区
- 同一分区内有序
- 关键:选好分区 key
2.2 分区 key 怎么选?
示例:订单消息
场景:创建订单 → 支付订单 → 发货
选错 key:
分区 key = user_id
用户 A:订单1 → 订单2 → 订单3
用户 B:订单4 → 订单5
如果都发到同一个分区:
订单2、订单1、订单3 可能乱序!
选对 key:
分区 key = order_id
同一订单的所有操作(创建、支付、发货)用同一 order_id
这样同一订单的操作一定有序!
面试加分点:
"保证顺序的关键是:选对分区 key
同一笔业务操作必须发到同一分区"
三、消息可靠性问题
3.1 消息会丢失吗?
面试官追问:"消息队列里的消息会丢吗?"
消息丢失的环节:
1. 生产者 → MQ
网络抖动,消息没发成功
2. MQ 自身
消息在 MQ 内存中,还没持久化
MQ 宕机丢失
3. MQ → 消费者
消费者拿到消息,但处理失败
或者还没处理完,消费者就宕机了
解决:
1. 生产者确认
- 生产者发送后等待确认
- 失败重试
2. MQ 持久化
- 收到消息后先刷到磁盘
- 有代价,性能下降
3. 消费者确认
- 手动 ACK,处理完再确认
- 自动 ACK 可能丢消息
3.2 怎么保证消息不重复?
面试官追问:"消息重复怎么办?"
重复的原因:
1. 生产者重试
消息发成功了,但没收到确认
重试导致重复发送
2. 消费者重试
消息处理成功了,但 ACK 失败
MQ 重新投递
解决:幂等性
幂等性 = 多次操作和一次操作结果相同
实现方式:
1. 数据库唯一约束
INSERT INTO consume_log(msg_id) VALUES(?)
重复插入会失败
2. Redis 去重
SETNX msg:processed:{msg_id} "1"
已处理的直接跳过
3. 业务状态机
更新前检查状态是否已处理
面试能加分的回答:
"消息队列不能保证 exactly-once,
但可以通过幂等性保证最终一致性"
四、常见 MQ 对比
4.1 Kafka vs RabbitMQ
┌─────────────────────────────────────────────────┐
│ Kafka vs RabbitMQ │
├─────────────────────────────────────────────────┤
│ │
│ Kafka: │
│ - 高吞吐量(10 万级 TPS) │
│ - 消息堆积能力强(TB 级) │
│ - 适用:日志收集、大数据、实时计算 │
│ │
│ RabbitMQ: │
│ - 功能丰富(交换机、路由) │
│ - 消息可靠性高 │
│ - 适用:业务系统、小数据量 │
│ │
│ 选型建议: │
│ 大数据、日志 → Kafka │
│ 业务系统 → RabbitMQ / RocketMQ │
│ │
└─────────────────────────────────────────────────┘
五、面试总结
消息队列核心要点
┌─────────────────────────────────────────────────┐
│ 消息队列核心 │
├─────────────────────────────────────────────────┤
│ │
│ 核心价值: │
│ - 异步、解耦、削峰 │
│ │
│ 顺序保证: │
│ - 同一分区 key │
│ - 单消费者(损失性能) │
│ │
│ 可靠性: │
│ - 生产者确认 + MQ 持久化 + 消费者 ACK │
│ │
│ 幂等性: │
│ - 唯一约束 / Redis 去重 / 状态机 │
│ │
└─────────────────────────────────────────────────┘
面试能加分的回答
1. 能解释为什么需要消息队列
"异步解耦削峰,核心是让调用方不等待所有操作完成"
2. 能解释顺序性问题
"选对分区 key,同一业务的消息发到同一分区"
3. 能解释幂等性
"消息队列不保证 exactly-once,需要消费者自己实现幂等"