RabbitMQ 削峰:百万级写入如何平稳化

招聘平台有个典型的"潮汐流量"问题:早上 9 点到 11 点是求职高峰,投递请求瞬时量是平峰的 10 倍。数据库直写的话,要么连接池被打爆,要么主从延迟飙升。

一、为什么必须削峰

直写链路:API → 业务校验 → 事务写库。问题在于:

  1. 峰值并发超出库的承受上限:单库连接池 200,峰值 5000 QPS 涌入,全部排队等连接
  2. 事务放大:一次投递要写投递单 + 更新职位计数 + 写简历快照,一个请求 5+ 次写
  3. 雪崩风险:DB 慢 → 连接占满 → API 线程池耗尽 → 整站不可用

削峰的本质:把「瞬时 5000 QPS」变成「每秒恒定 800 的处理能力」,用队列做缓冲。

二、方案:RabbitMQ 异步削峰

A R M P a y I b S b Q i L t + M Q t o p i c : d e l i = v e r y . c r e a t e

关键配置

1
2
3
4
// 消费端:prefetch 限流,一次只取 N 条
channel.basicQos(200);
// 手动 ack,处理失败不丢消息
boolean autoAck = false;
  • prefetch=200:消费端同时处理 200 条,控制并发写库
  • 手动 ack:处理成功才确认,失败重回队列
  • 死信队列:重试 N 次仍失败进 DLX,人工兜底

削峰效果

指标 直写 RabbitMQ 削峰
API 响应 峰值时 2~5s 超时 稳定 < 100ms
峰值写库 QPS 5000(打爆) 恒定 800
高峰丢单 偶发超时丢 0(消息可靠)

三、削峰方案的三个坑

坑 1:消息堆积 = 用户感知延迟

削峰意味着投递不是"立即生效"。早上高峰,用户投完简历如果 30 秒后才看到"已投递",体验崩。

解法:双通道。 用户可感知的结果(投递状态)走同步直写(轻量),重量级的衍生写(简历快照、计数、通知)走异步队列。削的是"重"的,不削"轻"的。

坑 2:消费端不幂等 = 重复写

手动 ack 重试、消费端重启,都会造成消息重复投递。

解法:业务幂等键。 消息体带 delivery_id,消费时先查重(唯一索引/Redis SETNX),已处理直接 ack。

坑 3:消费能力跟不上积压

消费服务挂了 10 分钟,积压 100 万条,恢复后 prefetch 冲一波把库又打爆。

解法:渐进恢复。 消费启动时前 60 秒限速(prefetch 从 20 逐步升到 200),让 DB 连接池和缓冲池慢慢热身。

四、监控与兜底

  • 队列深度监控rabbitmq_queues_messages 告警阈值 5 万,超了自动扩容消费实例
  • 消费延迟:用消息时间戳算 now - enqueue_time,> 1 分钟告警
  • 死信兜底:DLX 队列每天巡检,重放或人工处理

总结

削峰不是把问题藏起来,是把瞬时压力换成可控延迟

  1. 分清轻重:用户可感知的同步,重的衍生写异步
  2. 队列要配限流:prefetch + 手动 ack,否则队列只是把压力搬到消费端
  3. 消费必须幂等:重复消息是异步系统的常态,不是异常

记住:队列不解决并发问题,队列解决的是"并发来了你扛不住"的问题——真正扛住的是恒定可控的消费速度。