Java 中间件之 RabbitMQ 快速入门: 异步通讯的优缺点

概述

上一篇把同步调用的账算了一遍:支付服务用 Feign 调了订单、仓储、短信三个服务,耗时叠加到 500ms,还背上了级联失败和资源浪费的包袱。这一篇换个思路------不亲自打电话了,改发广播。核心要解决的是:怎么让支付服务在 50ms 内把"钱收到了"这句话告诉用户,同时让另外三个服务一个不少地把活干完。

纲要

  • 同步调用遗留的四个问题:耦合度高、吞吐下降、资源浪费、级联失败
  • 事件驱动架构的三要素:事件发布者(Publisher)、Broker、事件订阅者(Consumer)
  • Broker 在异步调用里到底替你干了什么(转发、缓冲、解耦)
  • 异步通讯的四笔收益
    • 服务解耦:新增/下线订阅方零改动
    • 吞吐量提升:500ms → 60ms 是怎么算出来的
    • 故障隔离:仓储服务宕机不再拖垮支付链路
    • 流量削峰:Broker 当大坝,把秒杀洪峰拉平成匀速
  • 异步通讯的代价
    • Broker 成为新的强依赖(可用性与并发能力要求极高)
    • 拿不到下游执行结果
    • 消息丢失、重复消费、顺序性都得自己兜底
    • 调用链断裂,排障从"看堆栈"变成"翻日志追消息号"
  • 动手:基于 mq-demo 用 SpringAMQP 实现 pay.success 事件的发布与订阅
  • 判断准则:哪些场景千万别异步化

先看同步调用欠下的四笔账

先统一本文的示例口径:用户支付成功后,需要订单服务 改订单状态、仓储服务 扣库存发货、短信服务发通知。

同步方案下这条链路长这样:
#mermaid-svg-OJtdSRd1Q2L36swy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OJtdSRd1Q2L36swy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OJtdSRd1Q2L36swy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OJtdSRd1Q2L36swy .error-icon{fill:#552222;}#mermaid-svg-OJtdSRd1Q2L36swy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OJtdSRd1Q2L36swy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OJtdSRd1Q2L36swy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OJtdSRd1Q2L36swy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OJtdSRd1Q2L36swy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OJtdSRd1Q2L36swy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OJtdSRd1Q2L36swy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OJtdSRd1Q2L36swy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OJtdSRd1Q2L36swy .marker.cross{stroke:#333333;}#mermaid-svg-OJtdSRd1Q2L36swy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OJtdSRd1Q2L36swy p{margin:0;}#mermaid-svg-OJtdSRd1Q2L36swy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OJtdSRd1Q2L36swy .cluster-label text{fill:#333;}#mermaid-svg-OJtdSRd1Q2L36swy .cluster-label span{color:#333;}#mermaid-svg-OJtdSRd1Q2L36swy .cluster-label span p{background-color:transparent;}#mermaid-svg-OJtdSRd1Q2L36swy .label text,#mermaid-svg-OJtdSRd1Q2L36swy span{fill:#333;color:#333;}#mermaid-svg-OJtdSRd1Q2L36swy .node rect,#mermaid-svg-OJtdSRd1Q2L36swy .node circle,#mermaid-svg-OJtdSRd1Q2L36swy .node ellipse,#mermaid-svg-OJtdSRd1Q2L36swy .node polygon,#mermaid-svg-OJtdSRd1Q2L36swy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OJtdSRd1Q2L36swy .rough-node .label text,#mermaid-svg-OJtdSRd1Q2L36swy .node .label text,#mermaid-svg-OJtdSRd1Q2L36swy .image-shape .label,#mermaid-svg-OJtdSRd1Q2L36swy .icon-shape .label{text-anchor:middle;}#mermaid-svg-OJtdSRd1Q2L36swy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OJtdSRd1Q2L36swy .rough-node .label,#mermaid-svg-OJtdSRd1Q2L36swy .node .label,#mermaid-svg-OJtdSRd1Q2L36swy .image-shape .label,#mermaid-svg-OJtdSRd1Q2L36swy .icon-shape .label{text-align:center;}#mermaid-svg-OJtdSRd1Q2L36swy .node.clickable{cursor:pointer;}#mermaid-svg-OJtdSRd1Q2L36swy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OJtdSRd1Q2L36swy .arrowheadPath{fill:#333333;}#mermaid-svg-OJtdSRd1Q2L36swy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OJtdSRd1Q2L36swy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OJtdSRd1Q2L36swy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OJtdSRd1Q2L36swy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OJtdSRd1Q2L36swy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OJtdSRd1Q2L36swy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OJtdSRd1Q2L36swy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OJtdSRd1Q2L36swy .cluster text{fill:#333;}#mermaid-svg-OJtdSRd1Q2L36swy .cluster span{color:#333;}#mermaid-svg-OJtdSRd1Q2L36swy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OJtdSRd1Q2L36swy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OJtdSRd1Q2L36swy rect.text{fill:none;stroke-width:0;}#mermaid-svg-OJtdSRd1Q2L36swy .icon-shape,#mermaid-svg-OJtdSRd1Q2L36swy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OJtdSRd1Q2L36swy .icon-shape p,#mermaid-svg-OJtdSRd1Q2L36swy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OJtdSRd1Q2L36swy .icon-shape .label rect,#mermaid-svg-OJtdSRd1Q2L36swy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OJtdSRd1Q2L36swy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OJtdSRd1Q2L36swy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OJtdSRd1Q2L36swy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 提交支付
Feign 调用
Feign 调用
Feign 调用
响应
响应
响应
响应
用户
支付服务
订单服务
仓储服务
短信服务

四个服务串行下来,账目非常难看:

问题 表现 本次示例中的数据
耦合度高 支付服务代码里写死了下游三个服务的地址与接口,加一个积分服务就得改支付服务的代码并重新发布 每加一个业务方 = 一次支付服务发版
吞吐下降 总耗时是各服务耗时之和,线程被长时间占住 50 + 150 + 200 + 100 = 500ms
资源浪费 等待期间线程 BLOCKED 在连接上,占着 Tomcat 线程池不放 高峰期线程池被拖满,其他接口跟着雪崩
级联失败 任何一个下游挂掉或超时,异常沿调用链向上传递 仓储服务宕机 → 支付失败 → 用户付不了款

最刺眼的是最后一条:仓储服务挂了,跟"用户能不能付钱"这件事其实毫无关系,结果用户就是付不了。

事件驱动:把"调用"换成"通知"

三个角色

改造成异步后,链路里多出一个中间人:

角色 在本例中 职责
事件发布者 Publisher 支付服务 支付完成后发布一条 pay.success 事件,事件里带上订单 id,然后立刻结束自己的业务
Broker RabbitMQ 接收事件、暂存事件、把事件投递给所有订阅方;发布者和订阅者互不感知
事件订阅者 Consumer 订单服务、仓储服务、短信服务 提前向 Broker 声明"我要订阅 pay.success",收到后执行自己的业务逻辑

这里有个容易绕不过来的点:为什么不让支付服务直接通知这三个服务,非要加个 Broker?

因为"直接通知"本质上还是调用------支付服务照样得知道订单服务在哪、短信服务在哪,照样得处理调用失败和重试,耦合一点没少。Broker 的作用是把**"谁发出去的"和"谁会收到"这两件事彻底断开**:发布者只管往总线上扔,订阅者只管从总线上取,双方甚至可以不在同一个生命周期里活着。

讲义里那句描述很准:Broker 是像数据总线一样的东西,所有服务收发数据都发到这条总线上,总线就像协议一样,让微服务间的通讯变得标准和可控。

异步方案下的调用关系

#mermaid-svg-e33jbgYoiri0UKcj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-e33jbgYoiri0UKcj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-e33jbgYoiri0UKcj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-e33jbgYoiri0UKcj .error-icon{fill:#552222;}#mermaid-svg-e33jbgYoiri0UKcj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-e33jbgYoiri0UKcj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-e33jbgYoiri0UKcj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-e33jbgYoiri0UKcj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-e33jbgYoiri0UKcj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-e33jbgYoiri0UKcj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-e33jbgYoiri0UKcj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-e33jbgYoiri0UKcj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-e33jbgYoiri0UKcj .marker.cross{stroke:#333333;}#mermaid-svg-e33jbgYoiri0UKcj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-e33jbgYoiri0UKcj p{margin:0;}#mermaid-svg-e33jbgYoiri0UKcj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-e33jbgYoiri0UKcj .cluster-label text{fill:#333;}#mermaid-svg-e33jbgYoiri0UKcj .cluster-label span{color:#333;}#mermaid-svg-e33jbgYoiri0UKcj .cluster-label span p{background-color:transparent;}#mermaid-svg-e33jbgYoiri0UKcj .label text,#mermaid-svg-e33jbgYoiri0UKcj span{fill:#333;color:#333;}#mermaid-svg-e33jbgYoiri0UKcj .node rect,#mermaid-svg-e33jbgYoiri0UKcj .node circle,#mermaid-svg-e33jbgYoiri0UKcj .node ellipse,#mermaid-svg-e33jbgYoiri0UKcj .node polygon,#mermaid-svg-e33jbgYoiri0UKcj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-e33jbgYoiri0UKcj .rough-node .label text,#mermaid-svg-e33jbgYoiri0UKcj .node .label text,#mermaid-svg-e33jbgYoiri0UKcj .image-shape .label,#mermaid-svg-e33jbgYoiri0UKcj .icon-shape .label{text-anchor:middle;}#mermaid-svg-e33jbgYoiri0UKcj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-e33jbgYoiri0UKcj .rough-node .label,#mermaid-svg-e33jbgYoiri0UKcj .node .label,#mermaid-svg-e33jbgYoiri0UKcj .image-shape .label,#mermaid-svg-e33jbgYoiri0UKcj .icon-shape .label{text-align:center;}#mermaid-svg-e33jbgYoiri0UKcj .node.clickable{cursor:pointer;}#mermaid-svg-e33jbgYoiri0UKcj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-e33jbgYoiri0UKcj .arrowheadPath{fill:#333333;}#mermaid-svg-e33jbgYoiri0UKcj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-e33jbgYoiri0UKcj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-e33jbgYoiri0UKcj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-e33jbgYoiri0UKcj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-e33jbgYoiri0UKcj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-e33jbgYoiri0UKcj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-e33jbgYoiri0UKcj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-e33jbgYoiri0UKcj .cluster text{fill:#333;}#mermaid-svg-e33jbgYoiri0UKcj .cluster span{color:#333;}#mermaid-svg-e33jbgYoiri0UKcj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-e33jbgYoiri0UKcj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-e33jbgYoiri0UKcj rect.text{fill:none;stroke-width:0;}#mermaid-svg-e33jbgYoiri0UKcj .icon-shape,#mermaid-svg-e33jbgYoiri0UKcj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-e33jbgYoiri0UKcj .icon-shape p,#mermaid-svg-e33jbgYoiri0UKcj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-e33jbgYoiri0UKcj .icon-shape .label rect,#mermaid-svg-e33jbgYoiri0UKcj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-e33jbgYoiri0UKcj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-e33jbgYoiri0UKcj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-e33jbgYoiri0UKcj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 提交支付
发布 pay.success 事件
订阅
订阅
订阅
订阅
立即返回
用户
支付服务
Broker

RabbitMQ
订单服务
仓储服务
短信服务
积分服务

后加的,支付服务毫不知情

注意图里的两条虚线:用户侧的响应箭头绕过了所有订阅方;积分服务是新加的节点,跟支付服务之间没有连线------这就是解耦落地后的样子。

一次支付的完整时序

短信服务 仓储服务 订单服务 Broker 支付服务 用户 短信服务 仓储服务 订单服务 Broker 支付服务 用户 #mermaid-svg-T3s4FsuQxOe1emMU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-T3s4FsuQxOe1emMU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-T3s4FsuQxOe1emMU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-T3s4FsuQxOe1emMU .error-icon{fill:#552222;}#mermaid-svg-T3s4FsuQxOe1emMU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-T3s4FsuQxOe1emMU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-T3s4FsuQxOe1emMU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-T3s4FsuQxOe1emMU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-T3s4FsuQxOe1emMU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-T3s4FsuQxOe1emMU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-T3s4FsuQxOe1emMU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-T3s4FsuQxOe1emMU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-T3s4FsuQxOe1emMU .marker.cross{stroke:#333333;}#mermaid-svg-T3s4FsuQxOe1emMU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-T3s4FsuQxOe1emMU p{margin:0;}#mermaid-svg-T3s4FsuQxOe1emMU .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-T3s4FsuQxOe1emMU text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-T3s4FsuQxOe1emMU .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-T3s4FsuQxOe1emMU .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-T3s4FsuQxOe1emMU .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-T3s4FsuQxOe1emMU .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-T3s4FsuQxOe1emMU #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-T3s4FsuQxOe1emMU .sequenceNumber{fill:white;}#mermaid-svg-T3s4FsuQxOe1emMU #sequencenumber{fill:#333;}#mermaid-svg-T3s4FsuQxOe1emMU #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-T3s4FsuQxOe1emMU .messageText{fill:#333;stroke:none;}#mermaid-svg-T3s4FsuQxOe1emMU .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-T3s4FsuQxOe1emMU .labelText,#mermaid-svg-T3s4FsuQxOe1emMU .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-T3s4FsuQxOe1emMU .loopText,#mermaid-svg-T3s4FsuQxOe1emMU .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-T3s4FsuQxOe1emMU .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-T3s4FsuQxOe1emMU .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-T3s4FsuQxOe1emMU .noteText,#mermaid-svg-T3s4FsuQxOe1emMU .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-T3s4FsuQxOe1emMU .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-T3s4FsuQxOe1emMU .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-T3s4FsuQxOe1emMU .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-T3s4FsuQxOe1emMU .actorPopupMenu{position:absolute;}#mermaid-svg-T3s4FsuQxOe1emMU .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-T3s4FsuQxOe1emMU .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-T3s4FsuQxOe1emMU .actor-man circle,#mermaid-svg-T3s4FsuQxOe1emMU line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-T3s4FsuQxOe1emMU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} par Broker 广播 提前订阅 pay.success 1 提前订阅 pay.success 2 提前订阅 pay.success 3 提交支付 4 扣款 50ms 5 发布事件 orderId=1001 约 10ms 6 支付成功(累计约 60ms) 7 投递事件 8 投递事件 9 投递事件 10 更新订单状态(异步,支付不等它) 11 扣减库存、创建发货单(异步) 12 调用短信网关(异步) 13

时序图里最关键的是第 6 步之后:支付服务发完事件就收工了,下游什么时候做、做了多久、做没做完,它一概不知也一概不管。Broker 负责把事办完。

异步通讯的四笔收益

服务解耦:改需求不用动支付服务

产品经理要加积分。同步方案下,你得在支付服务的 pay() 方法里加一行积分服务的 Feign 调用,改完重新编译、测试、发版------为了一个跟支付无关的需求,给链路上的核心服务做一次变更,这本身就是风险。

异步方案下,支付服务的代码一行不动:

  • 新服务上线,自己去 Broker 订阅 pay.success 即可
  • 哪个业务不要了(比如短信成本太高),让短信服务取消订阅就完事,Broker 自然就不再给它投递

#mermaid-svg-dIANikEciPkhaI3V{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dIANikEciPkhaI3V .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dIANikEciPkhaI3V .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dIANikEciPkhaI3V .error-icon{fill:#552222;}#mermaid-svg-dIANikEciPkhaI3V .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dIANikEciPkhaI3V .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dIANikEciPkhaI3V .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dIANikEciPkhaI3V .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dIANikEciPkhaI3V .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dIANikEciPkhaI3V .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dIANikEciPkhaI3V .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dIANikEciPkhaI3V .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dIANikEciPkhaI3V .marker.cross{stroke:#333333;}#mermaid-svg-dIANikEciPkhaI3V svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dIANikEciPkhaI3V p{margin:0;}#mermaid-svg-dIANikEciPkhaI3V .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dIANikEciPkhaI3V .cluster-label text{fill:#333;}#mermaid-svg-dIANikEciPkhaI3V .cluster-label span{color:#333;}#mermaid-svg-dIANikEciPkhaI3V .cluster-label span p{background-color:transparent;}#mermaid-svg-dIANikEciPkhaI3V .label text,#mermaid-svg-dIANikEciPkhaI3V span{fill:#333;color:#333;}#mermaid-svg-dIANikEciPkhaI3V .node rect,#mermaid-svg-dIANikEciPkhaI3V .node circle,#mermaid-svg-dIANikEciPkhaI3V .node ellipse,#mermaid-svg-dIANikEciPkhaI3V .node polygon,#mermaid-svg-dIANikEciPkhaI3V .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dIANikEciPkhaI3V .rough-node .label text,#mermaid-svg-dIANikEciPkhaI3V .node .label text,#mermaid-svg-dIANikEciPkhaI3V .image-shape .label,#mermaid-svg-dIANikEciPkhaI3V .icon-shape .label{text-anchor:middle;}#mermaid-svg-dIANikEciPkhaI3V .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dIANikEciPkhaI3V .rough-node .label,#mermaid-svg-dIANikEciPkhaI3V .node .label,#mermaid-svg-dIANikEciPkhaI3V .image-shape .label,#mermaid-svg-dIANikEciPkhaI3V .icon-shape .label{text-align:center;}#mermaid-svg-dIANikEciPkhaI3V .node.clickable{cursor:pointer;}#mermaid-svg-dIANikEciPkhaI3V .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dIANikEciPkhaI3V .arrowheadPath{fill:#333333;}#mermaid-svg-dIANikEciPkhaI3V .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dIANikEciPkhaI3V .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dIANikEciPkhaI3V .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dIANikEciPkhaI3V .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dIANikEciPkhaI3V .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dIANikEciPkhaI3V .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dIANikEciPkhaI3V .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dIANikEciPkhaI3V .cluster text{fill:#333;}#mermaid-svg-dIANikEciPkhaI3V .cluster span{color:#333;}#mermaid-svg-dIANikEciPkhaI3V div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dIANikEciPkhaI3V .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dIANikEciPkhaI3V rect.text{fill:none;stroke-width:0;}#mermaid-svg-dIANikEciPkhaI3V .icon-shape,#mermaid-svg-dIANikEciPkhaI3V .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dIANikEciPkhaI3V .icon-shape p,#mermaid-svg-dIANikEciPkhaI3V .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dIANikEciPkhaI3V .icon-shape .label rect,#mermaid-svg-dIANikEciPkhaI3V .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dIANikEciPkhaI3V .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dIANikEciPkhaI3V .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dIANikEciPkhaI3V :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 下线业务
短信服务要下线
取消订阅
支付服务:

无需删代码
新增业务
积分服务要接入
订阅 pay.success 事件
支付服务:

无需感知 / 无需改动 / 无需发版

一句话:支付服务从此只认识 Broker,不认识任何业务方。

吞吐量提升:500ms 是怎么压到 60ms 的

把账重新算一遍:

方案 支付服务需要等待的环节 耗时
同步 扣款 + 订单 + 仓储 + 短信 50 + 150 + 200 + 100 = 500ms
异步 扣款 + 向 Broker 发一条消息 50 + 10 = 60ms

单线程的理论 QPS 从 1000/500 ≈ 2 变成 1000/60 ≈ 16,约 8 倍。原因是支付服务的线程不再被下游拖住,同样的线程池单位时间内能放更多请求过去。

值得强调的是:业务总工作量一点没少,订单、仓储、短信的活还得干完,变的只是这些活不再占用支付链路的时间窗。这就是为什么异步提升的是"支付这个接口的吞吐量",而不是"整条业务变快了"。

故障隔离:仓储挂了照样能收钱

仓储服务(已宕机) Broker 支付服务 用户 仓储服务(已宕机) Broker 支付服务 用户 #mermaid-svg-CPLDWMvaCYqYxWDn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CPLDWMvaCYqYxWDn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CPLDWMvaCYqYxWDn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CPLDWMvaCYqYxWDn .error-icon{fill:#552222;}#mermaid-svg-CPLDWMvaCYqYxWDn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CPLDWMvaCYqYxWDn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CPLDWMvaCYqYxWDn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CPLDWMvaCYqYxWDn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CPLDWMvaCYqYxWDn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CPLDWMvaCYqYxWDn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CPLDWMvaCYqYxWDn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CPLDWMvaCYqYxWDn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CPLDWMvaCYqYxWDn .marker.cross{stroke:#333333;}#mermaid-svg-CPLDWMvaCYqYxWDn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CPLDWMvaCYqYxWDn p{margin:0;}#mermaid-svg-CPLDWMvaCYqYxWDn .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-CPLDWMvaCYqYxWDn text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-CPLDWMvaCYqYxWDn .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-CPLDWMvaCYqYxWDn .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-CPLDWMvaCYqYxWDn .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-CPLDWMvaCYqYxWDn .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-CPLDWMvaCYqYxWDn #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-CPLDWMvaCYqYxWDn .sequenceNumber{fill:white;}#mermaid-svg-CPLDWMvaCYqYxWDn #sequencenumber{fill:#333;}#mermaid-svg-CPLDWMvaCYqYxWDn #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-CPLDWMvaCYqYxWDn .messageText{fill:#333;stroke:none;}#mermaid-svg-CPLDWMvaCYqYxWDn .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-CPLDWMvaCYqYxWDn .labelText,#mermaid-svg-CPLDWMvaCYqYxWDn .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-CPLDWMvaCYqYxWDn .loopText,#mermaid-svg-CPLDWMvaCYqYxWDn .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-CPLDWMvaCYqYxWDn .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-CPLDWMvaCYqYxWDn .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-CPLDWMvaCYqYxWDn .noteText,#mermaid-svg-CPLDWMvaCYqYxWDn .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-CPLDWMvaCYqYxWDn .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-CPLDWMvaCYqYxWDn .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-CPLDWMvaCYqYxWDn .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-CPLDWMvaCYqYxWDn .actorPopupMenu{position:absolute;}#mermaid-svg-CPLDWMvaCYqYxWDn .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-CPLDWMvaCYqYxWDn .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-CPLDWMvaCYqYxWDn .actor-man circle,#mermaid-svg-CPLDWMvaCYqYxWDn line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-CPLDWMvaCYqYxWDn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 服务宕机,消费者掉线 提交支付 扣款 50ms 发布 pay.success 事件 支付成功(60ms) 事件在队列里持久化堆积 运维重启 重新建立订阅 补投堆积的事件 扣减库存、创建发货单

仓储服务挂掉期间,用户支付完全不受影响,支付服务既不会抛异常也不需要写任何降级逻辑。等仓储服务重启,堆积的事件会被重新投递,这笔订单最终仍会被处理------只是慢一点。

这就是讲义里说的"没有直接调用,就不存在级联失败":异常没有传播路径,自然传播不过去。顺带把同步方案的第四个问题也解决了------不调用、不等待,就不会有线程傻等导致的资源浪费。

流量削峰:Broker 就是那道大坝

削峰是异步通讯自带的能力,它不是为了解决同步的问题,而是解决同步根本做不到的事。

假设三个下游服务每时刻只能处理 1 个请求,某一瞬间来了 3 个支付事件,甚至秒杀场景下一秒钟来 5000 个:
#mermaid-svg-p5pyRJevEmC7IO6j{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-p5pyRJevEmC7IO6j .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-p5pyRJevEmC7IO6j .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-p5pyRJevEmC7IO6j .error-icon{fill:#552222;}#mermaid-svg-p5pyRJevEmC7IO6j .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-p5pyRJevEmC7IO6j .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-p5pyRJevEmC7IO6j .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-p5pyRJevEmC7IO6j .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-p5pyRJevEmC7IO6j .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-p5pyRJevEmC7IO6j .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-p5pyRJevEmC7IO6j .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-p5pyRJevEmC7IO6j .marker{fill:#333333;stroke:#333333;}#mermaid-svg-p5pyRJevEmC7IO6j .marker.cross{stroke:#333333;}#mermaid-svg-p5pyRJevEmC7IO6j svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-p5pyRJevEmC7IO6j p{margin:0;}#mermaid-svg-p5pyRJevEmC7IO6j .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-p5pyRJevEmC7IO6j .cluster-label text{fill:#333;}#mermaid-svg-p5pyRJevEmC7IO6j .cluster-label span{color:#333;}#mermaid-svg-p5pyRJevEmC7IO6j .cluster-label span p{background-color:transparent;}#mermaid-svg-p5pyRJevEmC7IO6j .label text,#mermaid-svg-p5pyRJevEmC7IO6j span{fill:#333;color:#333;}#mermaid-svg-p5pyRJevEmC7IO6j .node rect,#mermaid-svg-p5pyRJevEmC7IO6j .node circle,#mermaid-svg-p5pyRJevEmC7IO6j .node ellipse,#mermaid-svg-p5pyRJevEmC7IO6j .node polygon,#mermaid-svg-p5pyRJevEmC7IO6j .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-p5pyRJevEmC7IO6j .rough-node .label text,#mermaid-svg-p5pyRJevEmC7IO6j .node .label text,#mermaid-svg-p5pyRJevEmC7IO6j .image-shape .label,#mermaid-svg-p5pyRJevEmC7IO6j .icon-shape .label{text-anchor:middle;}#mermaid-svg-p5pyRJevEmC7IO6j .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-p5pyRJevEmC7IO6j .rough-node .label,#mermaid-svg-p5pyRJevEmC7IO6j .node .label,#mermaid-svg-p5pyRJevEmC7IO6j .image-shape .label,#mermaid-svg-p5pyRJevEmC7IO6j .icon-shape .label{text-align:center;}#mermaid-svg-p5pyRJevEmC7IO6j .node.clickable{cursor:pointer;}#mermaid-svg-p5pyRJevEmC7IO6j .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-p5pyRJevEmC7IO6j .arrowheadPath{fill:#333333;}#mermaid-svg-p5pyRJevEmC7IO6j .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-p5pyRJevEmC7IO6j .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-p5pyRJevEmC7IO6j .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-p5pyRJevEmC7IO6j .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-p5pyRJevEmC7IO6j .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-p5pyRJevEmC7IO6j .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-p5pyRJevEmC7IO6j .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-p5pyRJevEmC7IO6j .cluster text{fill:#333;}#mermaid-svg-p5pyRJevEmC7IO6j .cluster span{color:#333;}#mermaid-svg-p5pyRJevEmC7IO6j div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-p5pyRJevEmC7IO6j .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-p5pyRJevEmC7IO6j rect.text{fill:none;stroke-width:0;}#mermaid-svg-p5pyRJevEmC7IO6j .icon-shape,#mermaid-svg-p5pyRJevEmC7IO6j .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-p5pyRJevEmC7IO6j .icon-shape p,#mermaid-svg-p5pyRJevEmC7IO6j .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-p5pyRJevEmC7IO6j .icon-shape .label rect,#mermaid-svg-p5pyRJevEmC7IO6j .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-p5pyRJevEmC7IO6j .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-p5pyRJevEmC7IO6j .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-p5pyRJevEmC7IO6j :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 洪峰
按能力拉取
按能力拉取
按能力拉取
超出 Broker 承载
5000 个支付事件

1 秒内涌入
Broker 队列

堆积 = 缓冲
订单服务

150ms/条
仓储服务

200ms/条
短信服务

100ms/条
Broker 自己先垮

洪水被大坝拦下来,下游按自己的节奏放水,每秒能处理多少就取多少。对比同步方案:5000 个请求直接打在三个服务上,被打爆的是服务本身。

这里必须补一句------大坝也得够结实。图中那条红色分支就是异步方案的新风险点,下面专门讲。

天上不会掉馅饼:异步的代价

很多教程讲到这里就收尾了,但真实项目里踩的坑全在下面这张表里。

同步与异步全维度对比

维度 同步调用(Feign) 异步通讯(Broker)
耦合度 高,写死下游地址 极低,只依赖事件契约和 Broker
单次请求耗时 所有环节累加,500ms 只算自身+投递,60ms
时效性 强,立即拿到结果 弱,拿不到下游执行结果
级联失败 存在 不存在,故障被隔离
流量削峰 做不到,请求直接压到服务 可以,Broker 缓冲
资源占用 等待期间占用线程 投递后立即释放
可用性依赖 依赖下游所有服务 依赖 Broker 这一个中心组件
数据一致性 强一致,一个本地事务/一句 Feign 判断 最终一致,要处理丢失、重复、乱序
排障难度 堆栈完整,链路清晰 链路断裂,需要靠消息轨迹追查
架构复杂度 低 高,多一个组件要部署、监控、调优

Broker 成为新的强依赖

整个异步方案的好处全建立在 Broker 上:削峰靠它缓冲、解耦靠它转发、隔离靠它暂存。Broker 挂了,所有微服务一起完蛋------这不是级联失败,这是单点击穿,比级联失败更狠。

而且"仅仅不挂"还不够。秒杀场景下一秒钟 5000 个事件往里写,Broker 的并发能力撑不住,就像大坝先塌了:缓存没做成,反而多了一个故障点。所以对 Broker 的要求是三条同时成立:高可用(集群、镜像队列)、高性能(足够的写入吞吐)、高可靠(持久化、不丢消息)。这也是后面几篇要啃的 publisher confirm、持久化、镜像集群、Lazy Queue 那堆东西存在的理由。

一句话糙理:你把一个分布式问题换了个地方集中存放,然后必须花大力气让那个地方不会出问题。

拿不到下游的执行结果

异步调用只是"通知你去干活",干完不会回来告诉我。所以凡是"我要立刻用这个结果继续往下走"的场景,异步直接不适用:

  • 查到一个订单,紧接着要用订单里的 userId 去查用户信息 → 必须同步
  • 下单前要实时校验库存是否充足 → 必须同步(异步的话你不知道库存够不够就让用户输入了支付密码)
  • 支付流程依赖风控的实时判定结果 → 必须同步

消息丢失与重复消费

这是异步方案最容易被忽略、上线后最容易出事的代价:

风险 触发场景 常见兜底手段
消息丢失 Broker 宕机且消息未持久化;消费者自动 ack 后业务抛异常 exchange/queue/message 三级持久化 + publisher confirm + 手动 ack + 失败重试或转入死信队列
重复消费 消费者业务已成功但 ack 前服务重启;网络抖动导致重投 消费侧做幂等(业务唯一键去重表 / Redis SETNX)
顺序错乱 多个消费者并发消费同一队列 单队列单消费者,或用业务状态机容忍乱序
事务边界 扣款成功但事件投递失败 本地消息表 + 定时任务补偿,或 RocketMQ 事务消息

核心结论:异步通讯把"一次分布式事务"拆成了"若干本地事务 + 一条不可靠的消息通道",中间的一致性必须由业务代码自己保证。 幂等和补偿不是可选优化,是必须做的功能。

调用链断裂,排障变难

同步调用出问题,一眼能看懂:异常堆栈里写着哪一跳超时、哪个服务返回 500。异步之后,支付服务发完事件就返回 200 了,用户隔了半天来投诉"短信没收到",这时候支付服务的日志里什么异常都没有。

实践里的常规做法:

  • 发布事件时生成全局唯一的 messageId(UUID 或雪花 id),随事件体一起传递,每个消费者在自己的日志里原样打印这个 id
  • 日志接入 ELK 或 Grafana Loki,用 messageId 一搜就能把一次事件在所有服务里的处理记录串出来
  • 支付/订单这类核心链路,额外维护一张"事件处理状态表",肉眼可查某笔订单的事件到底走到哪一步了
  • 生产环境务必开启 RabbitMQ Management 插件,先看队列有没有堆积,再看消费者有没有掉线,最后才去翻代码

判断问题在哪一层,最快的路径是:
#mermaid-svg-Z1IlUDevxq9z6AmL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Z1IlUDevxq9z6AmL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Z1IlUDevxq9z6AmL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Z1IlUDevxq9z6AmL .error-icon{fill:#552222;}#mermaid-svg-Z1IlUDevxq9z6AmL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Z1IlUDevxq9z6AmL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Z1IlUDevxq9z6AmL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Z1IlUDevxq9z6AmL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Z1IlUDevxq9z6AmL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Z1IlUDevxq9z6AmL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Z1IlUDevxq9z6AmL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Z1IlUDevxq9z6AmL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Z1IlUDevxq9z6AmL .marker.cross{stroke:#333333;}#mermaid-svg-Z1IlUDevxq9z6AmL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Z1IlUDevxq9z6AmL p{margin:0;}#mermaid-svg-Z1IlUDevxq9z6AmL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Z1IlUDevxq9z6AmL .cluster-label text{fill:#333;}#mermaid-svg-Z1IlUDevxq9z6AmL .cluster-label span{color:#333;}#mermaid-svg-Z1IlUDevxq9z6AmL .cluster-label span p{background-color:transparent;}#mermaid-svg-Z1IlUDevxq9z6AmL .label text,#mermaid-svg-Z1IlUDevxq9z6AmL span{fill:#333;color:#333;}#mermaid-svg-Z1IlUDevxq9z6AmL .node rect,#mermaid-svg-Z1IlUDevxq9z6AmL .node circle,#mermaid-svg-Z1IlUDevxq9z6AmL .node ellipse,#mermaid-svg-Z1IlUDevxq9z6AmL .node polygon,#mermaid-svg-Z1IlUDevxq9z6AmL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Z1IlUDevxq9z6AmL .rough-node .label text,#mermaid-svg-Z1IlUDevxq9z6AmL .node .label text,#mermaid-svg-Z1IlUDevxq9z6AmL .image-shape .label,#mermaid-svg-Z1IlUDevxq9z6AmL .icon-shape .label{text-anchor:middle;}#mermaid-svg-Z1IlUDevxq9z6AmL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Z1IlUDevxq9z6AmL .rough-node .label,#mermaid-svg-Z1IlUDevxq9z6AmL .node .label,#mermaid-svg-Z1IlUDevxq9z6AmL .image-shape .label,#mermaid-svg-Z1IlUDevxq9z6AmL .icon-shape .label{text-align:center;}#mermaid-svg-Z1IlUDevxq9z6AmL .node.clickable{cursor:pointer;}#mermaid-svg-Z1IlUDevxq9z6AmL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Z1IlUDevxq9z6AmL .arrowheadPath{fill:#333333;}#mermaid-svg-Z1IlUDevxq9z6AmL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Z1IlUDevxq9z6AmL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Z1IlUDevxq9z6AmL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Z1IlUDevxq9z6AmL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Z1IlUDevxq9z6AmL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Z1IlUDevxq9z6AmL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Z1IlUDevxq9z6AmL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Z1IlUDevxq9z6AmL .cluster text{fill:#333;}#mermaid-svg-Z1IlUDevxq9z6AmL .cluster span{color:#333;}#mermaid-svg-Z1IlUDevxq9z6AmL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Z1IlUDevxq9z6AmL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Z1IlUDevxq9z6AmL rect.text{fill:none;stroke-width:0;}#mermaid-svg-Z1IlUDevxq9z6AmL .icon-shape,#mermaid-svg-Z1IlUDevxq9z6AmL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Z1IlUDevxq9z6AmL .icon-shape p,#mermaid-svg-Z1IlUDevxq9z6AmL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Z1IlUDevxq9z6AmL .icon-shape .label rect,#mermaid-svg-Z1IlUDevxq9z6AmL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Z1IlUDevxq9z6AmL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Z1IlUDevxq9z6AmL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Z1IlUDevxq9z6AmL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 持续堆积
没有堆积
没有
有
没有
有
用户反馈:某笔业务没生效
队列有堆积吗?
消费者处理太慢或已掉线

看消费速率 & 服务是否存活
发布方日志里有 messageId 吗?
发布失败:连接/权限/序列化问题

看 publisher confirm 回调
消费方日志里有同一个 messageId 吗?
路由问题:routingKey 或 binding 配错
消费时业务抛异常

看是否进入死信队列

动手:用 SpringAMQP 复刻一次支付事件

下面这套代码在课程 mq-demo 工程的基础上改写,把 itcast.fanout 换成业务化的 pay.success 场景。环境:Spring Boot 2.3.9.RELEASE、Java 8、RabbitMQ 3.8。

工程结构

text 复制代码
mq-demo
├── pom.xml                                  # 父工程,聚合 publisher / consumer
├── publisher                                # 事件发布者:支付服务
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java/cn/itcast/mq
│       │   │   ├── PublisherApplication.java
│       │   │   ├── config/PayFanoutConfig.java
│       │   │   ├── domain/PayEvent.java
│       │   │   └── service/PayEventPublisher.java
│       │   └── resources/application.yml
│       └── test/java/cn/itcast/mq/PayEventTest.java
└── consumer                                 # 事件订阅者:订单 / 仓储 / 短信 / 积分
    ├── pom.xml
    └── src
        ├── main
        │   ├── java/cn/itcast/mq
        │   │   ├── ConsumerApplication.java
        │   │   ├── config/PayFanoutConfig.java
        │   │   └── listener/PayEventListener.java
        │   └── resources/application.yml
        └── test/java/cn/itcast/mq/PayEventTest.java

父工程依赖

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.itcast.demo</groupId>
    <artifactId>mq-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <modules>
        <module>publisher</module>
        <module>consumer</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.9.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- AMQP 依赖,内部已包含 RabbitMQ 客户端 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
    </dependencies>
</project>

连接配置

publisher/src/main/resources/application.yml:

yaml 复制代码
logging:
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
spring:
  rabbitmq:
    host: 192.168.150.101 # RabbitMQ 地址
    port: 5672
    username: itcast
    password: 123321
    virtual-host: /

consumer/src/main/resources/application.yml 比发布方多了一个关键配置:

yaml 复制代码
logging:
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
spring:
  rabbitmq:
    host: 192.168.150.101
    port: 5672
    username: itcast
    password: 123321
    virtual-host: /
    listener:
      simple:
        prefetch: 1   # 每次只从队列中预取 1 条,处理完 ack 才拿下一条

prefetch: 1 是削峰的一部分:不设的话 RabbitMQ 会把队列里的消息成批推给某个消费者堆在内存里,看起来消息被"消费"了,实际还在排队,一旦该消费者宕机,这批消息全部要重投。设成 1 才是真正的"能拿几条拿几条"。

事件体与序列化

发布方想在事件里带一个 Java 对象,默认的序列化器是 JDK 原生序列化(二进制、体积大、跨语言读不了),统一换成 JSON:

java 复制代码
package cn.itcast.mq;

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class PublisherApplication {
    public static void main(String[] args) {
        SpringApplication.run(PublisherApplication.class);
    }

    /**
     * 用 JSON 替换默认的 JDK 序列化,发布方和消费方必须一致,否则消费者会抛反序列化异常。
     */
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

消费方同样要注册这个 Bean(ConsumerApplication.java),代码结构一致,此处不重复粘贴。

事件对象建议带上唯一标识和发生时间,这对后面排查链路是刚需:

java 复制代码
package cn.itcast.mq.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 支付成功事件:支付服务发布,下游订阅方消费。
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PayEvent implements Serializable {

    /** 事件唯一标识,用于全链路排查 */
    private String messageId;
    /** 订单 id */
    private Long orderId;
    /** 支付金额(分) */
    private Long amount;
    /** 支付完成时间 */
    private LocalDateTime paidAt;
}

发布方:一笔支付只做两件事

交换机和队列的声明写在发布方一份、消费方一份都可以,生产里习惯由发布方 声明交换机、由消费方声明自己的队列并绑定,谁用谁负责。这里为了代码自包含,两边都放一份声明(重复声明同名的 Bean 不会出问题,参数一致即可):

java 复制代码
package cn.itcast.mq.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 支付成功事件使用 fanout 交换机:一条消息广播给所有绑定队列,
 * 新增订阅方只需要新增自己的队列与绑定,发布方代码零改动。
 */
@Configuration
public class PayFanoutConfig {

    public static final String PAY_EXCHANGE = "pay.exchange";
    public static final String ORDER_QUEUE = "order.queue";
    public static final String WAREHOUSE_QUEUE = "warehouse.queue";
    public static final String SMS_QUEUE = "sms.queue";
    public static final String POINTS_QUEUE = "points.queue";

    @Bean
    public FanoutExchange payExchange() {
        // durable 默认 true,Broker 重启后交换机不丢
        return new FanoutExchange(PAY_EXCHANGE);
    }

    @Bean
    public Queue orderQueue() {
        return new Queue(ORDER_QUEUE);
    }

    @Bean
    public Queue warehouseQueue() {
        return new Queue(WAREHOUSE_QUEUE);
    }

    @Bean
    public Queue smsQueue() {
        return new Queue(SMS_QUEUE);
    }

    @Bean
    public Queue pointsQueue() {
        return new Queue(POINTS_QUEUE);
    }

    @Bean
    public Binding orderBinding(Queue orderQueue, FanoutExchange payExchange) {
        return BindingBuilder.bind(orderQueue).to(payExchange);
    }

    @Bean
    public Binding warehouseBinding(Queue warehouseQueue, FanoutExchange payExchange) {
        return BindingBuilder.bind(warehouseQueue).to(payExchange);
    }

    @Bean
    public Binding smsBinding(Queue smsQueue, FanoutExchange payExchange) {
        return BindingBuilder.bind(smsQueue).to(payExchange);
    }

    @Bean
    public Binding pointsBinding(Queue pointsQueue, FanoutExchange payExchange) {
        return BindingBuilder.bind(pointsQueue).to(payExchange);
    }
}

注意这份配置里已经包含了发布方完全不知道的 points.queue------它就是"加积分需求不用改支付服务"这件事的代码形态。

发布逻辑:

java 复制代码
package cn.itcast.mq.service;

import cn.itcast.mq.config.PayFanoutConfig;
import cn.itcast.mq.domain.PayEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.UUID;

@Slf4j
@Service
@RequiredArgsConstructor
public class PayEventPublisher {

    private final RabbitTemplate rabbitTemplate;

    /**
     * 模拟支付:扣款 50ms + 发布事件 10ms,不等待任何下游。
     * 事件发布失败这里只做了记录,生产环境必须改为本地消息表 + 补偿(见"消息丢失"一节)。
     */
    public void payAndPublish(Long orderId, Long amount) {
        long start = System.currentTimeMillis();

        try {
            Thread.sleep(50); // 模拟调用第三方支付渠道扣款
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        PayEvent event = new PayEvent(
                UUID.randomUUID().toString(),
                orderId,
                amount,
                LocalDateTime.now()
        );

        try {
            // fanout 交换机忽略 routingKey,这里传空串
            rabbitTemplate.convertAndSend(PayFanoutConfig.PAY_EXCHANGE, "", event);
            log.info("支付成功,事件已发布 orderId={} messageId={}", orderId, event.getMessageId());
        } catch (Exception e) {
            log.error("支付成功但事件发布失败,需要补偿 orderId={} messageId={}",
                    orderId, event.getMessageId(), e);
        }

        log.info("支付服务本次耗时:{}ms", System.currentTimeMillis() - start);
    }
}

订阅方:几个服务各干各的

java 复制代码
package cn.itcast.mq.listener;

import cn.itcast.mq.config.PayFanoutConfig;
import cn.itcast.mq.domain.PayEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class PayEventListener {

    /**
     * 订单服务:更新订单状态。
     */
    @RabbitListener(queues = PayFanoutConfig.ORDER_QUEUE)
    public void listenOrderQueue(PayEvent event) throws InterruptedException {
        Thread.sleep(150); // 模拟订单表更新耗时
        log.info("[订单服务] 订单状态已更新 orderId={} messageId={}",
                event.getOrderId(), event.getMessageId());
    }

    /**
     * 仓储服务:扣减库存、创建发货单。
     */
    @RabbitListener(queues = PayFanoutConfig.WAREHOUSE_QUEUE)
    public void listenWarehouseQueue(PayEvent event) throws InterruptedException {
        Thread.sleep(200); // 模拟仓储接口较慢
        log.info("[仓储服务] 库存已扣减、发货单已创建 orderId={} messageId={}",
                event.getOrderId(), event.getMessageId());
    }

    /**
     * 短信服务:调用短信网关。
     */
    @RabbitListener(queues = PayFanoutConfig.SMS_QUEUE)
    public void listenSmsQueue(PayEvent event) throws InterruptedException {
        Thread.sleep(100); // 模拟短信网关耗时
        log.info("[短信服务] 短信已发送 orderId={} messageId={}",
                event.getOrderId(), event.getMessageId());
    }

    /**
     * 积分服务:后加的业务,支付服务对此一无所知。
     */
    @RabbitListener(queues = PayFanoutConfig.POINTS_QUEUE)
    public void listenPointsQueue(PayEvent event) {
        log.info("[积分服务] 积分已增加 orderId={} amount={} messageId={}",
                event.getOrderId(), event.getAmount(), event.getMessageId());
    }
}

跑起来看效果

先启动 RabbitMQ(课程虚拟机的环境):

bash 复制代码
docker start mq
# 未容器化的环境:systemctl start rabbitmq-server

然后依次启动 ConsumerApplication 和 PublisherApplication,跑这个测试:

java 复制代码
package cn.itcast.mq;

import cn.itcast.mq.config.PayFanoutConfig;
import cn.itcast.mq.domain.PayEvent;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.time.LocalDateTime;
import java.util.UUID;

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class PayEventTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 单笔支付:支付服务在 60ms 左右返回,下游四个服务在后台各干各的。
     */
    @Test
    public void testSendPayEvent() {
        long start = System.currentTimeMillis();
        PayEvent event = new PayEvent(UUID.randomUUID().toString(), 1001L, 9900L, LocalDateTime.now());
        rabbitTemplate.convertAndSend(PayFanoutConfig.PAY_EXCHANGE, "", event);
        log.info("发布耗时:{}ms", System.currentTimeMillis() - start);
    }

    /**
     * 秒杀削峰:1 秒内灌 5000 条事件,观察队列堆积与消费速率。
     */
    @Test
    public void testSeckillBurst() {
        long start = System.currentTimeMillis();
        for (int i = 1; i <= 5000; i++) {
            PayEvent event = new PayEvent(UUID.randomUUID().toString(), 100000L + i, 9900L, LocalDateTime.now());
            rabbitTemplate.convertAndSend(PayFanoutConfig.PAY_EXCHANGE, "", event);
        }
        log.info("5000 条事件全部投递完成,耗时:{}ms", System.currentTimeMillis() - start);
    }
}

控制台的典型输出:

text 复制代码
支付成功,事件已发布 orderId=1001 messageId=6b1f...
支付服务本次耗时:61ms
[订单服务] 订单状态已更新 orderId=1001 messageId=6b1f...
[积分服务] 积分已增加 orderId=1001 amount=9900 messageId=6b1f...
[短信服务] 短信已发送 orderId=1001 messageId=6b1f...
[仓储服务] 库存已扣减、发货单已创建 orderId=1001 messageId=6b1f...

支付服务的 61ms 和仓储服务的 200ms 完全解耦,四个消费者谁先完成取决于各自的模拟耗时。

削峰实验跑了之后,另一个终端看一下堆积:

bash 复制代码
rabbitmqctl list_queues name messages_ready messages_unacknowledged consumers
# 或打开 http://192.168.150.101:15672 看队列曲线的锯齿状上升-回落

实测数据大致是这样:

指标 观测值
5000 条事件投递到 Broker 约 700~900ms
order.queue 瞬时堆积 接近 5000 条
订单服务消费速率 约 1000/150 ≈ 6~7 条/秒(单消费者单线程)
全部消化所需时间 约 12 分钟

发布方几乎瞬间完成,压力全部落在队列里,下游按自己的节奏慢慢吃------这就是削峰。如果嫌 12 分钟太久,正确的做法是给订单服务多开几个消费者进程(同一队列的多个消费者是竞争关系,消息不会重复投递到两个消费者手上),而不是让发布方慢下来。

这里还有个坑:prefetch 默认不限量时,这 5000 条会被一次性推给唯一那个消费者堆在内存里,管理页面上 Unacked 会飙到几千,看着像消费了其实还在排队,这时只看队列长度会误判。取值给个经验:消费者单条处理 50ms 意味着理论 20 条/秒,prefetch 设 1~5 就够,再大只是徒增内存占用和宕机时的重投成本。

什么时候别异步化

判断标准只有一句话:你需不需要马上拿到对方的结果。
#mermaid-svg-Y7KTFwpTw8PnS6Q5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .error-icon{fill:#552222;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .marker.cross{stroke:#333333;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 p{margin:0;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .cluster-label text{fill:#333;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .cluster-label span{color:#333;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .cluster-label span p{background-color:transparent;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .label text,#mermaid-svg-Y7KTFwpTw8PnS6Q5 span{fill:#333;color:#333;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .node rect,#mermaid-svg-Y7KTFwpTw8PnS6Q5 .node circle,#mermaid-svg-Y7KTFwpTw8PnS6Q5 .node ellipse,#mermaid-svg-Y7KTFwpTw8PnS6Q5 .node polygon,#mermaid-svg-Y7KTFwpTw8PnS6Q5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .rough-node .label text,#mermaid-svg-Y7KTFwpTw8PnS6Q5 .node .label text,#mermaid-svg-Y7KTFwpTw8PnS6Q5 .image-shape .label,#mermaid-svg-Y7KTFwpTw8PnS6Q5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .rough-node .label,#mermaid-svg-Y7KTFwpTw8PnS6Q5 .node .label,#mermaid-svg-Y7KTFwpTw8PnS6Q5 .image-shape .label,#mermaid-svg-Y7KTFwpTw8PnS6Q5 .icon-shape .label{text-align:center;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .node.clickable{cursor:pointer;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .arrowheadPath{fill:#333333;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .cluster text{fill:#333;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .cluster span{color:#333;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .icon-shape,#mermaid-svg-Y7KTFwpTw8PnS6Q5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .icon-shape p,#mermaid-svg-Y7KTFwpTw8PnS6Q5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .icon-shape .label rect,#mermaid-svg-Y7KTFwpTw8PnS6Q5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Y7KTFwpTw8PnS6Q5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 需要
不需要
没有,量很小
有,或希望彻底解耦
不能
能
一次跨服务调用
需要对方的返回值

继续走下面的业务逻辑吗?
同步调用

Feign
对并发/吞吐量

有较高要求吗?
同步调用

别为了用 MQ 而用 MQ
能接受最终一致

与秒级延迟吗?
异步通讯

Broker

拆开看,下面这些场景不适合异步:

场景 原因
查询类操作(查订单后查用户、查商品后查库存余额) 要立即用返回值,异步拿不到结果
强一致要求的资金操作(下单扣库存的实时校验、风控实时判定) 异步只保证最终一致,中间窗口可能出现超卖
调用频率极低、下游就一两个的内部管理后台功能 引入 MQ 的运维成本远大于收益
团队没有 Broker 的运维能力(没人值守集群、没监控、没告警) 新增一个你自己兜不住的单点
消息顺序强相关的业务且没做顺序保障 异步天然乱序,除非单队列单消费者

反过来,适合异步的场景特征很鲜明:

  • 主流程做完就可以给用户结果,剩下的事情可以慢慢来(支付后发货、注册后送券、审核后推送)
  • 一个动作触发多个互不相干的下游,且下游数量随时会变(一次发布、多方订阅)
  • 流量有明显的波峰波谷,且下游扛不住峰值(秒杀、抢券、日志采集)
  • 下游允许延迟、允许重试、天然幂等(短信、推送、生成报表)

补充一句大实话:日常开发里同步调用占绝大多数。多数业务的并发压力没大到需要 MQ,而对时效性的要求往往是刚性的。异步是解决问题用的工具,不是架构先进性的证明。

API 速览

类 / 注解 作用 典型用法
RabbitTemplate SpringAMQP 的消息发送入口 convertAndSend(exchange, routingKey, object)
@RabbitListener 声明一个消费者方法 @RabbitListener(queues = "order.queue")
@QueueBinding / @Queue / @Exchange 在注解里一次性声明队列、交换机和绑定关系 见 SpringRabbitListener 的 topic/direct 写法
FanoutExchange 广播交换机,一条消息投递给所有绑定队列 支付成功事件的标准选型
DirectExchange 按 routingKey 精确匹配投递 按级别分发告警消息
TopicExchange 按 routingKey 通配符匹配(* 一个词,# 零或多个词) china.#、#.news
Jackson2JsonMessageConverter 用 JSON 替换 JDK 序列化 注册为 MessageConverter Bean
SimpleMessageListenerContainer 通过 yml listener.simple 配置的监听容器 prefetch、concurrency、acknowledge-mode

官方文档

总结

异步通讯的价值在于把"调用"换成"通知",顺手抹掉了同步调用的四笔账:耦合度高、吞吐下降、资源浪费、级联失败,还额外附赠了流量削峰。手段是引入 Broker:发布者只管往总线上扔事件,订阅者自己去订阅,互不感知。

代价同样明确,别只记优点:

  • Broker 变成整个架构里的单点,对它可用性、并发能力、可靠性的要求远高于任何一个业务服务
  • 拿不到下游执行结果,"我查完马上要用"这类场景异步直接不适用
  • 丢失、重复、乱序这三件事必须由业务兜底(持久化 + confirm + 手动 ack + 幂等 + 补偿),不是配几个参数就能完事
  • 调用链断了,排查靠 messageId 串联日志,靠队列堆积量判断是哪一层出了问题

选型时先问一句话:这次调用要不要等结果? 要,同步;不要,再看有没有吞吐压力或解耦诉求,有才上异步。剩下的场景别硬套。

下一篇进入 RabbitMQ 本体,先把单机起来、把几个核心概念的对应关系(channel / exchange / queue / virtual host)厘清,再用最简模型把消息跑通。

相关推荐
用户094248568031 小时前
第31章:JVM字节码解释器与模板解释器源码路径
java·jvm
子非鱼a1 小时前
【WEB】EasySSTI
java·开发语言·前端
m0_587383001 小时前
深圳 24 小时自助健身房系统软件开发实战指南与案例解析
java·spring boot·小程序·架构·需求分析
Nebula_g1 小时前
JavaSE项目实践:红包雨-线程池/线程
java·开发语言
小羊没烦恼!1 小时前
jQuery1.5的改进细节
java·服务器·开发语言·前端·c#
霸道流氓气质1 小时前
Prompt 版本控制与 A/B 测试实战:语义版本管理、一致性哈希分流与 Thompson Sampling 的 Java 生产级实现
java·prompt·哈希算法
嵌入式er2 小时前
影石系统组 一二面OC面经
java·开发语言
春涧草茶2 小时前
慢就是快12-12手动抛出异常
java·linux·前端
企业数字化笔记3 小时前
AI工具参数很多怎么办?预设、表单校验、危险参数与配置审计
java·spring boot·python·音视频