从 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
2
Connection → Session → Destination(Queue / Topic)
→ MessageProducer / MessageConsumer

它的价值在于解耦:业务代码只依赖 javax.jms(或新版 jakarta.jms)接口,具体由谁实现无所谓。真正实现这套接口的产品叫 JMS Provider

1
2
3
4
5
6
        应用程序

JMS API(jakarta.jms)

┌───────┼───────┐
ActiveMQ IBM MQ Artemis

好处是换 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
2
3
Producer → Queue → ┬ Consumer1
├ Consumer2 ← 只有一个拿到
└ Consumer3

这就是 Queue,工作队列,本质是负载均衡。像寄快递,一个包裹只送到一个人手上。

另一类是事件广播,一件事得让所有关心的人都知道。支付成功后,短信、积分、风控、统计一个都不能漏:

1
2
3
   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
2
3
Topic: order
Group A(订单服务): 实例1 / 实例2 / 实例3 ← 组内负载均衡,等价 Queue
Group B(统计服务) ← 换个组,又收到一份,等价广播

窍门在 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
2
command.order.create   ← 有唯一处理者,必须执行
event.order.created ← 谁关心谁订阅,已经发生

一句话收尾:技术实现上,Topic 可以扛下 Queue 的活;但业务建模里,”任务”和”事件”始终是两回事,现代 MQ 只是用同一套底层机制,把两种语义都装了进去。