概述
上一篇把同步调用的账算了一遍:支付服务用 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)厘清,再用最简模型把消息跑通。