从一个 800ms 的下单接口说起
老王的咖啡连锁去年上线了小程序点单,生意好得很。但最近运营搞了场「第二杯半价」,热闹是热闹,祸事也跟着来了:下单接口平均响应 800ms,高峰期直接超时,用户盯着转圈的手指已经按到了差评按钮。
打开代码一看,下单逻辑长这样:
public void createOrder(CreateOrderRequest req) {
orderService.save(req); // 30ms 落订单
stockService.deduct(req.getSku()); // 120ms 扣库存(库存服务偶尔抽风)
pointService.addPoint(req.getUserId()); // 150ms 发积分(积分服务更慢)
couponService.grant(req.getUserId()); // 180ms 发优惠券
smsService.sendNotify(req.getPhone()); // 300ms 发短信(运营商排队)
}串行调五个服务,耗时直接相加。更要命的是:任何一个下游抖一下,用户就下不了单。发短信慢,跟用户付钱买咖啡有什么关系?可它就是能把下单拖死。
这就是没有消息队列时的典型姿势。这一篇我们把病根拆开看,再让消息队列把三件事各归各位:解耦、异步、削峰。
同步链路的三宗罪
第一宗罪:耦合。下单服务必须认识库存、积分、优惠券、短信四个服务。积分服务哪天重构了接口签名,下单服务跟着改版发版。新加一个「下单送抽奖」,又得改下单代码。订单服务成了全公司的依赖中枢,谁动它都得小心翼翼。
第二宗罪:慢。用户其实只关心两件事:钱付了、单成了。扣库存是必须的,但发积分、发券、发短信对「下单」这个动作而言是附属品。把附属品的耗时算进主流程,就是让用户为不关心的事买单。
第三宗罪:脆。链路上任何一个环节故障,都会沿着调用链向上传染。短信服务商限流了,下单跟着超时;线程池被慢调用占满,连落库都做不了。故障从一条支路灌进了主干道。
插一台消息队列,链路怎么变
消息队列(Message Queue,下文简称 MQ)本质上就是一台「存消息的缓冲区」:生产者把消息投进去就返回,消费者按自己的节奏取出来处理。插进下单链路之后,代码变成这样:
public void createOrder(CreateOrderRequest req) {
orderService.save(req); // 30ms 落订单
stockService.deduct(req.getSku()); // 120ms 扣库存(核心,保留同步)
// 剩下的都是「发生了下单这件事」的后果,发消息即可
mq.send(new OrderPaidEvent(req.getOrderId(), req.getUserId()));
}积分、优惠券、短信三个下游改成订阅 OrderPaidEvent,谁关心谁订阅,下单服务一个都不认识。整条链路的变化用一张表看清楚:
| 对比项 | 同步调用 | 引入 MQ 后 |
|---|---|---|
| 接口耗时 | 30+120+150+180+300 = 780ms | 30+120+发送消息约 5ms ≈ 155ms |
| 下游故障影响 | 短信挂,下单挂 | 短信挂,消息还在队列里,恢复后续跑 |
| 新增下游 | 改下单代码、发版 | 新服务自己订阅事件,主链路零改动 |
| 流量压力 | 洪峰直接砸到所有下游 | 洪峰先进队列,下游按能力消费 |
耗时为什么还剩 155ms?因为扣库存我们刻意保留为同步调用——库存扣失败了订单就不能成,这类强一致的事不该走消息。分清「必须同步的」和「可以异步的」,是设计的第一步。
解耦:把「我通知你」变成「你听我广播」
同步调用是点对点的强关联:我调你,就必须知道你是谁、接口长什么样、挂了怎么办。MQ 之后变成了发布订阅:订单服务只对全世界广播一句「订单 10086 已支付」,至于谁听、听去干嘛,它一概不知。
这种模式下,新增「下单送抽奖」只需要抽奖服务自己订阅事件,下单服务一行代码不用改,甚至不用重新发版。模块之间的依赖从网状变成了以事件为中心的星状,改动的爆炸半径小了一大圈。
异步:快的是「感知」,不是「业务」
这里要泼一盆冷水:引入 MQ 之后,积分并没有更快发到用户账上——它只是不再阻塞下单接口了。用户感知变快,主流程变快,但下游的总工作量一点没少。
所以异步的前提是业务能接受「最终完成」:短信晚 3 秒到、积分晚 1 秒入账,用户无感。反过来,如果业务要求「扣款成功必须立刻看到余额变化」,那这条支路就不该异步。把不适合异步的硬塞进 MQ,等于把bug 从今天寄到了明天。
削峰:队列是流量和消费力之间的蓄水池
「第二杯半价」开场那一秒,下单请求是每秒一万笔,但下游的消费能力只有每秒两千。同步模式下,多出来的八千笔要么堆积在线程池里把服务撑爆,要么被限流挡在门外变成丢单。
有了 MQ,洪峰先写进队列——写队列是磁盘顺序写,每秒几万到几十万笔都不在话下。下游按自己的能力匀速消费,把一秒钟的洪水摊成十秒钟的细流。队列在这里就是蓄水池:上游可以任性,下游保持体面。
但蓄水池有容量上限,洪峰太大、持续太久,队列也会积压。积压了怎么办、怎么提前发现、怎么紧急扩容,这是第 12 篇的主题,这里先按下不表。
哪些场景适合往 MQ 里塞
| 场景 | 典型例子 | 主要收益 |
|---|---|---|
| 事后通知 | 短信、推送、站内信 | 异步提速 + 故障隔离 |
| 数据投递 | 订单数据同步到搜索、数仓 | 解耦,收发两端独立演进 |
| 流量整形 | 秒杀、大促、打卡高峰 | 削峰填谷 |
| 任务缓冲 | 视频转码、报表生成 | 重活排队慢慢干 |
别急着动手
看到这里你可能已经想回工位拆链路了,先坐稳。消息队列不是免费午餐:消息会丢、会重、会乱序,同步调用的「要么都成、要么都败」被拆成了两个系统各自为政——一致性成了新的头号难题。老王把 MQ 插上去的第三周,就因为积分服务重复消费,一位会员的积分翻了一倍,客服电话被打爆。
下一篇,我们先把 MQ 的代价摆上桌:引入它到底背了哪些债,哪些场景看着适合、其实不该用。知道代价再上路,才算真的会算账。咖啡馆不着急,咖啡豆管够。
评论 (0)