从 JMS 说起:Queue 与 Topic
聊消息队列,绕不开一堆”规范”:AMQP、JMS、MQTT、STOMP。但它们其实不在一个层面上——有的是网络协议,有的只是编程接口,混在一起看只会越看越乱。这篇顺着 JMS 这条线讲清楚:它是什么、为什么要分 Queue 和 Topic,以及为什么到了 Kafka 这一代,大家干脆把 Queue 砍了。
先分清:协议和 API 不是一回事
这些”规范”大致分两摞。
一摞是网络协议,管字节怎么在网线上走:AMQP(RabbitMQ 的招牌)、MQTT(物联网里极省带宽)、STOMP(文本协议,好调试),以及 Kafka、RocketMQ、Pulsar 各自的私有二进制协议。
另一摞是编程 API 规范,不关心字节怎么传,只规定代码怎么写。JMS 属于这摞——它是 Java 定的一套消息接口标准,不是协议。
这个区别很关键。ActiveMQ 底层可以跑 OpenWire 或 AMQP,但对上层 Java 代码暴露的始终是 JMS 接口。一句话:JMS 管”应用怎么调用”,协议管”字节怎么走”。
JMS 是什么
JMS(Java Message Service)是 Java EE 定的一套消息 API,核心接口就几个:
1 | Connection → Session → Destination(Queue / Topic) |
它的价值在于解耦:业务代码只依赖 javax.jms(或新版 jakarta.jms)接口,具体由谁实现无所谓。真正实现这套接口的产品叫 JMS Provider:
1 | 应用程序 |
好处是换 Provider 基本只改配置,业务逻辑一行不用动。
那谁实现了 JMS?大致是这么个格局:
| 产品 | JMS | 说明 |
|---|---|---|
| ActiveMQ Classic / Artemis | ✅ | 最经典的开源实现,Artemis 支持 JMS 2.0 |
| IBM MQ / TIBCO EMS | ✅ | 商业,金融电信常见 |
| OpenMQ | ✅ | JMS 参考实现 |
| RabbitMQ | ⚠️ | 靠 JMS Client 桥接,非原生 |
| Kafka / RocketMQ / Pulsar | ❌ | 各自的客户端 API,不是 JMS |
Kafka、RocketMQ 不实现 JMS,不是做不到,而是它们的消息模型(Partition、Offset、Consumer Group、事务消息)跟 JMS 的抽象对不上,硬套反而把自己的能力捆住。
为什么 JMS 要分 Queue 和 Topic
初学时最容易犯嘀咕:都是发消息,为什么要两个概念?因为企业里本来就有两类语义完全不同的需求,塞进一个模型才别扭。
一类是任务分发,一件事只能有一个人处理。电商下单后扣库存、发物流、改状态,这串活只能有一个消费者干,两个都抢到同一条消息就是扣两次库存的事故。所以要的是:
1 | Producer → Queue → ┬ Consumer1 |
这就是 Queue,工作队列,本质是负载均衡。像寄快递,一个包裹只送到一个人手上。
另一类是事件广播,一件事得让所有关心的人都知道。支付成功后,短信、积分、风控、统计一个都不能漏:
1 | Topic: OrderPaid |
这就是 Topic,发布/订阅,更像电视台播新闻,所有观众都能看到同一条。
除了消费方式,两者的生命周期也不同:Queue 里没消费者,消息照样留着等人取;Topic 默认没人订阅就直接丢,后来 JMS 才补了 Durable Subscription(持久订阅)让它也能存。
正因为差这么多,JMS 干脆做成两个 API——createQueue(...) 一看就知道是派活,createTopic(...) 一看就知道是广播,接口本身就把业务意图说清楚了。
| Queue | Topic | |
|---|---|---|
| 语义 | 请完成这项任务 | 发生了一件事,请知晓 |
| 消费 | 一条消息一个消费者 | 每个订阅者各收一份 |
| 目标 | 负载均衡 | 广播通知 |
| 场景 | 订单、支付、异步任务 | 通知、缓存刷新、领域事件 |
如果熟悉 DDD,会发现这正好对上一组老概念:Queue 像 Command(命令),Topic 像 Event(事件)。命令有唯一处理者、必须执行;事件是已发生的事实,谁关心谁订阅。
现代 MQ 为什么只留 Topic
有意思的是,到了 Kafka、Pulsar 这一代,Queue 直接消失了,只剩 Topic + Consumer Group,用一套机制覆盖两种需求:
1 | Topic: order |
窍门在 Consumer Group:同一个组内,一条消息只分给一个实例——这就是 Queue 的负载均衡;换一个组,又能完整收到一份——这就是 Pub/Sub。所以在 Kafka 看来,Queue 只是 Topic 的一种消费方式,犯不着单独做成一种数据结构。
JMS 之所以还留着 Queue,是时代印记。它定型于 1998 年前后,那会儿 IBM MQ 那批系统就把点对点和发布订阅当成两种独立模型,JMS 照着抽象成两个 API。而 Kafka 是 2011 年后才流行的,吸收了分布式日志的思路,才有底气用一套模型把两件事统一掉。
Queue 能被 Topic 完全替代吗?
聊到这,自然会冒出这个问题。我的看法是:基本可以,但”完全”有点满,得分两层看。
从实现层面说,可以。Kafka、Pulsar 已经证明 Topic + Consumer Group 能覆盖 Queue 的全部功能,确实不再需要一个独立的 Queue 结构。
但从语义层面说,不建议把它们当成一回事。Queue<OrderTask>(请处理这个订单)和 Topic<OrderCreated>(订单已创建)表达的意图完全不同,一个是命令,一个是事件——这种区别在建模时是有价值的,丢掉挺可惜。所以很多系统底层清一色是 Topic,但仍会在命名上把意图留出来:
1 | command.order.create ← 有唯一处理者,必须执行 |
一句话收尾:技术实现上,Topic 可以扛下 Queue 的活;但业务建模里,”任务”和”事件”始终是两回事,现代 MQ 只是用同一套底层机制,把两种语义都装了进去。