7.0 KiB
id, title, type, domain, stack, tags, level, related, week
| id | title | type | domain | stack | tags | level | related | week | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend.mq.interview | 消息队列面试题 | interview | backend |
|
|
intermediate |
|
|
消息队列面试题
基础
【基础】为什么要用消息队列?会带来什么问题?
参考答案:异步(非核心步骤不阻塞主流程,接口更快)、解耦(新增下游不用改上游)、削峰(突发流量先进队列,按能力消费)。问题:系统更复杂,多了一个要维护的组件;消息可能重复、丢失、乱序,要处理;数据变成最终一致;排查链路变长。
追问:什么情况下不该用消息队列?→ 调用方需要立即拿到结果、链路简单、数据量小的场景,用同步调用更简单;引入 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' 取消并在同一事务回补库存,和支付回调天然互斥、重复消息幂等;保留低频的兜底扫描任务防止消息丢失。详见延迟取消场景。
追问:订单在取消前被支付了怎么办?→ 取消时用条件更新(WHERE status = '待支付'),状态已变则什么都不做;支付回调同样用条件更新,两者竞争时只有一个成功。