每秒 200 笔订单请求到支付服务,支付服务需调用第三方支付,限流 100 笔每秒。请你设计支付服务,要求分布式的,并且是 FIFO,不超过第三方限流量,但是要用满第三方 100 笔每秒的量,请问如何设计,需要考虑哪些方面?
- 标签:后端、场景题
🏛 业务架构师
这道题的本质是:200 QPS 的入口流量要在限流 100 QPS 的第三方通道上排队通过,还要 FIFO、还要把配额用满——这是一个"全局速率控制 + 公平排队"的设计题,也是真实支付系统(微信支付、银联通道)的核心问题之一。
核心方案:全局排队 + 令牌控制
请求到达 → 进入FIFO队列(Redis/MQ) → 放行器按速率取队头 → 调第三方 → 返回结果关键设计点
- 单点速率控制(全局一致性):100 QPS 的配额必须全局共享,用 Redis + Lua 原子地"取令牌"(或自研令牌服务单点主备)——多实例各限 100 会超配额被打回
- FIFO 保序:MQ 天然有序(单分区),或 Redis List(LPUSH + RPOP)做队列;全局严格 FIFO 会牺牲吞吐(队头阻塞),要和业务确认:是"按下单时间公平"还是"同用户有序即可"——后者可按用户分片并行,吞吐好得多
- 用满 100 QPS:令牌桶按固定速率生成 + 队列深度自适应——不能"每 10ms 放一个"的机械节拍(处理间隙浪费配额),应滑动窗口统计实际通过量,缺口立刻补(连续放行直到窗口配额用完)
- 背压与用户体验:200 进 100 出,队列必然增长——设队列上限(如 5000),超限快速失败(明确提示"排队人数过多"),绝不无限堆积;排队中的请求给预计等待时间("前方 200 笔,约 2 秒"),透明的等待显著降低放弃率,这比技术参数更影响转化
- 失败处理:第三方返回失败要区分(业务失败 → 直接回绝用户,不占重试;超时 → 计入已用配额保守处理,防重复支付)
- 配额弹性:和第三方谈临时提升配额的机制(大促前置换),商务手段 often 比技术手段便宜
收尾:这个系统的监控三件套:实际通过率(是否用满 100)、队列深度 P99、快速失败率——三个指标健康,方案就成立。
🏗 系统架构师
这道题的要求非常精确:分布式、FIFO、不超 100/s、且要打满 100/s。我按"限额分配 → 全局排队 → 可靠性"三层设计。
第一层:第三方限流量的分配(谁有权发起支付) 第三方限制 100 笔/s,必须有一个全局准入控制,分布式部署的支付服务实例不能各自为政(各限 100 会超限,各限 100/N 会用不满)。方案:
- Redis + Lua 令牌桶:Lua 脚本原子地"补令牌-扣令牌",多实例共享一个桶,全局精确 100/s
- 打满的关键:桶容量与补Token速率精确设 100/s,突发容量(burst)按第三方能容忍的瞬时突发设(如 200),实例间不搞二次限流
第二层:FIFO 的保证 FIFO 要求全局有序,只能靠"单一队列"实现:
- 订单请求进来先落库排队(
pay_queue表或 Kafka/RocketMQ 单分区),天然持久化,宕机不丢 - 消费者从队列按序取,取到后先抢令牌(Lua 扣桶),抢到才调第三方;抢不到令牌不能确认消费(MQ 场景延迟重投/本地重试),保证顺序不被跳过
- 要点:顺序消费 + 令牌不足时阻塞等待,如果某单抢不到令牌就跳过去调下一单,顺序就乱了;所以队列消费者并发度=1(单分区单线程),吞吐 100/s 完全够
- 严格 FIFO 与失败冲突:某笔支付调第三方失败重试会阻塞后续订单——设计上区分"业务失败"(订单关闭,立即跳过)与"系统失败"(超时/未知状态,重试 N 次后转人工,不阻塞后续,但记录乱序豁免),这是 FIFO 与可用性的现实折中
第三层:分布式一致性与可靠性
- 调第三方必须幂等(商户单号唯一),超时后查询补偿(先查后冲正),防止"超时但实际成功"造成重复支付
- 支付结果回调/查询与队列解耦,状态机驱动:排队→处理中→成功/失败/未知,未知态由对账任务兜底
- 监控三件套:令牌剩余水位(持续高位=没打满,持续为 0=可能超限)、队列堆积深度、第三方耗时 P99;堆积告警触发消费者水平扩(但注意 FIFO 分区不扩,扩的是后处理阶段)
架构总图:订单入口 → 持久化队列(单分区 FIFO) → 顺序消费者 → Redis Lua 全局令牌桶(100/s) → 第三方支付 → 幂等回调/对账。这个设计把"不超、打满、有序、不丢"四个约束分别落在对应组件上,是我会给面试官的完整答案。
💻 全栈开发专家
我的结论:入口削峰进 MQ 顺序队列,出口用分布式令牌桶匀速消费,把 100 QPS 打满。
整体架构
订单 200/s → 支付服务(生产者)→ MQ 顺序队列 → 网关消费者(分布式限流 100/s)→ 第三方支付。
关键设计点
- FIFO 保证:RocketMQ 顺序消息,订单号 hash 选 queue(
MessageQueueSelector),单队列内严格 FIFO。要讲清 trade-off:这是"按订单分区有序";若要求全局 FIFO,只能单队列单消费者,吞吐上限就是消费速率,鱼与熊掌不可兼得。 - 精确限流 100/s 且用满:消费端用 Redis + Lua 令牌桶(或 Redisson
RRateLimiter),速率 100、桶容量 100,允许秒级小突发把额度吃满;漏桶过于平滑反而浪费额度。Lua 里"判令牌 + 扣减"原子执行,多消费实例共享同一把限流器——这就是"分布式"的含义。 - 失败不阻塞:调第三方非限流类失败先本地重试 2 次,仍失败转重试 topic,绝不阻塞顺序队列拖慢整体速率。
- 背压与监控:MQ 堆积量是核心指标,超阈值告警;持续 200 进 100 出,队列在大促时段必然堆积,提前和第三方谈额度扩容。
消费端骨架
java
while (running) {
if (rateLimiter.tryAcquire()) {
payGateway.call(nextOrder()); // 100/s 之内
} else {
pause(50); // 未拿到令牌:不 ack、不推进 offset
}
}加分项:批量预取令牌(一次取 N 个批量提交)减少 Redis 往返;本地支付流水与第三方对账文件 T+1 对账兜底,闭环。