连载中 1/20

为什么你需要一个消息队列:解耦、异步、削峰

2026-07-29 · 1209 阅读 · 0 评论 · 0 赞

从一个 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 = 780ms30+120+发送消息约 5ms ≈ 155ms
下游故障影响短信挂,下单挂短信挂,消息还在队列里,恢复后续跑
新增下游改下单代码、发版新服务自己订阅事件,主链路零改动
流量压力洪峰直接砸到所有下游洪峰先进队列,下游按能力消费

耗时为什么还剩 155ms?因为扣库存我们刻意保留为同步调用——库存扣失败了订单就不能成,这类强一致的事不该走消息。分清「必须同步的」和「可以异步的」,是设计的第一步。

解耦:把「我通知你」变成「你听我广播」

同步调用是点对点的强关联:我调你,就必须知道你是谁、接口长什么样、挂了怎么办。MQ 之后变成了发布订阅:订单服务只对全世界广播一句「订单 10086 已支付」,至于谁听、听去干嘛,它一概不知。

这种模式下,新增「下单送抽奖」只需要抽奖服务自己订阅事件,下单服务一行代码不用改,甚至不用重新发版。模块之间的依赖从网状变成了以事件为中心的星状,改动的爆炸半径小了一大圈。

异步:快的是「感知」,不是「业务」

这里要泼一盆冷水:引入 MQ 之后,积分并没有更快发到用户账上——它只是不再阻塞下单接口了。用户感知变快,主流程变快,但下游的总工作量一点没少。

所以异步的前提是业务能接受「最终完成」:短信晚 3 秒到、积分晚 1 秒入账,用户无感。反过来,如果业务要求「扣款成功必须立刻看到余额变化」,那这条支路就不该异步。把不适合异步的硬塞进 MQ,等于把bug 从今天寄到了明天。

削峰:队列是流量和消费力之间的蓄水池

「第二杯半价」开场那一秒,下单请求是每秒一万笔,但下游的消费能力只有每秒两千。同步模式下,多出来的八千笔要么堆积在线程池里把服务撑爆,要么被限流挡在门外变成丢单。

有了 MQ,洪峰先写进队列——写队列是磁盘顺序写,每秒几万到几十万笔都不在话下。下游按自己的能力匀速消费,把一秒钟的洪水摊成十秒钟的细流。队列在这里就是蓄水池:上游可以任性,下游保持体面

但蓄水池有容量上限,洪峰太大、持续太久,队列也会积压。积压了怎么办、怎么提前发现、怎么紧急扩容,这是第 12 篇的主题,这里先按下不表。

哪些场景适合往 MQ 里塞

场景典型例子主要收益
事后通知短信、推送、站内信异步提速 + 故障隔离
数据投递订单数据同步到搜索、数仓解耦,收发两端独立演进
流量整形秒杀、大促、打卡高峰削峰填谷
任务缓冲视频转码、报表生成重活排队慢慢干

别急着动手

看到这里你可能已经想回工位拆链路了,先坐稳。消息队列不是免费午餐:消息会丢、会重、会乱序,同步调用的「要么都成、要么都败」被拆成了两个系统各自为政——一致性成了新的头号难题。老王把 MQ 插上去的第三周,就因为积分服务重复消费,一位会员的积分翻了一倍,客服电话被打爆。

下一篇,我们先把 MQ 的代价摆上桌:引入它到底背了哪些债,哪些场景看着适合、其实不该用。知道代价再上路,才算真的会算账。咖啡馆不着急,咖啡豆管够。

503

10 年全栈工程师 · 503咖啡馆主理人

#消息队列#MQ#解耦#异步#削峰

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 3 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞