10 - 项目经历话术:怎么讲"我们怎么落地的"
定位:面试中讲 RabbitMQ 项目经历的标准话术模板 用法:背框架 + 填你自己的业务细节,不要死背原话
🎯 万能开场框架
面试官问:"你们项目中是怎么用 RabbitMQ 的?"
我们在 XX 项目中引入 RabbitMQ 主要解决三个问题:
1. 业务解耦:订单服务和下游服务(库存/积分/通知)异步通信
2. 削峰填谷:秒杀场景下保护数据库不被打垮
3. 异步处理:耗时操作(发短信/生成报表)不阻塞主流程
技术选型上,我们对比了 Kafka 和 RocketMQ,
最终选 RabbitMQ 的原因是:
- 延迟低(微秒级),适合业务消息而非日志流
- 路由灵活(Exchange + Binding),能支撑复杂的业务消息分发
- 生态成熟,Spring AMQP 整合度高,团队上手快一、消息可靠性 — 话术
🔹 场景引入
"你们怎么保证消息不丢的?"
📝 话术模板
这个问题我们踩过坑。上线初期出现过一次消息丢失,
排查后发现是消费端用了自动 ACK,消费者处理到一半崩了,
消息直接被 Broker 标记为已消费。
之后我们做了全链路可靠性改造,分三个环节:生产端(Producer → Broker)
第一,生产端开启了异步 Confirm 模式。
我们没有用同步 waitForConfirms(),因为吞吐量扛不住。
异步模式下用 ConcurrentSkipListSet 维护未确认的 deliveryTag,
收到 ACK 批量清理,收到 NACK 触发重试。
重试策略是指数退避,最多 3 次:
第 1 次立即重试,第 2 次等 1 秒,第 3 次等 3 秒。
3 次都失败就写入本地失败消息表,由定时任务兜底重发。
另外我们还开了 mandatory=true + ReturnListener,
防止消息到达 Exchange 后路由不到任何 Queue 被静默丢弃。
这个是后来做压测时发现的——消息发成功了但队列里没有,
就是因为 routingKey 写错了,没开 Return 根本不知道。Broker 端(存储)
第二,Broker 端做了三重持久化:
- Exchange 声明时 durable=true
- Queue 声明时 durable=true
- 消息发送时 deliveryMode=2(PERSISTENT)
三个缺一不可。我们 QA 环境出过一次问题:
Queue 持久化了,但 Exchange 没持久化,
Broker 重启后 Exchange 消失,消息全部丢失。
集群层面,我们用的是 3 节点仲裁队列(Quorum Queue),
基于 Raft 协议,3 节点写入需要 2 个确认才算成功。
比老的镜像队列更可靠——镜像队列是异步复制,
Master 宕机时有数据丢失的风险。消费端(Broker → Consumer)
第三,消费端改成手动 ACK,顺序是"先处理业务,后 ACK"。
之前出问题的根因就是 autoAck=true,
Broker 投递完就删消息了,消费者崩了消息就没了。
现在改为:
1. 先执行业务逻辑(写 DB、调下游)
2. 业务成功 → basicAck
3. 业务失败 → basicNack + requeue=false → 进入死信队列
另外 prefetch 设成了 10,一次最多拿 10 条。
设太大了内存扛不住,设太小了吞吐上不去,
压测下来 10 是比较均衡的值。🔹 追问:"Confirm 和事务你们用的哪个?"
用的 Confirm,没用事务。
事务的吞吐量太低了——我们压测过,
txSelect/txCommit 模式下 TPS 从 8000 掉到 800,差了 10 倍。
Confirm 模式异步回调,性能几乎没有损耗。
唯一要注意的是异步回调里操作 ConcurrentSkipListSet 要小心,
我们用 headSet(deliveryTag + 1).clear() 批量清理,
不要逐条 remove,否则高并发下有性能问题。二、延迟队列 — 话术
🔹 场景引入
"你们怎么实现订单超时关闭的?"
📝 话术模板
我们最开始用的是定时任务轮询数据库,
每分钟扫一次"30 分钟前创建且未支付的订单",
然后批量关闭。
上线后发现两个问题:
1. 扫表对数据库压力很大,高峰期扫一次要 3-5 秒
2. 精度不够,用户可能等了 29 分钟付了款,也可能等了 31 分钟才被关
3. 到了双 11 订单量暴增,扫表直接拖慢了主库
所以改成了 RabbitMQ 延迟队列方案。方案演进
第一版:TTL + 死信队列(DLX)
流程是这样的:
创建订单 → 发消息到延迟队列(TTL=30min)
→ 30 分钟后消息过期 → 进入死信队列
→ 死信消费者检查订单状态 → 未支付则关闭
这个方案能跑,但后来发现了一个坑:
RabbitMQ 的消息级 TTL 有个排队问题——
消息只有到达队列头部时才会检查是否过期。
如果队列前面的消息 TTL 更长,后面的消息就算到期了也不会被处理。
举个例子:前面有个 TTL=60min 的消息排着,
后面 TTL=30min 的消息要等到前面那个过期后才会被检查。
这就导致超时关闭的时间不准确。第二版(现在用的):延迟消息插件
我们安装了 rabbitmq_delayed_message_exchange 插件,
声明一个 x-delayed-message 类型的交换机。
发送消息时在 Header 里设 x-delay: 1800000(30 分钟),
插件会在 Broker 端存储消息,到期后才投递到队列。
优势:
1. 精确——不存在排队问题,每条消息独立计时
2. 灵活——同一条队列可以有不同的延迟时间
(订单超时 30 分钟、支付提醒 15 分钟、红包过期 24 小时,都走同一个延迟交换机)
3. 简单——不需要维护多组延迟队列 + 死信队列幂等保障
延迟消息消费端有一个关键点——幂等。
因为消息可能重复投递(网络抖动、消费者重启),
所以关闭订单前必须先检查状态:
1. Redis 查订单当前状态(快路径)
2. 如果是"已支付" → 直接 ACK 跳过
3. 如果是"未支付" → UPDATE orders SET status='CLOSED' WHERE order_id=? AND status='UNPAID'
(乐观锁,返回 0 说明已被其他消费者处理了)
这样即使收到重复的延迟消息,订单也只会被关闭一次。数据修正
还有一个兜底:虽然用了延迟队列,但我们保留了一个低频定时任务,
每天凌晨跑一次,扫描"超过 1 小时仍未关闭的未支付订单"。
这是为了防止极端情况:
比如 Broker 插件 bug 导致消息丢失,或者消费端异常导致消息没有被处理。
凌晨扫一次对数据库压力可以忽略不计,但能兜住极端 case。三、消息积压治理 — 话术
🔹 场景引入
"你们遇到过消息积压吗?怎么处理的?"
📝 话术模板
遇到过。有一次是下游支付接口超时引发的连锁反应。
那天下午支付渠道出了故障,接口响应从 50ms 飙到 5 秒,
消费者线程全部阻塞在 HTTP 调用上,
10 分钟内消息堆积了 50 多万条。
告警响了之后我们是这样处理的:紧急止血(前 10 分钟)
第一步:快速扩容消费者。
我们 k8s 部署的,消费者 Pod 从 5 个直接扩到 50 个。
RabbitMQ 的消费者数量没有硬限制,加 Pod 就能消费。
但这里有个前提——消费者不能有状态。
我们的消费者是无状态的,扩缩容直接生效。
如果有状态(比如本地缓存),扩容效果会打折扣。持续处理(10-30 分钟)
第二步:发现扩到 50 个 Pod 还是消费不过来。
因为消费瓶颈不在 RabbitMQ,而在下游支付接口——
接口超时 5 秒,每个消费者线程最多同时处理 1 条消息,
50 个 Pod × 每 Pod 10 个并发线程 = 500 并发,
500 × 5 秒/条 = 每秒 100 条,远远追不上生产速率。
所以我们做了两个决策:
1. 消费端开启降级:检测到支付接口超时后,
不再调用真实接口,改为写入本地 DB 的待处理表(毫秒级)
2. 把原队列的消息转移到 10 个临时队列,
每个临时队列分配独立的消费者组(扇出分流)
转移消费者的代码很简单:
从原队列消费 → 不处理业务 → round-robin 发到 10 个临时队列 → ACK。
每个临时队列 5 个 Pod,总共 50 个 Pod 在并行消费。恢复闭环(30 分钟后)
第三步:支付接口恢复后,消费速度上来了,
临时队列的消息在 1 小时内全部消化完。
但还有一个问题:降级期间写入 DB 待处理表的那些消息。
我们写了一个补偿任务:
- 每 5 分钟扫描一次待处理表
- 逐条调用支付接口重试
- 成功后更新状态为"已处理"
- 失败的保留,下次继续(最多重试 3 次,超过进入人工工单)
整个积压从 50 万条到完全消化,大概用了 2 小时。长期优化(事后复盘)
事后复盘我们做了几个长期优化:
1. 消费者并发模型优化
之前是同步阻塞调用,改成了 CompletableFuture 异步化:
消费者线程只负责从 Queue 取消息 + 提交到线程池,
实际的业务处理在线程池里跑。
改完后单 Pod 吞吐量从 10 条/秒 提升到 200 条/秒。
2. 下游超时熔断
接入了 Sentinel 熔断器:
- 支付接口错误率 > 50% → 自动熔断 10 秒
- 熔断期间消费端走降级逻辑(写 DB 待处理表)
- 半开状态探测恢复后自动切回正常模式
3. 队列长度告警
- messages_ready > 1 万 → P2 告警(企微通知)
- messages_ready > 10 万 → P1 告警(电话通知)
- 这样下次积压能在第一时间发现
4. 预取数调优
之前 prefetch=100,改成了 10。
prefetch 太大有个问题:消息被批量取到消费者内存,
如果消费者崩了,这些未 ACK 的消息要等连接超时后才会重新投递,
恢复时间变长。四、组合话术 — "讲 3 分钟版本"
面试官:"用 3 分钟讲一下你们 RabbitMQ 的整体设计"
好的,我从技术选型、核心设计、可靠性保障、踩过的坑四个维度讲。
【技术选型】
我们的电商业务需要的是业务消息(订单、支付、通知),
不是日志流,所以选了 RabbitMQ 而不是 Kafka。
RabbitMQ 延迟低、路由灵活、Spring 生态整合好。
【核心设计】
- 交换机类型:订单相关用 Direct(精确路由),
通知类用 Fanout(广播给短信/邮件/站内信多个服务)
- 队列设计:每个业务场景独立队列,
比如 order.create / order.pay / order.timeout 各一个
- 集群:3 节点仲裁队列,强一致性,容忍 1 节点故障
【可靠性保障】
- 生产端:异步 Confirm + mandatory + Return 回调
- Broker:Exchange + Queue + Message 三重持久化
- 消费端:手动 ACK + Redis 幂等校验 + 状态机兜底
- 异常处理:死信队列兜底 + 告警通知
【踩过的坑】
1. 最大的坑是消费端自动 ACK,导致消费者崩溃时消息丢失,
改成手动 ACK 后解决了
2. 延迟队列用 TTL+DLX 有排队问题,换成延迟插件后才精确
3. 积压治理不能只扩消费者,瓶颈往往在下游,
需要异步化 + 熔断降级配合
目前这套方案在线上跑了大半年,日消息量在百万级,
没出过消息丢失的问题。五、面试禁忌:不要这样说
| ❌ 错误说法 | ✅ 正确说法 |
|---|---|
| "RabbitMQ 能保证消息不丢" | "RabbitMQ 提供了机制,但需要我们在三个环节分别配置" |
| "设了持久化就不会丢" | "持久化有异步窗口期,还需要仲裁队列多副本保障" |
| "延迟队列用 TTL 就行" | "消息级 TTL 有排队问题,生产用延迟插件更精确" |
| "加消费者就能解决积压" | "瓶颈可能在下游,需要异步化+熔断+分流配合" |
| "ACK 了就安全了" | "ACK 只保证 Broker 知道你消费了,业务层面还要幂等" |
| "我们用了事务保证可靠" | "事务性能太差,生产用 Confirm 模式" |
六、关联笔记
- **消息可靠性保障** - 可靠性技术细节
- **死信队列与延迟队列** - 延迟队列实现方案
- **消息幂等性与重复消费** - 幂等设计细节
- **消息积压与消费能力** - 积压处理方案
- **RabbitMQ面试高频题汇总** - 面试题速查