vault backup: 2026-06-08 23:08:57

This commit is contained in:
hhs
2026-06-08 23:08:57 +08:00
parent d67831afe0
commit 51de196713
136 changed files with 26069 additions and 344 deletions
@@ -1,6 +1,7 @@
---
tags: [MQ, JMS, OpenMessaging, Java, 协议]
create time: 2026-05-24 19:52
update time: 2026-06-07 17:30
---
# JMS 与 OpenMessaging
@@ -47,6 +48,9 @@ graph LR
> [!question] Queue 模型中,如果多个消费者监听同一个 Queue,消息会被谁收到?
> JMS 规范本身没有规定负载均衡策略,具体由实现厂商决定。但通常的行为是:消息在多个消费者之间轮询(round-robin)分发,每条消息只投递给其中一个消费者。这和 Kafka Consumer Group 的行为类似。
> [!info] JMS 1.1 的统一域模型
> 早期 JMS 1.0 中,PTP 和 Pub/Sub 是两套**完全独立**的 API:`QueueConnectionFactory` / `TopicConnectionFactory`、`QueueSession` / `TopicSession`、`QueueSender` / `TopicPublisher`……写一套代码同时支持两种模型很痛苦。JMS 1.1(2002)统一了域模型——`ConnectionFactory`、`Session`、`MessageProducer`、`MessageConsumer` 同时适用于 Queue 和 Topic,通过创建 Session 时传入不同参数来切换模式。这就是为什么你在下面的核心接口表里看到的是统一的 `MessageProducer` / `MessageConsumer`,而不是 JMS 1.0 时代的 `QueueSender` / `TopicPublisher` 这样分裂的类型。
### JMS API 核心接口
JMS 的编程模型围绕以下几个核心接口展开:
@@ -75,6 +79,85 @@ JMS 规范定义了五种消息体类型,覆盖了常见的数据传输需求
> [!question] ObjectMessage 看起来很方便,为什么生产中反而不推荐?
> 因为 ObjectMessage 依赖 Java 序列化,存在严重的安全漏洞(反序列化攻击链)。而且它和 Java 绑死,跨语言几乎不可能。现代实践更倾向用 TextMessage + JSON/Protobuf 编码。
### 消息确认与持久化
JMS 通过两个维度控制消息的可靠性:**投递模式**(消息是否持久化)和**确认模式**(消费者何时确认已消费)。
**投递模式(DeliveryMode)**
| 模式 | 说明 |
|------|------|
| **PERSISTENT**(默认) | Broker 在收到消息后将其写入持久存储(如磁盘),保证 Broker 崩溃后消息不丢 |
| **NON_PERSISTENT** | Broker 不保证持久化,性能更高但可能丢消息。适用于可以容忍丢失的场景(如监控指标) |
生产者可以通过 `producer.setDeliveryMode(DeliveryMode.PERSISTENT)` 设置,也可以在发送单条消息时指定。
**确认模式(AcknowledgeMode)**
确认模式决定了消费者告诉 Broker "我已成功处理消息"的时机,直接影响消息的可靠性语义:
| 模式 | 行为 | 适用场景 |
|------|------|----------|
| **AUTO_ACKNOWLEDGE** | Session 在 `receive()` 返回或 `onMessage()` 方法正常结束时自动确认 | 简单场景,允许极小概率丢消息 |
| **CLIENT_ACKNOWLEDGE** | 消费者显式调用 `message.acknowledge()` 确认,确认时会批量确认该 Session 中所有已消费的消息 | 需要精确控制确认时机 |
| **DUPS_OK_ACKNOWLEDGE** | Session 延迟确认(惰性确认),可能投递重复消息 | 吞吐优先、允许重复的场景 |
| **SESSION_TRANSACTED** | 事务模式,Session 提交时自动确认,回滚时消息重新入队 | 需要原子性:消息消费 + 数据库操作要么全成功要么全回滚 |
> [!question] JMS 的 AUTO_ACKNOWLEDGE 和 AMQP 的 autoAck 有什么区别?
> 思路上类似,但 JMS 的 AUTO_ACKNOWLEDGE 是在方法**正常返回**时确认(异常则不确认),比 AMQP 的 autoAck 更安全一些。不过两者都不推荐在生产环境使用——最可靠的做法都是手动确认。参见 [[5-AMQP-协议]] 中 Prefetch 和手动 ACK 的讨论。
### 消息选择器(Message Selector)
当一个 Topic 有多个消费者,但每个消费者只关心部分消息时怎么办?JMS 提供了 **Message Selector**——消费者可以在创建时指定一个基于 SQL-92 子集的过滤表达式,Broker 会在投递前过滤掉不匹配的消息。
```java
// 只接收 priority > 5 且 region 为华东的消息
String selector = "priority > 5 AND region = 'east'";
consumer = session.createConsumer(queue, selector);
```
选择器作用于消息的 **Properties**(自定义键值对)和部分 **Header** 字段(如 `JMSPriority`、`JMSCorrelationID`),不涉及 Body 内容。
> [!warning] 选择器的性能陷阱
> 选择器在 Broker 端执行,实质上是对每条消息做条件匹配。如果选择器表达式复杂、消息量又大,会显著增加 Broker 的 CPU 开销。部分实现(如 ActiveMQ)的选择器性能在高吞吐场景下可能成为瓶颈。如果你发现选择器性能不够用,通常意味着应该用不同的 Queue/Topic 来拆分消息流,而不是在一个队列上做过滤。
### 请求-应答模式(Request-Reply)
JMS 原生支持一种常见的同步通信模式:生产者发送请求消息,并期望收到一条应答消息。这通过两个标准 Header 字段实现:
- **`JMSReplyTo`**:请求消息中指定应答消息应发往的 Destination(通常是临时 Queue)。
- **`JMSCorrelationID`**:应答消息携带此 ID,与原始请求关联。
```mermaid
sequenceDiagram
participant Client as "Client"
participant ReqQ as "Request Queue"
participant Server as "Server"
participant ReplyQ as "Reply Queue"
Client->>ReqQ: "send request"
ReqQ->>Server: "deliver"
Server->>ReplyQ: "send response with CorrelationID"
ReplyQ->>Client: "deliver"
```
```go
// Go 伪代码展示 JMS 请求-应答的核心思路
// 请求方:发消息时指定 ReplyTo 和 CorrelationID
req := NewMessage(body)
req.SetReplyTo(replyQueue)
req.SetCorrelationID(requestID)
producer.Send(requestQueue, req)
// 应答方:从请求中取出 ReplyTo,应答时回填 CorrelationID
resp := NewMessage(result)
resp.SetCorrelationID(request.GetCorrelationID())
producer.Send(request.GetReplyTo(), resp)
```
> [!question] 既然可以做请求-应答,那 JMS 能替代 RPC 吗?
> 技术上可以,但通常不这么做。请求-应答模式适合**异步、解耦**的场景(如订单处理后通知),但它的延迟比直接 RPC 高(至少经过两次 Broker 投递),且需要处理超时、应答丢失等边界情况。如果你的场景需要同步、低延迟的调用,用 gRPC 或 HTTP 更合适。
### JMS 的局限性
JMS 在 Java 企业级开发中统治了十多年,但它有几个根深蒂固的问题:
@@ -82,11 +165,60 @@ JMS 在 Java 企业级开发中统治了十多年,但它有几个根深蒂固
1. **Java 绑定**:JMS 是纯 Java API,无法直接被 Go、Python、C++ 等语言使用。在微服务和多语言架构兴起后,这个限制变得越来越致命。
2. **缺乏跨语言互操作**:不同 JMS 实现之间的有线协议(wire protocol)不统一,ActiveMQ 用 OpenWire,IBM MQ 用 MQI,互相不能直连。
3. **厂商实现碎片化**:虽然有统一规范,但各厂商在扩展特性(如延迟消息、事务消息、死信队列)上的实现差异很大,迁移成本并不低。
4. **规范更新缓慢**:JMS 2.0(2013 年)才引入简化 API(JMSContext),在此之前用 JMS 写代码要处理大量的样板代码(try-finally 关闭资源)。
4. **规范更新缓慢**:JMS 2.0(2013 年)才引入简化 API,JMS 3.0/3.1 随 Jakarta EE 改名进一步跟进,但整体迭代节奏远落后于社区需求(详见下文 JMS 2.0 改进)。
### JMS 2.0 的改进
JMS 1.1 的 API 以"繁琐"著称——每次发消息都需要经历 `创建连接 → 创建会话 → 创建生产者 → 创建消息 → 发送 → 关闭` 的完整流程。JMS 2.0(JSR 343,2013 年发布)对此做了大幅简化:
**1. JMSContext 合并 Connection + Session**
JMS 1.1 中需要分别管理 `Connection` 和 `Session` 两个对象的生命周期。JMS 2.0 引入 `JMSContext`,将二者合并为一个统一入口:
```java
// JMS 1.1:需要管理两个对象
Connection conn = factory.createConnection();
Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
// ... 业务代码 ...
session.close();
conn.close();
// JMS 2.0:一个 JMSContext 搞定
try (JMSContext context = factory.createContext()) {
context.createProducer().send(queue, "Hello");
} // 自动关闭
```
**2. 简化消息发送**
不再需要显式创建 `MessageProducer` 和 `TextMessage`:
```java
// JMS 1.1
TextMessage msg = session.createTextMessage("Hello");
MessageProducer producer = session.createProducer(queue);
producer.send(msg);
producer.close();
// JMS 2.0
context.createProducer().send(queue, "Hello"); // 一行搞定
```
**3. 新增特性**
| 特性 | 说明 |
|------|------|
| **Delivery Delay** | 消息延迟投递:`producer.setDeliveryDelay(30_000)` 表示 30 秒后才投递 |
| **Async Send** | 异步发送:`producer.setAsync(completionListener)` 发送后不阻塞,通过回调通知完成 |
| **Shared Subscription** | 多个消费者共享同一个 Topic 订阅,消息在消费者间负载均衡(类似 Queue 行为,但作用于 Topic) |
| **@JMSDestinationDefinition** | 通过注解声明式定义 JMS Destination,配合 Java EE 容器自动创建队列/主题 |
> [!tip] JMS 2.0 的实际影响
> 尽管 JMS 2.0 的 API 改进确实降低了开发门槛,但它发布得太晚了(2013 年)。彼时 Kafka(2011)、RabbitMQ(2007)已各自建立了庞大的生态,新一代开发者更倾向于直接使用 MQ 原生 SDK 而非 JMS 抽象层。JMS 2.0 更多是在存量 Java EE 系统中发挥作用,而非吸引新用户。
### OpenMessaging:面向云原生的开放标准
2017 年,阿里巴巴联合 Apache RocketMQ 团队发起了 OpenMessaging 项目,目标很明确:定义一个厂商中立、语言无关、云原生的消息和流标准规范。
2017 年,阿里巴巴在 Linux Foundation 下发起了 OpenMessaging 项目(Apache RocketMQ 是其参考实现之一),目标很明确:定义一个厂商中立、语言无关、云原生的消息和流标准规范。该项目吸引了 Huawei、Yahoo 等公司的参与。
OpenMessaging 的核心设计原则:
@@ -106,11 +238,14 @@ OpenMessaging 的核心设计原则:
| 事务支持 | JTA 事务集成 | 原生分布式事务 |
| 云原生 | 无原生支持 | Namespace、多租户、弹性伸缩 |
| 生态成熟度 | 非常成熟,20+ 年积累 | 仍在发展中,落地项目较少 |
| 典型实现 | ActiveMQ、IBM MQ | Apache RocketMQ(部分支持) |
| 典型实现 | ActiveMQ、IBM MQ | 尚无主流产品完整实现(活跃度低) |
> [!question] OpenMessaging 提出了很好的愿景,为什么它的落地进展相对缓慢?
> 一方面,Kafka 和 RocketMQ 各自已经形成了庞大生态,应用层集成稳定,切换标准的动力不足。另一方面,消息协议不像 HTTP 那样有极强的网络效应——MQ 的抽象层通常在 SDK 内部,业务开发者感知不到底层协议差异。标准化的收益没有数据库连接池(如 HikariCP 替换 DBCP)那么直接。
> [!warning] OpenMessaging 当前状态
> 截至 2026 年,OpenMessaging 项目的活跃度已大幅下降。GitHub 仓库的提交频率极低,核心维护者基本来自阿里云 RocketMQ 团队,社区生态远未形成。**目前没有主流 MQ 产品完整实现了 OpenMessaging 规范**。它更多地被视为一个概念验证(proof of concept)而非可落地的标准。如果你在做技术选型,不应将 OpenMessaging 作为考量因素——直接评估各 MQ 产品的原生 SDK 更务实。
### Go 代码:JMS 思路的对等实现
JMS 没有官方 Go SDK,但我们可以用 Go 的接口抽象来实现类似 JMS 的分层设计。以下示例展示如何用面向接口的方式构建消息客户端,底层可插拔不同 MQ 实现。