Files
zhuangang/topics/backend/mq/interview.md
T

86 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: backend.mq.interview
title: 消息队列面试题
type: interview
domain: backend
stack: [mq]
tags: [RabbitMQ, Kafka, 可靠性, 幂等, 面试]
level: intermediate
related: [backend.mq.rabbitmq, backend.mq.kafka, backend.mq.reliability]
week: [14, 16]
---
# 消息队列面试题
## 基础
### 【基础】为什么要用消息队列?会带来什么问题?
**参考答案**:异步(非核心步骤不阻塞主流程,接口更快)、解耦(新增下游不用改上游)、削峰(突发流量先进队列,按能力消费)。问题:系统更复杂,多了一个要维护的组件;消息可能重复、丢失、乱序,要处理;数据变成最终一致;排查链路变长。
**追问**:什么情况下不该用消息队列?→ 调用方需要立即拿到结果、链路简单、数据量小的场景,用同步调用更简单;引入 MQ 的复杂度要和收益匹配。
### 【基础】RabbitMQ 有哪几种交换机?
**参考答案**:direct(routing key 完全匹配)、topic(通配符匹配,`*` 匹配一个词、`#` 匹配零个或多个词)、fanout(广播给所有绑定队列)、headers(按消息头匹配)。
**追问**:topic 交换机中 `order.*` 和 `order.#` 有什么区别?→ `*` 只匹配一个词,`order.*` 匹配 `order.created`,不匹配 `order.paid.notify`;`#` 匹配零个或多个词,两者都匹配。
### 【基础】什么是死信队列?
**参考答案**:消息被拒绝且不重新入队、TTL 过期、队列达到长度上限时,会被转发到队列配置的死信交换机,再路由到死信队列。用于集中处理失败消息,也可以和 TTL 组合实现延迟消息。
**追问**:死信队列里的消息怎么处理?→ 告警 + 人工分析原因;修复后由运维工具重新投递到原队列;记录处理结果,避免无限循环投递。
### 【基础】Kafka 的分区和消费组是什么关系?
**参考答案**:主题分成多个分区;同一个消费组里,一个分区同一时间只能被一个消费者消费,一个消费者可以消费多个分区;不同消费组互不影响、各自消费全部消息。所以组内并行度上限是分区数。
**追问**:消费者数量多于分区数会怎样?→ 多出来的消费者空闲,不会分到分区;想提高并行度要增加分区数(Kafka 分区只能增加不能减少,且会影响按 key 的顺序)。
## 进阶
### 【进阶】怎么保证消息不丢失?
**参考答案**:分三段看。生产端:开启确认(RabbitMQ publisher confirm、Kafka `acks=all`),失败重试,必要时用发件箱模式保证和数据库事务一致。broker:持久化 + 多副本(RabbitMQ quorum 队列;Kafka 副本数 >=3、`min.insync.replicas>=2`)。消费端:处理完成后再手动 ack / 提交位移。这样是“至少一次”,消费端还要幂等。
**追问**:发件箱模式的轮询会不会重复投递?→ 会,所以是“至少一次”;消费端必须幂等。多实例轮询时用 `FOR UPDATE SKIP LOCKED` 避免重复取同一批。
### 【进阶】怎么保证消费幂等?
**参考答案**:利用业务状态的条件更新(如 `WHERE status='pending'`);或者用消息 ID 去重表,在同一个事务里插入去重记录(唯一约束)并执行业务;或者在业务表上建唯一约束(如同一订单只发一次积分)。Redis 去重可以作为前置过滤,但关键业务要以数据库约束为准。
**追问**:唯一约束和 Redis 去重怎么选?→ 数据库唯一约束(和业务操作同一个事务)最可靠;Redis 去重性能高但和业务操作不在一个事务里,要处理“记录了去重但业务失败”的情况。
### 【进阶】怎么保证消息顺序?
**参考答案**:Kafka 只保证分区内有序,把需要有序的消息用同一个 key(如订单 ID)发到同一分区,消费端单线程处理该分区或按 key 分发到固定线程。RabbitMQ 同一个队列单个消费者时有序,多个消费者并发时无序,可按 key 拆到多个队列。另外也可以在消息里带版本号 / 状态,消费端丢弃过期的消息,降低对顺序的依赖。
**追问**:全局严格有序需要怎么做?→ 只能用单分区、单消费者,吞吐会受限;绝大多数业务只需要“同一订单 / 同一用户”内有序,按 key 分区即可。
### 【进阶】Kafka 为什么吞吐量高?
**参考答案**:顺序写磁盘(追加日志);利用操作系统页缓存;零拷贝(sendfile)发送数据给消费者;生产和消费都支持批量和压缩;分区让读写可以分布到多台 broker 并行。
**追问**:Kafka 的消息会丢吗?→ 配置不当会:`acks=1` 时 leader 宕机可能丢;消费端先提交位移再处理,处理失败会丢。可靠配置见“怎么保证消息不丢失”。
## 场景题
### 【场景题】下单后要扣库存、发积分、发通知,怎么保证订单和消息的一致性?
**参考答案**:用发件箱模式:订单和待发消息在同一事务里写入数据库,后台程序(或 CDC)把消息投递到 MQ,成功后标记;下游消费者幂等。这样订单提交了消息一定会发出(至少一次)。如果是强一致的跨服务操作(比如扣库存必须和下单同时成功),可以用 TCC 或 Saga 补偿,但复杂度高,大部分业务用“可靠消息 + 最终一致 + 对账”。
**追问**:为什么不直接在业务事务里发消息?→ 发消息是网络调用,事务提交和消息发送无法原子化:先发后提交可能发了消息事务却回滚,先提交后发可能发送失败。发件箱表把消息和业务数据放在同一个事务里。
### 【场景题】线上消息积压了几百万条,怎么处理?
**参考答案**:先看消费者是否正常(是否卡在某条毒消息、下游是否变慢、是否频繁再均衡)。修复问题后扩容消费者(Kafka 要看分区数是否够),提高消费并发、批量处理。积压特别大时,写一个临时程序把消息快速转存到分区更多的新主题,用大量消费者并行处理。同时评估:过期没有意义的消息(如过时的通知)可以跳过。事后加上积压监控告警。
**追问**:临时扩容消费者没用是为什么?→ 消费者数量已达到分区数上限,或者瓶颈在下游(数据库、第三方接口)。要先找到真正的瓶颈。
### 【场景题】订单 30 分钟未支付自动取消,怎么设计?
**参考答案**:RabbitMQ 用 TTL + 死信队列(或延迟插件)发送延迟消息;消费者用条件更新 `WHERE status='pending'` 取消并在同一事务回补库存,和支付回调天然互斥、重复消息幂等;保留低频的兜底扫描任务防止消息丢失。详见[延迟取消场景](scenarios/delayed-order-cancel.md)。
**追问**:订单在取消前被支付了怎么办?→ 取消时用条件更新(`WHERE status = '待支付'`),状态已变则什么都不做;支付回调同样用条件更新,两者竞争时只有一个成功。