概述
事件驱动架构里,Broker 是单点命门:它一挂,上下游服务全都断链。ActiveMQ、RabbitMQ、RocketMQ、Kafka 四款 MQ 到底差在哪,面试常问,落地更常踩。这篇用一张横向对比表、四款逐一点评、一条可执行的决策路径,把选型这件事讲透,并给出可运行代码验证 RabbitMQ 的上手成本。
纲要
- MQ 在架构里的位置:事件驱动架构中的 Broker,承担解耦、削峰、故障隔离
- 四款 MQ 横向对比 :公司/社区、开发语言、协议支持、可用性、单机吞吐量、消息延迟、消息可靠性
- ActiveMQ(Apache / Java / 万级)
- RabbitMQ(Rabbit / Erlang / 万级 / 微秒级)
- RocketMQ(阿里 → Apache / Java / 十万级)
- Kafka(Apache / Scala&Java / 百万级)
- 吞吐量为啥差两个数量级:顺序写盘、零拷贝、批量压缩、副本同步策略
- Kafka 的 ISR 副本机制 :
acks=all+min.insync.replicas+unclean.leader.election.enable - 消息不丢的三段链路:生产者 → Broker → 消费者
- 多语言客户端支持:AMQP 是 RabbitMQ 的护城河
- 选型决策路径:Mermaid 决策流程图 + 场景对照表
- 动手验证:mq-demo 工程(原生 AMQP 客户端 + SpringAMQP)+ Kafka 生产者对照代码
MQ 到底在解决什么问题
同步调用(Feign 那套)的问题很具体:耦合度高、吞吐被最慢的那一环拖住、调用链上有个服务挂了就级联失败。异步通讯把这些问题的承载方换成了 Broker------发布者只管把事件丢给 Broker,订阅者按自己的节奏消费。
#mermaid-svg-MbmHysyeMtWf6FPb{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-MbmHysyeMtWf6FPb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MbmHysyeMtWf6FPb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MbmHysyeMtWf6FPb .error-icon{fill:#552222;}#mermaid-svg-MbmHysyeMtWf6FPb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MbmHysyeMtWf6FPb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MbmHysyeMtWf6FPb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MbmHysyeMtWf6FPb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MbmHysyeMtWf6FPb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MbmHysyeMtWf6FPb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MbmHysyeMtWf6FPb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MbmHysyeMtWf6FPb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MbmHysyeMtWf6FPb .marker.cross{stroke:#333333;}#mermaid-svg-MbmHysyeMtWf6FPb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MbmHysyeMtWf6FPb p{margin:0;}#mermaid-svg-MbmHysyeMtWf6FPb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MbmHysyeMtWf6FPb .cluster-label text{fill:#333;}#mermaid-svg-MbmHysyeMtWf6FPb .cluster-label span{color:#333;}#mermaid-svg-MbmHysyeMtWf6FPb .cluster-label span p{background-color:transparent;}#mermaid-svg-MbmHysyeMtWf6FPb .label text,#mermaid-svg-MbmHysyeMtWf6FPb span{fill:#333;color:#333;}#mermaid-svg-MbmHysyeMtWf6FPb .node rect,#mermaid-svg-MbmHysyeMtWf6FPb .node circle,#mermaid-svg-MbmHysyeMtWf6FPb .node ellipse,#mermaid-svg-MbmHysyeMtWf6FPb .node polygon,#mermaid-svg-MbmHysyeMtWf6FPb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MbmHysyeMtWf6FPb .rough-node .label text,#mermaid-svg-MbmHysyeMtWf6FPb .node .label text,#mermaid-svg-MbmHysyeMtWf6FPb .image-shape .label,#mermaid-svg-MbmHysyeMtWf6FPb .icon-shape .label{text-anchor:middle;}#mermaid-svg-MbmHysyeMtWf6FPb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MbmHysyeMtWf6FPb .rough-node .label,#mermaid-svg-MbmHysyeMtWf6FPb .node .label,#mermaid-svg-MbmHysyeMtWf6FPb .image-shape .label,#mermaid-svg-MbmHysyeMtWf6FPb .icon-shape .label{text-align:center;}#mermaid-svg-MbmHysyeMtWf6FPb .node.clickable{cursor:pointer;}#mermaid-svg-MbmHysyeMtWf6FPb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MbmHysyeMtWf6FPb .arrowheadPath{fill:#333333;}#mermaid-svg-MbmHysyeMtWf6FPb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MbmHysyeMtWf6FPb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MbmHysyeMtWf6FPb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MbmHysyeMtWf6FPb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MbmHysyeMtWf6FPb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MbmHysyeMtWf6FPb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MbmHysyeMtWf6FPb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MbmHysyeMtWf6FPb .cluster text{fill:#333;}#mermaid-svg-MbmHysyeMtWf6FPb .cluster span{color:#333;}#mermaid-svg-MbmHysyeMtWf6FPb 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-MbmHysyeMtWf6FPb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MbmHysyeMtWf6FPb rect.text{fill:none;stroke-width:0;}#mermaid-svg-MbmHysyeMtWf6FPb .icon-shape,#mermaid-svg-MbmHysyeMtWf6FPb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MbmHysyeMtWf6FPb .icon-shape p,#mermaid-svg-MbmHysyeMtWf6FPb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MbmHysyeMtWf6FPb .icon-shape .label rect,#mermaid-svg-MbmHysyeMtWf6FPb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MbmHysyeMtWf6FPb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MbmHysyeMtWf6FPb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MbmHysyeMtWf6FPb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 同步调用 Feign
同步调用 Feign
阻塞等待
阻塞等待
发布事件
订阅
订阅
订阅
订单服务
支付服务
仓储服务
订单服务
Broker / MQ
支付服务
仓储服务
通知服务
右边那张图就是事件驱动。代价也很明确:架构复杂了,业务流程不再是直线,而且整个系统的可靠性被押在 Broker 身上。所以选 MQ 本质上是在挑这四件事:
| 维度 | 为什么重要 |
|---|---|
| 可用性 | Broker 不能单点,得能集群、能故障转移 |
| 单机吞吐量 | 决定削峰能扛多大流量,以及你要堆几台机器 |
| 消息延迟 | 业务异步通知能接受毫秒,实时竞价可能只接受微秒 |
| 消息可靠性 | 订单消息丢一条就是资损,日志丢几条无所谓 |
四款 MQ 横向对比
下面这张是核心结论表:
| RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
|---|---|---|---|---|
| 公司/社区 | Rabbit(现属 Broadcom/VMware 体系) | Apache | 阿里,后捐赠 Apache(主要维护者仍是阿里) | Apache(原 LinkedIn 开源) |
| 开发语言 | Erlang | Java | Java | Scala & Java |
| 协议支持 | AMQP、XMPP、SMTP、STOMP | OpenWire、STOMP、REST、XMPP、AMQP | 自定义协议 | 自定义协议 |
| 客户端语言 | Java、Python、Go、.NET、Node.js、Ruby、PHP、Erlang 等,官方维护多语言库 | 以 Java 为主,另有 C++、.NET、Python 等 | Java 为主,另有 C/C++、Go、Python(社区活跃度参差) | Java/Scala 官方,Go(Sarama)、Python(confluent-kafka)、C/C++(librdkafka)、Node.js(kafkajs)由社区/Confluent 维护 |
| 可用性 | 高(主从/镜像集群、Quorum 队列) | 一般 | 高(多主多从、Dledger) | 高(分区多副本 + ISR) |
| 单机吞吐量 | 一般,万级 QPS | 差,万级 QPS 且堆积时掉得厉害 | 高,十万级 QPS | 非常高,百万级 QPS(极限压测) |
| 消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
| 消息可靠性 | 高 | 一般 | 高 | 一般(默认配置易丢,可配置到不丢) |
| 典型场景 | 业务解耦、任务分发、RPC 异步化 | 遗留系统、小规模内部集成 | 电商交易、金融级业务消息、定时/事务消息 | 日志采集、埋点、流计算、大数据管道 |
一句话归纳讲义的结论:
- 追求可用性:Kafka、RocketMQ、RabbitMQ
- 追求可靠性:RabbitMQ、RocketMQ
- 追求吞吐能力:RocketMQ、Kafka
- 追求低延迟:RabbitMQ、Kafka
吞吐量差两个数量级的原因
面试时只背"Kafka 百万级、RabbitMQ 万级"是不够的,得说得出为什么:
#mermaid-svg-l7A5P6mFK163EAHH{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-l7A5P6mFK163EAHH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-l7A5P6mFK163EAHH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-l7A5P6mFK163EAHH .error-icon{fill:#552222;}#mermaid-svg-l7A5P6mFK163EAHH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-l7A5P6mFK163EAHH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-l7A5P6mFK163EAHH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-l7A5P6mFK163EAHH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-l7A5P6mFK163EAHH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-l7A5P6mFK163EAHH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-l7A5P6mFK163EAHH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-l7A5P6mFK163EAHH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-l7A5P6mFK163EAHH .marker.cross{stroke:#333333;}#mermaid-svg-l7A5P6mFK163EAHH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-l7A5P6mFK163EAHH p{margin:0;}#mermaid-svg-l7A5P6mFK163EAHH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-l7A5P6mFK163EAHH .cluster-label text{fill:#333;}#mermaid-svg-l7A5P6mFK163EAHH .cluster-label span{color:#333;}#mermaid-svg-l7A5P6mFK163EAHH .cluster-label span p{background-color:transparent;}#mermaid-svg-l7A5P6mFK163EAHH .label text,#mermaid-svg-l7A5P6mFK163EAHH span{fill:#333;color:#333;}#mermaid-svg-l7A5P6mFK163EAHH .node rect,#mermaid-svg-l7A5P6mFK163EAHH .node circle,#mermaid-svg-l7A5P6mFK163EAHH .node ellipse,#mermaid-svg-l7A5P6mFK163EAHH .node polygon,#mermaid-svg-l7A5P6mFK163EAHH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-l7A5P6mFK163EAHH .rough-node .label text,#mermaid-svg-l7A5P6mFK163EAHH .node .label text,#mermaid-svg-l7A5P6mFK163EAHH .image-shape .label,#mermaid-svg-l7A5P6mFK163EAHH .icon-shape .label{text-anchor:middle;}#mermaid-svg-l7A5P6mFK163EAHH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-l7A5P6mFK163EAHH .rough-node .label,#mermaid-svg-l7A5P6mFK163EAHH .node .label,#mermaid-svg-l7A5P6mFK163EAHH .image-shape .label,#mermaid-svg-l7A5P6mFK163EAHH .icon-shape .label{text-align:center;}#mermaid-svg-l7A5P6mFK163EAHH .node.clickable{cursor:pointer;}#mermaid-svg-l7A5P6mFK163EAHH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-l7A5P6mFK163EAHH .arrowheadPath{fill:#333333;}#mermaid-svg-l7A5P6mFK163EAHH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-l7A5P6mFK163EAHH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-l7A5P6mFK163EAHH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-l7A5P6mFK163EAHH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-l7A5P6mFK163EAHH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-l7A5P6mFK163EAHH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-l7A5P6mFK163EAHH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-l7A5P6mFK163EAHH .cluster text{fill:#333;}#mermaid-svg-l7A5P6mFK163EAHH .cluster span{color:#333;}#mermaid-svg-l7A5P6mFK163EAHH 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-l7A5P6mFK163EAHH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-l7A5P6mFK163EAHH rect.text{fill:none;stroke-width:0;}#mermaid-svg-l7A5P6mFK163EAHH .icon-shape,#mermaid-svg-l7A5P6mFK163EAHH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-l7A5P6mFK163EAHH .icon-shape p,#mermaid-svg-l7A5P6mFK163EAHH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-l7A5P6mFK163EAHH .icon-shape .label rect,#mermaid-svg-l7A5P6mFK163EAHH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-l7A5P6mFK163EAHH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-l7A5P6mFK163EAHH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-l7A5P6mFK163EAHH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Kafka / RocketMQ
RabbitMQ
生产者批量发送
Broker 写盘方式
append 顺序写 PageCache
批量压缩 + 索引稀疏
消费时 sendfile 零拷贝
磁盘顺序读接近内存速度
per-message 路由与 ACK
exchange 按 binding 匹配分发
每条消息独立确认 + 落盘/刷盘
吞吐受限于单队列串行与确认开销
几个关键点:
- 顺序写 + PageCache:Kafka 和 RocketMQ 把消息当日志追加写,磁盘顺序写的吞吐能接近内存随机写。RabbitMQ 的消息经过 exchange 路由、per-message 的确认与持久化,单队列内部是串行的。
- 零拷贝 :Kafka 消费走
sendfile,数据从 PageCache 直接进网卡,不经过用户态拷贝两遍。这是它能做到每秒数百 MB 读出的核心。 - 批量与压缩 :Kafka 生产者默认会攒批(
linger.ms、batch.size)再发,配合snappy/lz4压缩,网络与磁盘 IO 都被摊薄。这也直接牺牲了单条消息的延迟------所以 Kafka 是毫秒级,RabbitMQ 是微秒级。 - 副本策略:老版本 RabbitMQ 的镜像队列是同步复制到所有镜像节点,副本越多越慢;Kafka 只要求 ISR 集合内落盘即可确认,写出来快但极端情况下可能丢。
数字要带条件看:单机百万是消息体很小、批量发送、硬件不错的极限压测值。真实业务里按字节吞吐更实际------Kafka 单机能到每秒数百 MB,RocketMQ 大约每秒百 MB 量级,RabbitMQ 大约每秒几十 MB。多数公司每秒几十 MB 根本用不完,这也是 RabbitMQ 够用的底气。
逐款点评
ActiveMQ:能用,但别上新项目
Apache 老牌,Java 写的,协议支持最花哨(OpenWire、STOMP、REST、XMPP、AMQP 全支持),十年前是 Java 生态的默认选项。
坑在哪:
- 单机吞吐是四款里最差的,万级 QPS 起步,消息堆积后性能衰减明显。
- 社区活跃度掉得厉害,新版本迭代慢,遇到问题基本靠自己啃源码。
- 高可用方案(LevelDB + ZooKeeper 主从)在极端网络分区下踩过不少坑。
现在选它只有一个理由:维护存量系统。新项目没有理由选 ActiveMQ,ActiveMQ Artemis 是另一个故事,别混为一谈。
RabbitMQ:吞吐不是最强,但省心
Erlang 写的,这是它最大的争议点也是最大的优势。Erlang 的 Actor 模型和软实时特性,让它在低延迟和并发连接上有天然优势;代价是 Java 工程师想深入定制基本没戏------你不会为了改个 MQ 去学 Erlang。
但真要定制的有几家?多数公司用 MQ 就是用,不是改。这时候 RabbitMQ 的优势就全出来了:
- 协议友好:AMQP 是标准协议、跨平台跨语言。同一套 Broker 能给 Java 服务、Python 脚本、Node.js 前端网关一起用,不用给每种语言找适配层。
- 路由模型强大:exchange 支持 fanout / direct / topic / headers,业务上"按规则分发"这类需求不用写代码,配 binding key 就行。
- 延迟最低:微秒级,做实时通知、秒杀结果回调这类对时延敏感的场景很合适。
- 运维成熟:管理界面开箱即用,队列积压、连接数、ACK 状态一眼能看到。
- 可靠性高:publisher confirm + 消息持久化 + consumer ack 三件套配齐,业务消息基本不丢。
坑也有,直说:
- 吞吐量天花板明确,万级 QPS。日活上来、消息量翻几倍后要分队列、分 vhost、加节点,扩展性不如 Kafka 那种加分区就完事。
- 消息大量堆积时内存压力大,容易触发流控把生产者堵住(生产环境一定要配磁盘告警和队列长度上限)。
- 老版本的镜像队列(ha-all 策略)同步复制性能差且网络分区后行为诡异,3.8 之后优先用 Quorum 队列。
结论:中小规模业务、团队没有专职中间件运维、需要多语言接入------选 RabbitMQ 基本不会错。这也是本文后续 Demo 用它的原因。
RocketMQ:业务消息的国产主力
阿里开源,Java 写的,后来捐给 Apache,主要维护者还是阿里的人。在国内的使用量排前三,电商、金融、交易链路用得最多。
强在哪:
- 十万级吞吐,比 RabbitMQ 高一个数量级,同时可靠性还很好。
- 功能特性是四款里最贴合业务的:事务消息(半消息 + 回查)、定时/延时消息(固定延迟级别)、消息重试与死信队列、消息轨迹、严格的顺序消息。
- Java 写的,源码读得懂,出问题时排查链路短。
坑:
- 客户端生态以 Java 为主,其他语言的客户端完善度一般,跨语言场景要评估。
- 功能多意味着配置项多,nameserver + broker + 主从架构要搞清楚再上生产,不然延时消息的延迟级别配置这种细节能坑你一次。
- 社区文档中文资料多,英文生态弱于 Kafka。
结论:交易、支付、订单这类"消息不能丢、还要事务/延时"的业务场景,RocketMQ 是国内最稳的选择。
Kafka:吞吐之王,但别拿它当业务消息中间件
Scala + Java 写的,LinkedIn 起源,现在是 Apache 顶级项目。设计目标是日志管道和流处理,不是业务消息。
它凭什么百万级:顺序写盘、PageCache、零拷贝、批量压缩、分区并行,前面讲过了。但要理解它的可靠性模型,必须搞懂 ISR。
Kafka 的 ISR 副本机制
#mermaid-svg-LZnOKG99zdrNMMcQ{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-LZnOKG99zdrNMMcQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LZnOKG99zdrNMMcQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LZnOKG99zdrNMMcQ .error-icon{fill:#552222;}#mermaid-svg-LZnOKG99zdrNMMcQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LZnOKG99zdrNMMcQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LZnOKG99zdrNMMcQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LZnOKG99zdrNMMcQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LZnOKG99zdrNMMcQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LZnOKG99zdrNMMcQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LZnOKG99zdrNMMcQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LZnOKG99zdrNMMcQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LZnOKG99zdrNMMcQ .marker.cross{stroke:#333333;}#mermaid-svg-LZnOKG99zdrNMMcQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LZnOKG99zdrNMMcQ p{margin:0;}#mermaid-svg-LZnOKG99zdrNMMcQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LZnOKG99zdrNMMcQ .cluster-label text{fill:#333;}#mermaid-svg-LZnOKG99zdrNMMcQ .cluster-label span{color:#333;}#mermaid-svg-LZnOKG99zdrNMMcQ .cluster-label span p{background-color:transparent;}#mermaid-svg-LZnOKG99zdrNMMcQ .label text,#mermaid-svg-LZnOKG99zdrNMMcQ span{fill:#333;color:#333;}#mermaid-svg-LZnOKG99zdrNMMcQ .node rect,#mermaid-svg-LZnOKG99zdrNMMcQ .node circle,#mermaid-svg-LZnOKG99zdrNMMcQ .node ellipse,#mermaid-svg-LZnOKG99zdrNMMcQ .node polygon,#mermaid-svg-LZnOKG99zdrNMMcQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LZnOKG99zdrNMMcQ .rough-node .label text,#mermaid-svg-LZnOKG99zdrNMMcQ .node .label text,#mermaid-svg-LZnOKG99zdrNMMcQ .image-shape .label,#mermaid-svg-LZnOKG99zdrNMMcQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-LZnOKG99zdrNMMcQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LZnOKG99zdrNMMcQ .rough-node .label,#mermaid-svg-LZnOKG99zdrNMMcQ .node .label,#mermaid-svg-LZnOKG99zdrNMMcQ .image-shape .label,#mermaid-svg-LZnOKG99zdrNMMcQ .icon-shape .label{text-align:center;}#mermaid-svg-LZnOKG99zdrNMMcQ .node.clickable{cursor:pointer;}#mermaid-svg-LZnOKG99zdrNMMcQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LZnOKG99zdrNMMcQ .arrowheadPath{fill:#333333;}#mermaid-svg-LZnOKG99zdrNMMcQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LZnOKG99zdrNMMcQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LZnOKG99zdrNMMcQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LZnOKG99zdrNMMcQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LZnOKG99zdrNMMcQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LZnOKG99zdrNMMcQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LZnOKG99zdrNMMcQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LZnOKG99zdrNMMcQ .cluster text{fill:#333;}#mermaid-svg-LZnOKG99zdrNMMcQ .cluster span{color:#333;}#mermaid-svg-LZnOKG99zdrNMMcQ 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-LZnOKG99zdrNMMcQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LZnOKG99zdrNMMcQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-LZnOKG99zdrNMMcQ .icon-shape,#mermaid-svg-LZnOKG99zdrNMMcQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LZnOKG99zdrNMMcQ .icon-shape p,#mermaid-svg-LZnOKG99zdrNMMcQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LZnOKG99zdrNMMcQ .icon-shape .label rect,#mermaid-svg-LZnOKG99zdrNMMcQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LZnOKG99zdrNMMcQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LZnOKG99zdrNMMcQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LZnOKG99zdrNMMcQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
acks=0
acks=1
acks=all/-1
Producer 发送消息到 Partition Leader
Leader 追加写入本地日志
Follower 持续从 Leader 拉取同步
Follower 是否在 replica.lag.time.max.ms 内追上
留在 ISR 集合 In-Sync Replicas
被踢出 ISR,变为 OSR 落后副本
acks 配置
不等任何确认,最快但必丢
仅 Leader 写入即确认,Leader 挂了就丢
ISR 全部副本写入才确认
配合 min.insync.replicas 保证最低副本数
几个必须记住的点:
- **ISR(In-Sync Replicas)**是当前与 Leader 保持同步的副本集合。Follower 超过
replica.lag.time.max.ms(默认 30s)没追上就被踢出 ISR。 acks=all不等于绝对不丢 :它只保证 ISR 里的副本都写了。如果 ISR 只剩 Leader 一个,acks=all就退化成acks=1。所以必须配min.insync.replicas=2,让 ISR 不足时生产者直接报错,而不是静默写入。unclean.leader.election.enable:默认 false。设成 true 时,ISR 全挂的情况下允许 OSR(落后副本)当选 Leader,此时会丢掉它没同步到的那段消息。追求可靠性就别开。- 消费端也可能"丢":offset 自动提交会在消息还没处理完时就提交位点,消费者重启后这段消息就跳过了。业务消费要手动提交 offset,处理成功再 commit。
所以 Kafka 的"可靠性一般"不是设计缺陷,是默认值偏向吞吐。想要不丢,代价是吞吐下降:
| 配置 | 吞吐 | 可靠性 |
|---|---|---|
acks=0 + 自动提交 offset |
最高 | 会丢,只适合可容忍丢失的埋点 |
acks=1 + 自动提交 offset |
高 | Leader 故障时丢少量 |
acks=all + min.insync.replicas=2 + 手动提交 offset |
中等(仍远高于 RabbitMQ) | 业务级不丢 |
结论:日志采集、用户行为埋点、流计算数据源、大数据管道------Kafka 是唯一正解。拿它做订单业务消息?能做,但你要自己补事务、重试、死信、消息查询这些业务语义,投入产出比不如 RocketMQ。
消息不丢:三段链路都要管
不管选哪款,"消息不丢"都要拆成三段检查,任何一段漏了都是白搭:
| 链路 | RabbitMQ | RocketMQ | Kafka |
|---|---|---|---|
| 生产者 → Broker | publisher confirm / 事务(事务性能差,慎用) | 同步发送 + 重试,或用事务消息 | acks=all + retries + 幂等生产者 |
| Broker 自身 | 消息持久化 + 队列持久化 + Quorum 队列 | 同步刷盘 + 同步复制(Dledger) | ISR + min.insync.replicas,别开 unclean 选举 |
| Broker → 消费者 | 手动 ACK,处理成功再 ack | 消费重试 + 死信队列 | 手动提交 offset,处理完再 commit |
还有一条容易被忽略:就算三段都配好,消费端也可能重复收到消息(网络抖动导致 ACK 丢失、消费者重平衡)。所以消费逻辑必须做幂等,这是设计约束,不是可选优化。
多语言客户端支持
这点在做技术选型时经常被忽略,但踩上就很痛。协议是分水岭:
| MQ | 协议性质 | 跨语言成本 |
|---|---|---|
| RabbitMQ | AMQP 开放标准协议 | 低。官方维护 Java/Python/.NET/Go/Node.js/Ruby 等多语言客户端,各语言实现质量接近 |
| ActiveMQ | OpenWire 为主,兼 STOMP/AMQP/MQTT | 低。想跨语言就用 STOMP 或 MQTT 接入 |
| RocketMQ | 自定义协议(Remoting) | 中。Java 客户端一等公民,其他语言客户端由社区维护,功能覆盖度不一 |
| Kafka | 自定义二进制协议 | 中高。官方 Java/Scala;其他语言靠 librdkafka(C/C++ 内核)或纯语言实现(Sarama、kafkajs),版本跟进有延迟 |
判断标准很简单:如果你的系统里有 Python 数据处理脚本、Go 的边缘服务、Node.js 的 BFF 层要同时接入 MQ,RabbitMQ 的 AMQP 是最省事的。清一色 Java 微服务,RocketMQ 或 Kafka 的客户端短板不构成问题。
选型决策路径
#mermaid-svg-fuGewdYF6agG8xyG{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-fuGewdYF6agG8xyG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fuGewdYF6agG8xyG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fuGewdYF6agG8xyG .error-icon{fill:#552222;}#mermaid-svg-fuGewdYF6agG8xyG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fuGewdYF6agG8xyG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fuGewdYF6agG8xyG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fuGewdYF6agG8xyG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fuGewdYF6agG8xyG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fuGewdYF6agG8xyG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fuGewdYF6agG8xyG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fuGewdYF6agG8xyG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fuGewdYF6agG8xyG .marker.cross{stroke:#333333;}#mermaid-svg-fuGewdYF6agG8xyG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fuGewdYF6agG8xyG p{margin:0;}#mermaid-svg-fuGewdYF6agG8xyG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fuGewdYF6agG8xyG .cluster-label text{fill:#333;}#mermaid-svg-fuGewdYF6agG8xyG .cluster-label span{color:#333;}#mermaid-svg-fuGewdYF6agG8xyG .cluster-label span p{background-color:transparent;}#mermaid-svg-fuGewdYF6agG8xyG .label text,#mermaid-svg-fuGewdYF6agG8xyG span{fill:#333;color:#333;}#mermaid-svg-fuGewdYF6agG8xyG .node rect,#mermaid-svg-fuGewdYF6agG8xyG .node circle,#mermaid-svg-fuGewdYF6agG8xyG .node ellipse,#mermaid-svg-fuGewdYF6agG8xyG .node polygon,#mermaid-svg-fuGewdYF6agG8xyG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fuGewdYF6agG8xyG .rough-node .label text,#mermaid-svg-fuGewdYF6agG8xyG .node .label text,#mermaid-svg-fuGewdYF6agG8xyG .image-shape .label,#mermaid-svg-fuGewdYF6agG8xyG .icon-shape .label{text-anchor:middle;}#mermaid-svg-fuGewdYF6agG8xyG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fuGewdYF6agG8xyG .rough-node .label,#mermaid-svg-fuGewdYF6agG8xyG .node .label,#mermaid-svg-fuGewdYF6agG8xyG .image-shape .label,#mermaid-svg-fuGewdYF6agG8xyG .icon-shape .label{text-align:center;}#mermaid-svg-fuGewdYF6agG8xyG .node.clickable{cursor:pointer;}#mermaid-svg-fuGewdYF6agG8xyG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fuGewdYF6agG8xyG .arrowheadPath{fill:#333333;}#mermaid-svg-fuGewdYF6agG8xyG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fuGewdYF6agG8xyG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fuGewdYF6agG8xyG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fuGewdYF6agG8xyG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fuGewdYF6agG8xyG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fuGewdYF6agG8xyG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fuGewdYF6agG8xyG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fuGewdYF6agG8xyG .cluster text{fill:#333;}#mermaid-svg-fuGewdYF6agG8xyG .cluster span{color:#333;}#mermaid-svg-fuGewdYF6agG8xyG 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-fuGewdYF6agG8xyG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fuGewdYF6agG8xyG rect.text{fill:none;stroke-width:0;}#mermaid-svg-fuGewdYF6agG8xyG .icon-shape,#mermaid-svg-fuGewdYF6agG8xyG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fuGewdYF6agG8xyG .icon-shape p,#mermaid-svg-fuGewdYF6agG8xyG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fuGewdYF6agG8xyG .icon-shape .label rect,#mermaid-svg-fuGewdYF6agG8xyG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fuGewdYF6agG8xyG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fuGewdYF6agG8xyG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fuGewdYF6agG8xyG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 每秒数十 MB 以上
或百万级 TPS
每秒几十 MB 以内
是,不能丢
需要事务/延时消息
否,日志/埋点/流计算
是
否
是
否,纯异步解耦
有,且追求极致吞吐
没有,优先稳定省心
存量系统已在用
开始选型
消息量级?
是否业务交易消息?
是否需要多语言接入
或复杂路由规则?
RocketMQ
Kafka
RabbitMQ
是否需要
事务消息/延时消息/消息轨迹?
RocketMQ
团队是否有
中间件运维能力?
Kafka
RabbitMQ
ActiveMQ
仅维护,新项目不选
场景对照,直接查表更快:
| 场景 | 推荐 | 理由 |
|---|---|---|
| 中小规模业务解耦、异步任务 | RabbitMQ | 万级吞吐够用,微秒级延迟,协议友好,运维省心 |
| 电商交易、支付、订单 | RocketMQ | 十万级吞吐 + 事务消息 + 延时消息 + 高可靠 |
| 日志采集、埋点上报、流计算 | Kafka | 百万级吞吐,生态(Connect/Flink/Spark)最全 |
| 定时通知、延迟任务 | RocketMQ / RabbitMQ(延迟插件) | 原生延时消息 vs 插件实现,选型看是否已有 MQ |
| 遗留系统集成 | ActiveMQ | 只在存量系统里保留 |
工程师视角的取舍判断,说白了三条:
- RabbitMQ 吞吐不是最强,但生态与协议友好,中小规模业务够用且运维省心。 别为了"将来可能上量"提前上 Kafka,那个将来的运维成本你现在就在付。
- RocketMQ 是业务消息与高吞吐之间的平衡点,前提是你接受 Java 单语言生态。
- Kafka 是数据管道,不是业务队列。 用它做业务消息要自己补齐事务、重试、死信这些语义,那部分工作量远比想象中大。
动手:用 mq-demo 验证 RabbitMQ 的上手成本
工程结构(源码来自课程 Demo):
tree
mq-demo
├── pom.xml # 父工程,Spring Boot 2.3.9,聚合 publisher / consumer
├── publisher # 消息发送方
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java/cn/itcast/mq/PublisherApplication.java
│ │ └── resources/application.yml
│ └── test/java/cn/itcast/mq
│ ├── helloworld/PublisherTest.java # 原生 AMQP 客户端
│ └── spring/SpringAmqpTest.java # SpringAMQP 模板
└── consumer # 消息消费方
├── pom.xml
└── src
├── main
│ ├── java/cn/itcast/mq
│ │ ├── ConsumerApplication.java
│ │ ├── config/FanoutConfig.java # 声明交换机与队列
│ │ └── listener/SpringRabbitListener.java
│ └── resources/application.yml
└── test/java/cn/itcast/mq/helloworld/ConsumerTest.java
父工程依赖,三行就能跑起来:
xml
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--AMQP 依赖,包含 RabbitMQ 客户端与 Spring 抽象层-->
<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>
连接配置(application.yml,publisher 与 consumer 各一份):
yaml
logging:
pattern:
dateformat: MM-dd HH:mm:ss:SSS
spring:
rabbitmq:
host: 192.168.150.101 # RabbitMQ 服务地址
port: 5672 # AMQP 端口,管理界面是 15672
username: itcast
password: 123321
virtual-host: / # 虚拟主机,用于隔离不同租户的 exchange/queue
原生 AMQP 客户端:看清底层四步
java
package cn.itcast.mq.helloworld;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class PublisherTest {
@Test
public void testSendMessage() throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.150.101");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("itcast");
factory.setPassword("123321");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道 Channel(复用 TCP 连接,避免频繁建连)
Channel channel = connection.createChannel();
// 3.声明队列,幂等操作,已存在则不会重复创建
// durable 直接给 true:后面的可靠性演示会用持久化队列,这里一步到位声明成 durable=true,
// 避免中途改参数触发 PRECONDITION_FAILED inequivalent arg 'durable'
String queueName = "simple.queue";
channel.queueDeclare(queueName, true, false, false, null);
// 4.发送消息,空交换机名表示走默认交换机,按 routingKey 直投队列
String message = "hello, rabbitmq!";
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println("发送消息成功:【" + message + "】");
// 5.关闭通道和连接
channel.close();
connection.close();
}
}
java
package cn.itcast.mq.helloworld;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConsumerTest {
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.150.101");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("itcast");
factory.setPassword("123321");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道
Channel channel = connection.createChannel();
// 3.声明队列(消费者侧也要声明,防止启动顺序问题)
// 与发送侧保持一致,durable=true
String queueName = "simple.queue";
channel.queueDeclare(queueName, true, false, false, null);
// 4.订阅消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
// 5.处理消息
String message = new String(body);
System.out.println("接收到消息:【" + message + "】");
}
});
System.out.println("等待接收消息。。。。");
}
}
注意 basicConsume 的第二个参数是 autoAck=true,消息一到就自动确认,消费过程中抛异常这条消息就没了。生产代码要改成 false,处理成功后手动 basicAck。这是 RabbitMQ 最常见的丢消息来源。
SpringAMQP:业务里真正该用的写法
原生客户端是理解原理用的,业务代码用 RabbitTemplate + @RabbitListener,声明式绑定交换机和队列:
java
package cn.itcast.mq.spring;
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;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendMessage2SimpleQueue() {
String queueName = "simple.queue";
String message = "hello, spring amqp!";
rabbitTemplate.convertAndSend(queueName, message);
}
@Test
public void testSendMessage2WorkQueue() throws InterruptedException {
String queueName = "simple.queue";
String message = "hello, message__";
for (int i = 1; i <= 50; i++) {
rabbitTemplate.convertAndSend(queueName, message + i);
Thread.sleep(20);
}
}
@Test
public void testSendFanoutExchange() {
// 交换机名称
String exchangeName = "itcast.fanout";
// 消息
String message = "hello, every one!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "", message);
}
@Test
public void testSendDirectExchange() {
// 交换机名称
String exchangeName = "itcast.direct";
// 消息
String message = "hello, red!";
// 发送消息,routingKey = red
rabbitTemplate.convertAndSend(exchangeName, "red", message);
}
@Test
public void testSendTopicExchange() {
// 交换机名称
String exchangeName = "itcast.topic";
// 消息
String message = "今天天气不错,我的心情好极了!";
// 发送消息,routingKey 支持通配符匹配
rabbitTemplate.convertAndSend(exchangeName, "china.weather", message);
}
}
java
package cn.itcast.mq.listener;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.time.LocalTime;
import java.util.Map;
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue1(String msg) throws InterruptedException {
System.out.println("消费者1接收到消息:【" + msg + "】" + LocalTime.now());
Thread.sleep(20);
}
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue2(String msg) throws InterruptedException {
System.err.println("消费者2........接收到消息:【" + msg + "】" + LocalTime.now());
Thread.sleep(200);
}
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) {
System.out.println("消费者接收到fanout.queue1的消息:【" + msg + "】");
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) {
System.out.println("消费者接收到fanout.queue2的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"red", "blue"}
))
public void listenDirectQueue1(String msg) {
System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenTopicQueue1(String msg) {
System.out.println("消费者接收到topic.queue1的消息:【" + msg + "】");
}
@RabbitListener(queues = "object.queue")
public void listenObjectQueue(Map<String, Object> msg) {
System.out.println("接收到object.queue的消息:" + msg);
}
}
交换机与队列也可以用 Java Config 声明,适合需要在启动时统一管理的场景:
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;
@Configuration
public class FanoutConfig {
// itcast.fanout
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("itcast.fanout");
}
// fanout.queue1
@Bean
public Queue fanoutQueue1() {
return new Queue("fanout.queue1");
}
// 绑定队列1到交换机
@Bean
public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
return BindingBuilder
.bind(fanoutQueue1)
.to(fanoutExchange);
}
@Bean
public Queue objectQueue() {
return new Queue("object.queue");
}
}
RabbitMQ 侧:开 confirm 与持久化
默认配置下 RabbitMQ 也是"尽力而为",业务不丢消息要显式打开 confirm:
java
package cn.itcast.mq.reliable;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ReliablePublisher {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.150.101");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("itcast");
factory.setPassword("123321");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
String queueName = "simple.queue";
// durable=true,队列持久化
channel.queueDeclare(queueName, true, false, false, null);
// 开启发布确认,Broker 落盘后会回调确认
channel.confirmSelect();
// PERSISTENT_TEXT_PLAIN:deliveryMode=2,消息持久化
channel.basicPublish("", queueName,
MessageProperties.PERSISTENT_TEXT_PLAIN,
"order-paid-event".getBytes());
// 同步等待确认,超过 5 秒抛异常;异步场景可用 ConfirmListener
channel.waitForConfirmsOrDie(5_000);
System.out.println("消息已被 Broker 确认接收");
}
}
}
注意别写成"开了 confirm 就万事大吉":队列声明 durable=true 和消息 deliveryMode=2 是两个独立开关,只开一个照样丢。
Kafka 侧对照:同一件事要配多少东西
java
package cn.itcast.mq.kafka;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.concurrent.Future;
public class ReliableKafkaProducer {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.150.101:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 可靠性三件套:所有 ISR 副本写入才确认 + 重试 + 幂等(避免重试产生重复消息)
props.put(ProducerConfig.ACKS_CONFIG, "all");
props.put(ProducerConfig.RETRIES_CONFIG, 3);
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
// 攒批参数,吞吐与延迟的取舍点
props.put(ProducerConfig.LINGER_MS_CONFIG, 5);
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 32 * 1024);
props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4");
try (KafkaProducer<String, String> producer = new KafkaProducer<>(props)) {
Future<RecordMetadata> future =
producer.send(new ProducerRecord<>("order-paid", "10086", "order-paid-event"));
RecordMetadata metadata = future.get();
System.out.println("写入成功:partition=" + metadata.partition()
+ ", offset=" + metadata.offset());
}
}
}
依赖:
xml
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
Broker 侧还要配 min.insync.replicas=2、主题 replication.factor>=3、unclean.leader.election.enable=false,消费端手动提交 offset。对比之下 RabbitMQ 只要 confirmSelect() + 持久化 + 手动 ACK 三个开关------这就是"运维省心"的具体含义,不是空话。
多语言接入:同一个 Broker,Python 直接消费
AMQP 的价值在这里最能体现,下面是 Python 消费同一条队列消息:
python
#!/usr/bin/env python3
# pip install pika
import pika
credentials = pika.PlainCredentials('itcast', '123321')
parameters = pika.ConnectionParameters(
host='192.168.150.101',
port=5672,
virtual_host='/',
credentials=credentials,
)
connection = pika.BlockingConnection(parameters)
channel = connection.channel()
channel.queue_declare(queue='simple.queue', durable=True)
def on_message(ch, method, properties, body):
print('接收到消息:%s' % body.decode('utf-8'))
# 处理成功后手动确认,auto_ack=False 时必做
ch.basic_ack(delivery_tag=method.delivery_tag)
# auto_ack=False,消费失败的消息会被重新投递
channel.basic_consume(queue='simple.queue', on_message_callback=on_message, auto_ack=False)
print('等待接收消息...')
channel.start_consuming()
同样的 AMQP 语义,pika、Java amqp-client、Go amqp091-go 行为一致。换成 Kafka 或 RocketMQ,各语言客户端的能力对齐程度要自己评估。
常见坑清单
autoAck=true:RabbitMQ 消费端默认示例里的写法,消息处理异常直接丢失。生产环境一律手动 ACK。- 只配消息持久化,队列没配
durable:Broker 重启队列就没了,消息自然也没了。 - Kafka 只配
acks=all不配min.insync.replicas:ISR 退化到单副本时静默写入,和acks=1一样会丢。 - Kafka offset 自动提交:消费者重启后消息被跳过,看起来像"丢消息",实际是提前提交位点。
- 拿 Kafka 做业务消息:没有事务消息、没有延时消息(固定级别)、没有消费重试与死信,全要自己造。
- RabbitMQ 消息堆积不管:内存打满触发流控,生产者被阻塞,表现为上游接口突然变慢。要配队列长度上限和磁盘水位告警。
- 老版本镜像队列(ha-all):同步复制 + 网络分区行为诡异,3.8+ 优先 Quorum 队列。
- 迷信吞吐量数字:百万级是极限压测值。真实场景先算字节吞吐,再决定要不要上 Kafka。
API 速览
| 组件 | API / 配置 | 作用 |
|---|---|---|
| RabbitMQ | channel.queueDeclare(queue, durable, exclusive, autoDelete, args) |
声明队列,durable=true 才持久化 |
| RabbitMQ | channel.basicPublish(exchange, routingKey, props, body) |
发送消息,空 exchange 走默认直投 |
| RabbitMQ | MessageProperties.PERSISTENT_TEXT_PLAIN |
deliveryMode=2,消息落盘 |
| RabbitMQ | channel.confirmSelect() / waitForConfirmsOrDie(ms) |
发布确认,确保 Broker 已接收 |
| RabbitMQ | channel.basicConsume(queue, autoAck, callback) |
订阅消息,autoAck=false 需手动 ack |
| Spring AMQP | rabbitTemplate.convertAndSend(exchange, routingKey, obj) |
发送,自动做 JSON 序列化 |
| Spring AMQP | @RabbitListener(bindings = @QueueBinding(...)) |
声明式声明队列/交换机/绑定并监听 |
| Spring AMQP | spring.rabbitmq.host/port/virtual-host |
连接配置,vhost 做租户隔离 |
| Kafka | ProducerConfig.ACKS_CONFIG |
0/1/all,决定可靠性档位 |
| Kafka | min.insync.replicas |
ISR 最小副本数,配 acks=all 才有意义 |
| Kafka | unclean.leader.election.enable |
是否允许落后副本当选 Leader,可靠性场景设 false |
| Kafka | enable.idempotence=true |
幂等生产者,避免重试产生重复消息 |
| RocketMQ | 事务消息、延时消息、消息重试与死信 | 业务消息语义,Kafka 缺失的部分 |
官方文档
- RabbitMQ 官方文档
- RabbitMQ 教程(Hello World / Work Queues / Publish-Subscribe)
- ActiveMQ 官方文档
- RocketMQ 官方文档
- Kafka 官方文档
- Spring AMQP 参考文档
总结
四款 MQ 的差异集中在吞吐量、延迟、可靠性三者的取舍上,没有全能选手:
- 中小规模业务、多语言接入、团队没有专职中间件运维------RabbitMQ。吞吐不是最强,但协议友好、延迟最低、运维省心,万级 QPS 覆盖绝大多数业务。
- 交易支付类业务,需要事务消息、延时消息、消息轨迹------RocketMQ,十万级吞吐和可靠性兼顾,代价是 Java 单语言生态。
- 日志、埋点、流计算------Kafka,百万级吞吐靠顺序写、零拷贝、批量压缩堆出来,代价是默认配置偏吞吐,可靠性要自己配 ISR 那套参数。
- ActiveMQ 只在存量系统里保留,新项目不要选。
两个判断原则,比背表格有用:先看消息语义(要不要事务/延时/严格不丢),再看量级(每秒多少 MB),最后才看吞吐量上限。业务消息用 Kafka、日志管道用 RabbitMQ,是两个方向上都常见的误用。