一,JetStream介绍
JetStream 是什么?
JetStream 是 NATS 提供的原生流式消息引擎(streaming + persistence) ,是 NATS Server
自带的 高级消息系统组件,对标 Kafka / Pulsar / RabbitMQ 的流式能力。
JetStream 是 NATS 的 高可靠、有状态 消息机制,适合企业级分布式系统中的异步通信、事件溯源、重试补偿、幂等消费等场景。
JetStream 相比普通 NATS 的区别
功能 | NATS Core(核心) | JetStream(增强) |
---|---|---|
消息类型 | 转瞬即逝(fire & forget) | 持久化、有序 |
消费模式 | Pub/Sub(无状态) | Push / Pull(可控) |
消息确认 | 无确认机制 | 显式 ack / 自动 ack |
消息保存 | 不支持 | Stream 持久保存 |
重放 | 不支持 | 可重放、历史回看 |
多租户 | 不支持 | 支持 JetStream Domain |
二,JetStream快速入门
-
引入依赖
xml<dependency> <groupId>io.nats</groupId> <artifactId>jnats</artifactId> <version>2.21.1</version> </dependency>
-
编写配置bean
java@Configuration @Slf4j public class NatsConfig { @Value("${nats.server}") private String natsServer; @Value("${nats.name}") private String serverName; @Value("${nats.userName}") private String userName; @Value("${nats.password}") private String password; @Bean public Connection natsConnection() throws IOException, InterruptedException { Options options = new Options.Builder() .server(natsServer) // 服务器地址数组 .connectionTimeout(Duration.ofSeconds(5)) // 连接超时时间 .reconnectWait(Duration.ofSeconds(1)) // 重连等待时间 .maxReconnects(10) // 最大重连次数,-1代表无限重连 .connectionName(serverName) // 连接名称(服务端可见) .errorListener(new CustomErrorListener()) // 错误监听器 .connectionListener(new CustomConnectionListener())//连接状态监听器 .reconnectBufferSize(1024 * 1024 * 1024)//重连缓冲区大小 .userInfo(userName,password) .build(); return Nats.connect(options); } @Bean public JetStream jetStream(Connection natsConnection) throws IOException { return natsConnection.jetStream(); } @Bean public JetStreamManagement jetStreamManagement(Connection natsConnection) throws IOException { return natsConnection.jetStreamManagement(); } }
-
编写消息发送者
java@Component @Slf4j public class JetStreamPublisherClient { @Autowired private JetStream jetStream; public void sendMessage(String subject, String message) throws IOException, JetStreamApiException { Message msg = NatsMessage.builder() .subject(subject) .data(message.getBytes(StandardCharsets.UTF_8)) .build(); PublishAck ack = jetStream.publish(msg); System.out.println("Message sent, stream: " + ack.getStream()); } }
-
编写消费者
-
编写sub消费者
javapackage org.example.natsdemo.server; import io.nats.client.*; import io.nats.client.api.*; import lombok.extern.slf4j.Slf4j; import org.example.natsdemo.config.JetStreamResourceManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.io.IOException; import java.nio.charset.StandardCharsets; @Component @Slf4j public class JetStreamSubService { @Autowired private Connection natsConnection; @Autowired private JetStream jetStream; @Autowired private JetStreamResourceManager jetStreamResourceManager; String stream = "stream1"; String subject = "js.subject1"; String durable = "consumer-sub"; @PostConstruct public void init() throws Exception { jetStreamResourceManager.ensureStream(stream, subject); ConsumerConfiguration consumerConfiguration = ConsumerConfiguration.builder() .durable(durable) .ackPolicy(AckPolicy.Explicit) .build(); jetStreamResourceManager.ensureConsumer(stream, durable, consumerConfiguration); startConsumerWithDispatcher(); } private void startConsumerWithDispatcher() throws IOException, JetStreamApiException { Dispatcher dispatcher = natsConnection.createDispatcher(); ConsumerConfiguration cc = ConsumerConfiguration.builder() .durable(durable) .ackPolicy(AckPolicy.Explicit) .build(); PushSubscribeOptions options = PushSubscribeOptions.builder() .stream(stream) .configuration(cc) .build(); jetStream.subscribe( subject, dispatcher, msg -> { String data = new String(msg.getData(), StandardCharsets.UTF_8); log.info("收到消息:{}", data); try { msg.ack(); // 显式确认 } catch (Exception e) { log.error("确认消息失败", e); } }, false, // 是否自动 ack:false 表示手动 ack options ); log.info("JetStream Sub 订阅启动成功,主题:{},消费者:{}", subject, durable); } }
-
编写pull消费者
javapackage org.example.natsdemo.server; import io.nats.client.*; import io.nats.client.api.*; import lombok.extern.slf4j.Slf4j; import org.example.natsdemo.config.JetStreamResourceManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.List; @Component @Slf4j public class JetStreamPullService { @Autowired private JetStreamResourceManager jetStreamResourceManager; @Autowired private JetStream jetStream; private JetStreamSubscription subscription; private final String stream = "stream2"; private final String subject = "js.subject2"; private final String durable = "consumer-pull"; private volatile boolean running = true; @PostConstruct public void init() throws Exception { // 确保 Stream 和 Consumer 存在 jetStreamResourceManager.ensureStream(stream, subject); ConsumerConfiguration consumerConfiguration = ConsumerConfiguration.builder() .durable(durable) .ackPolicy(AckPolicy.Explicit) .build(); jetStreamResourceManager.ensureConsumer(stream, durable, consumerConfiguration); PullSubscribeOptions pullOptions = PullSubscribeOptions.builder() .stream(stream) .configuration(consumerConfiguration) .build(); subscription = jetStream.subscribe(subject, pullOptions); log.info("JetStream Pull 订阅启动成功,主题:{},消费者:{}", subject, durable); // 启动拉取线程 new Thread(this::pullLoop, "nats-pull-thread").start(); } private void pullLoop() { while (running) { try { // 每次拉取最多 10 条消息,超时 2 秒 List<Message> messages = subscription.fetch(10, Duration.ofSeconds(2)); for (Message msg : messages) { String data = new String(msg.getData(), StandardCharsets.UTF_8); log.info("拉取到消息:{}", data); msg.ack(); // 显式确认 } } catch (Exception e) { log.error("拉取消息出错: {}", e.getMessage()); } try { Thread.sleep(1000); // 可根据实际场景调整拉取频率 } catch (InterruptedException ignored) { log.warn("线程被中断"); } } } }
-
-
封装JetStreamResourceManager用于统一管理stream和consumer的生命周期
javapackage org.example.natsdemo.config; import io.nats.client.JetStreamManagement; import io.nats.client.api.ConsumerConfiguration; import io.nats.client.api.StreamConfiguration; import io.nats.client.api.StreamInfo; import io.nats.client.api.ConsumerInfo; import io.nats.client.api.StorageType; import io.nats.client.JetStreamApiException; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.HashSet; import java.util.Set; @Component @Slf4j public class JetStreamResourceManager { private final JetStreamManagement jsm; // 保存初始化过的 Stream 和 Consumer,避免重复执行 private final Set<String> initializedStreams = new HashSet<>(); private final Set<String> initializedConsumers = new HashSet<>(); public JetStreamResourceManager(JetStreamManagement jsm) { this.jsm = jsm; } /** * 确保 Stream 存在,不存在则自动创建 */ public void ensureStream(String streamName, String... subjects) { if (initializedStreams.contains(streamName)) return; try { StreamInfo info = jsm.getStreamInfo(streamName); log.info("Stream 已存在: {}", info); } catch (JetStreamApiException e) { if (e.getErrorCode() == 10059) { // stream not found try { StreamConfiguration streamConfig = StreamConfiguration.builder() .name(streamName) .subjects(subjects) .storageType(StorageType.Memory) .build(); jsm.addStream(streamConfig); log.info("创建新 Stream 成功: {}", streamName); } catch (Exception ex) { log.error("创建 Stream 失败: {}", streamName, ex); } } else { log.error("检查 Stream 失败: {}", streamName, e); } } catch (IOException ioe) { log.error("获取 Stream 失败: {}", ioe.getMessage()); } initializedStreams.add(streamName); } /** * 确保 Durable Consumer 存在,不存在则创建 */ public void ensureConsumer(String stream, String durable, ConsumerConfiguration cc) { String key = stream + ":" + durable; if (initializedConsumers.contains(key)) return; try { ConsumerInfo info = jsm.getConsumerInfo(stream, durable); log.info("Consumer 已存在: {}", info); } catch (JetStreamApiException e) { if (e.getErrorCode() == 10014) { // consumer not found try { jsm.addOrUpdateConsumer(stream, cc); log.info("创建新 Consumer 成功: {}", durable); } catch (Exception ex) { log.error("创建 Consumer 失败: {},失败原因:{}", durable, ex.getMessage()); } } else { log.error("检查 Consumer 失败: {},失败原因:{}", durable, e.getMessage()); } } catch (IOException ioe) { log.error("获取 Consumer 失败: {}", ioe.getMessage()); } initializedConsumers.add(key); } /** * 删除 Consumer */ public void deleteConsumer(String stream, String durable) { try { jsm.deleteConsumer(stream, durable); log.info("删除 Consumer 成功: {}", durable); } catch (Exception e) { log.warn("删除 Consumer 失败: {},失败原因:{}", durable,e.getMessage()); } } /** * 删除 Stream */ public void deleteStream(String stream) { try { jsm.deleteStream(stream); log.info("删除 Stream 成功: {}", stream); } catch (Exception e) { log.warn("删除 Stream 失败: {}", stream); } } }
-
测试接口
java@PostMapping("/jetStreamPub") public String sendMessageByJetStream(String msg) throws JetStreamApiException, IOException { jetStreamPublisherClient.sendMessage("js.subject1", msg); jetStreamPublisherClient.sendMessage("js.subject2", msg); return "ok"; }
三,JetStream详解
3.1 StreamConfiguration
StreamConfiguration
------ 定义你的消息"仓库"
示例:
java
StreamConfiguration sc = StreamConfiguration.builder()
.name("mystream") // 流名称
.subjects("orders.*") // 对应的主题(可通配),是个数组
.storageType(StorageType.File) // 存储方式:Memory / File
.retentionPolicy(RetentionPolicy.Limits) // 保留策略
.maxAge(Duration.ofHours(24)) // 消息最大存活时间
.maxMessages(100_000) // 最大消息数量
.maxBytes(10_000_000) // 最大总字节数
.build();
常用配置说明:
配置项 | 说明 | 示例 |
---|---|---|
name |
流名(唯一) | "stream1" |
subjects |
关联哪些 subject(可通配) | "js.*" |
storageType |
Memory (非持久)或 File (落盘) |
StorageType.File |
retentionPolicy |
消息保留策略 | Limits / Interest / WorkQueue |
maxMessages |
最多保留多少条消息 | 100_000 |
maxBytes |
消息最大总占用空间 | 10_000_000 |
maxAge |
每条消息最大保留时间 | Duration.ofHours(1) |
replicas |
副本数(高可用配置)(这里指nats服务数量) | 1 , 3 |
discardPolicy |
超过限制后如何处理 | Old (删老的)或 New (拒绝新消息) |
-
name
--- 流名称(必须项)-
类型 :
String
-
作用:唯一标识这个消息流(stream),后续消费者、监控、管理都依赖这个名字。
-
是否必须:✅ 必须设置
-
命名规范 :只能使用字母、数字、
-
、_
,不能重复。 -
创建成功后,
jetstreamctl stream info user-events
可查看状态。 -
如果你尝试使用已存在的
name
再addStream
会报错:stream name already in use
故我们的stream流只需要创建一次即可
-
-
subjects
--- 流绑定的主题(subject)-
类型 :
String...
或List<String>
-
作用:定义这个 Stream 会"捕获"哪些主题的消息(支持通配符)
-
默认值:无,必须设置,否则不会接收任何消息
-
可用的通配符:
通配符 含义 示例 *
匹配一个片段 user.*
可匹配user.login
,user.logout
>
匹配剩余所有 user.>
可匹配user.a
,user.a.b
,user.x.y.z
-
-
storageType
--- 消息存储方式- 类型 :
StorageType
枚举 - 可选值 :
Memory
:内存中存储,重启丢失File
:落盘,支持持久化
- 类型 :
-
retentionPolicy
--- 消息保留策略-
类型 :
RetentionPolicy
枚举 -
作用:控制消息保留直到什么条件下可以被删除
值 含义 Limits
(默认)根据 maxMessages
,maxBytes
,maxAge
清除Interest
只保留还有"消费者订阅兴趣"的消息 WorkQueue
每条消息只保留给一个 consumer,适合任务队列 默认选
Limits
,做任务队列(每条只消费一次)可用WorkQueue
-
-
maxMessages
,maxBytes
--- 消息数量和体积上限- 控制消息最多保留多少条或多少字节
- 超过限制后按
discardPolicy
删除 - 如果两者都配置,哪个先到就先触发删除
-
maxAge
--- 每条消息最大保留时间- 超过这个时间的消息会被自动删除
- 不管是否消费,只要时间到了就删
- 与
maxMsgs
,maxBytes
可叠加使用
-
discardPolicy
--- 达到上限后丢弃策略选项 含义 Old
(默认)丢最旧的消息(先进先出) New
拒绝新消息(发送失败) -
replicas
--- 副本数(高可用)- 需要 NATS 集群和 JetStream replication 启用
- 设置为 3 则有主副本+两个备份,容灾能力提升
-
allowDirect
--- 允许非 JetStream 消费true
时可以用普通 NATS consumer 读取- 性能提升,但会绕过 JetStream 的 tracking
3.2 ConsumerConfiguration
定义你的"消费者策略":你每次拉取、推送消息,其实都在使用这个配置控制行为。
示例:
java
ConsumerConfiguration cc = ConsumerConfiguration.builder()
.durable("my-consumer") // durable 名字(必须)
.ackPolicy(AckPolicy.Explicit) // 手动 ack
.ackWait(Duration.ofSeconds(30)) // ack 超时后重投
.deliverPolicy(DeliverPolicy.All) // 从最早一条开始
.maxDeliver(5) // 最多重试次数
.filterSubject("orders.created") // 只订阅某个 subject
.replayPolicy(ReplayPolicy.Instant)// 消息回放速度
.build();
常用配置说明:
配置项 | 说明 | 示例 |
---|---|---|
durable |
消费者的唯一名称(保留状态) | "consumer1" |
ackPolicy |
消息确认策略 | Explicit , None , All |
ackWait |
多久未 ack 就重投 | Duration.ofSeconds(30) |
deliverPolicy |
从哪里开始消费 | All , Last , New , ByStartSequence , ByStartTime |
maxDeliver |
同一条消息最多重试多少次 | 5 |
filterSubject |
精确匹配消费的 subject | "orders.created" |
replayPolicy |
消息回放速率 | Instant / Original |
-
durable
--- Durable 消费者名称-
类型 :
String
-
作用:唯一标识该消费者,用于记录消费进度(offset)
-
是否必须:推荐设置(尤其是需要消息持久消费时)
-
影响:
- Durable consumer 可以断线重连,恢复未确认消息
- 非 durable(临时)消费者断线后,消费状态丢失,消息会重新发送
场景 有 durable 无 durable 消费者断开重连 恢复未确认偏移 重头开始接收 服务重启 消费进度保存 消费状态丢失
-
-
ackPolicy
--- 消息确认策略- 类型 :
AckPolicy
枚举 - 作用:控制消息确认方式,确保消息不丢失或重复
值 含义 Explicit
手动调用 msg.ack()
None
不需要确认,性能最高但消息可能丢失 All
对批量消息的自动确认 - 类型 :
-
ackWait
--- ack 超时重投- 类型 :
Duration
- 作用:消息投递后,未收到 ack 多少时间后重发消息
- 默认:30秒
- 类型 :
-
deliverPolicy
--- 消费起点策略- 类型 :
DeliverPolicy
枚举 - 作用:控制消费者从流中哪个位置开始接收消息
值 含义 All
从流中最早消息开始(历史消息都投) Last
从最近一条消息开始(不重投历史) New
只接收新产生的消息 ByStartSequence
从指定序号开始 ByStartTime
从指定时间开始 - 类型 :
-
filterSubject
--- 过滤主题- 类型 :
String
- 作用:消费者只接收流中匹配此 subject 的消息
- 类型 :
-
maxDeliver
--- 最大重投次数- 类型 :
int
- 作用:消息最多投递次数,超过则丢弃或发送到死信流(DLQ)
- 类型 :
-
replayPolicy
--- 消息重放速度- 类型 :
ReplayPolicy
枚举 - 作用:消息投递时的速度
值 含义 Instant
快速投递(默认) Original
以消息原始生产速率投递 - 类型 :
-
flowControl
&idleHeartbeat
--- 高级流控与心跳flowControl
:启用时 JetStream 会控制推送速度,避免消费者处理不过来idleHeartbeat
:消费者空闲时发送心跳包,保持连接活跃
3.3 JetStreamOptions
JetStreamOptions
是用于创建 JetStream
客户端实例时的配置参数,控制连接 JetStream 服务端的行为,包括:
主要作用 | 示例配置项 |
---|---|
控制是否自动创建流/消费者 | isThrowOnError() |
控制 JetStream 的访问上下文 | domain() |
访问的 API 前缀(多租户/隔离) | prefix() |
客户端对 JetStream 功能的容忍程度 | JetStreamOptions.Builder |
创建 JetStreamOptions 的方式
java
JetStreamOptions jsOptions = JetStreamOptions.builder()
.domain("my-domain") // 可选:JetStream 的隔离域
.prefix("js") // 可选:设置 NATS 路由前缀
.requestTimeout(Duration.ofSeconds(2)) // 请求 JetStream API 的超时时间
.build();
然后通过连接对象创建 JetStream 客户端:
java
JetStream js = connection.jetStream(jsOptions);
常用配置项详解
-
domain(String domain)
JetStream 支持"多域(domain)"架构:你可以把不同 Stream/Consumer 部署在不同 JetStream 域中,实现资源隔离。
- 默认域为
null
(全局) - 使用前确保你的 NATS 服务端开启了 JetStream 域支持(
--js-domain
)
- 默认域为
-
prefix(String prefix)
设置 JetStream 所使用的 API 路由前缀。例如,在多租户或 NATS 代理服务下,会改变 API 路由规则。
- 默认值为空(即
$JS.API.*
) - 注意:这个 prefix 只影响 JetStream 的管理请求,不影响消息 subject。
- 默认值为空(即
-
requestTimeout(Duration timeout)
设置 JetStream 客户端发起请求(如添加 Stream、获取 info)的最大等待时间。
- 默认约为 2 秒
- 服务端压力大或慢时可适当加长
什么是 JetStream 的多租户(Domain)与 API 前缀(Prefix)
-
什么是 JetStream 的"多租户"(domain)
JetStream 是一个持久化消息平台,它允许你创建 Stream、Consumer、消息历史和快照。在复杂的系统中,比如:
- 微服务架构
- SaaS 多租户系统
- 多个项目团队共用一个 NATS 集群
你可能希望不同租户(用户)拥有彼此隔离的 JetStream 环境 ,互不影响。JetStream 支持这种"逻辑隔离"能力,就是 domain(域)机制。
JetStream Domain 是什么?
-
JetStream domain 是一种逻辑命名空间,每个域相当于一套独立的 JetStream 实例(API 路由、资源空间都隔离)。
-
你可以在一个 NATS 集群中启用多个 JetStream 域,每个域都有自己独立的 stream、consumer 等。
一句话理解 :
domain
= JetStream 的命名空间。
如何启用?:服务端配置(需要 NATS 启动时加参数)
bashnats-server -js -sd ./store -js-domain tenant-a
这样你启动了一个名为
tenant-a
的 JetStream 域。你也可以在多个服务上分别设置
-js-domain tenant-a
、tenant-b
,实现每个租户一个 JetStream 后端实例。
客户端访问特定 domain:
javaJetStreamOptions options = JetStreamOptions.builder() .domain("tenant-a") .build(); JetStream js = connection.jetStream(options);
-
什么是 JetStream 的 Prefix?
JetStream 的 API 路由在 NATS 上是通过特定主题实现的,比如:
bash$JS.API.STREAM.INFO.stream-name
这个是默认的 JetStream API 路由格式。
如果你有代理层、网关层,或者需要将 JetStream 的 API 隐藏在特定路径下,比如:
bashmytenant.$JS.API.STREAM.INFO.stream-name
你可以通过设置 prefix 来实现这个目标。
3.4 JetStreamManagement
-
JetStreamManagement 是什么?
一句话理解:
JetStreamManagement
是用来 管理 JetStream 的资源 的接口,例如创建、删除、查询 Stream 和 Consumer。常见功能:
功能 方法示例 创建 Stream jsm.addStream(StreamConfiguration)
删除 Stream jsm.deleteStream(name)
获取 Stream 信息 jsm.getStreamInfo(name)
创建 Consumer jsm.addOrUpdateConsumer(stream, config)
删除 Consumer jsm.deleteConsumer(stream, durable)
获取消费者信息 jsm.getConsumerInfo(stream, durable)
拉取 Stream 列表 jsm.getStreamNames()
拉取 Consumer 列表 jsm.getConsumerNames(stream)
-
如何获得 JetStreamManagement 实例
你需要有一个
Connection
实例,然后调用:javaJetStreamManagement jsm = connection.jetStreamManagement();
-
JetStreamManagement 高阶用法
-
获取 Stream 的详细状态
javaStreamInfo streamInfo = jsm.getStreamInfo("orders"); StreamState state = streamInfo.getStreamState(); System.out.println("总消息数: " + state.getMsgCount()); System.out.println("总字节数: " + state.getTotalBytes()); System.out.println("最早序列: " + state.getFirstSeq()); System.out.println("最新序列: " + state.getLastSeq());
用于:监控 backlog,检查是否消费滞后、是否需要扩容。
-
获取/分页所有流和消费者列表
javaList<String> streams = jsm.getStreamNames(); // 默认100条 获取所有 Stream 名称 List<String> consumers = jsm.getConsumerNames("orders"); //获取某个流下的所有 Consumer:
-
强制删除消费者或 Stream(无视状态)
javajsm.deleteStream("test-stream"); jsm.deleteConsumer("orders", "slow-consumer");
如果你遇到"无法删除 consumer,因为尚有未确认消息"等错误,用 JetStream CLI 或服务端配置可启用"强删":
bashnats stream delete orders --force
-
消息清理:删除序列前的所有消息
javajsm.purgeStream("orders"); // 清空整个 stream PurgeOptions options = PurgeOptions.builder() .sequence(1050) // 删除序号 < 1050 的所有消息 .build(); jsm.purgeStream("orders", options);
-
3.5 PushSubscribeOptions
-
什么是 PushSubscribeOptions?
在 JetStream 中,消息消费者有两种模式:
模式 描述 Pull 模式 客户端主动拉取消息 Push 模式 服务端主动推送消息给客户端 当你使用 Push 模式订阅 (
jetStream.subscribe(...)
)时,必须指定PushSubscribeOptions
。它包含了如下关键参数:
javaPushSubscribeOptions options = PushSubscribeOptions.builder() .stream("stream-name") .durable("consumer-name") .configuration(ConsumerConfiguration) // 自定义消费者行为 .build();
-
常见用途
功能 是否需要 PushSubscribeOptions 使用 durable consumer ✅ 是 配置 ack 策略、重试次数 ✅ 是(通过 ConsumerConfiguration) 多个 stream 下的 subject ✅ 明确指定 stream 并发订阅 ✅ 否则可能报错(找不到 stream) -
主要参数详解
.stream(String stream)
告诉 JetStream 这个订阅是属于哪个 Stream 的。
这是必须的,否则如果多个 stream 的 subject 有重叠,会抛出异常:
Ambiguous Stream Name
.java.stream("orders-stream")
.durable(String durableName)
设置 Durable Consumer 名字(JetStream 将自动管理消费进度)。
java.durable("orders-processor")
- Durable Consumer 的好处是:消费进度持久化,断连后恢复
- 不设置 durable 就是 "ephemeral(临时)消费者",断连后消息可能丢失
.configuration(ConsumerConfiguration config)
内嵌设置消费者的行为,比如:
javaConsumerConfiguration config = ConsumerConfiguration.builder() .ackPolicy(AckPolicy.Explicit) .deliverPolicy(DeliverPolicy.New) .maxDeliver(5) .deliverSubject("inbox.orders") .build();
再组合进来:
javaPushSubscribeOptions options = PushSubscribeOptions.builder() .stream("orders-stream") .durable("orders-processor") .configuration(config) .build();
-
常见错误解析
错误信息 原因 解决办法 No stream matches subject
未指定 stream,subject 冲突 用 .stream(...)
显式指明Consumer already exists as pull consumer
之前以 pull 方式订阅 删除 consumer 后重建,或统一用 pull Consumer already exists with different config
durable 名字冲突,但配置不同 改名字或删除旧 consumer
3.6 PullSubscribeOptions
-
什么是 PullSubscribeOptions?
在 JetStream 的 拉取消费模式 中,你需要先通过:
javaJetStreamSubscription subscription = jetStream.subscribe(subject, pullSubscribeOptions);
注册一个「拉取式消费者(pull consumer)」,然后使用:
javasubscription.fetch(10, Duration.ofSeconds(2));
主动拉取消息。
而
PullSubscribeOptions
就是在这个订阅动作中,指定该消费者的详细配置。 -
使用场景
使用目的 是否需要 PullSubscribeOptions ✅ 设置 durable(持久订阅) 必须 ✅ 配置 maxDeliver、ack 策略等 推荐(通过 ConsumerConfiguration
)✅ 拉取模式注册多个消费者 强烈建议 ❌ 临时拉取,不需要 durable 可以省略(但容易混乱) -
各项参数详解
.stream(String streamName)
明确绑定这个订阅属于哪个 Stream。
arduino.stream("orders-stream")
必须指定,如果多个 stream 有重叠 subject,系统无法自动判断。
.durable(String durableName)
指定一个持久化的消费者名称,用于:
- 保持消费进度(ack)
- 消费断开后恢复
- 防止"Consumer already configured as push" 报错
java.durable("inventory-puller")
拉取模式必须设置 durable,否则报错:
javaJetStreamApiException: consumer must be durable for pull mode
.configuration(ConsumerConfiguration)
设置这个拉取式消费者的详细行为(ack 策略、最大消息数、延迟等)
javaConsumerConfiguration cc = ConsumerConfiguration.builder() .ackPolicy(AckPolicy.Explicit) .deliverPolicy(DeliverPolicy.New) .maxDeliver(5) .build();
然后:
java.configuration(cc)
-
常见错误解析
报错信息 原因 解决方案 consumer must be durable for pull mode
没指定 durable 设置 .durable("xxx")
consumer already configured as push
相同 durable 已经被 push 模式使用 删除原来的 consumer 或改名 no stream matches subject
未指定 stream,subject 冲突 设置 .stream("xxx")
-
Push 和 Pull 对比(订阅参数)
模式 使用类 是否必须 durable 使用场景 Push PushSubscribeOptions 可选但推荐 实时推送,适合低延迟 Pull PullSubscribeOptions ✅ 必须 控制节奏、批处理、高并发拉
3.7 PublishOptions
-
什么是
PublishOptions
?在 JetStream 中,默认的
jetStream.publish(subject, data)
是最简单的同步发布。而如果你想要:
需求 是否用 PublishOptions
指定消息属于哪个 Stream(多 Stream 情况) ✅ 推荐 带上 MsgId
做幂等控制✅ 推荐 关联到某个 expected sequence(CAS) ✅ 必须 发布目标为 domain
中的 JetStream✅ 必须 幂等、分区、幂等性策略 ✅ 必须 就需要
PublishOptions
。 -
创建 PublishOptions 的常见方式
javaPublishOptions options = PublishOptions.builder() .stream("orders") // 指定 Stream 名 .messageId("order-1234") // 设置幂等唯一 ID(msgId) .expectedStream("orders") // 期望属于哪个 Stream .expectedLastSequence(104) // CAS(Check-And-Set) .expectedLastSubjectSequence(56) // 指定某 subject 的最新序列 .domain("tenant-A") // 多租户隔离时的 JetStream 域 .build();
-
主要参数详解
.stream(String streamName)
指定消息属于哪个 Stream。
java.stream("audit-stream")
.messageId(String msgId)
设置幂等 ID,JetStream 会自动防止重复提交。
java.messageId("event-uuid-1234")
JetStream 会记录最近一段时间的
messageId
,重复的 ID 会被拒绝,防止幂等写入。
.expectedStream(String name)
指定你希望这条消息投递到哪个 stream。若不匹配,将报错。
java.expectedStream("audit-stream")
可用于保障流投递正确性,避免 subject 冲突时误投。
.expectedLastSequence(long seq)
使用乐观锁机制:只有当 Stream 当前最新序号是你期望的 seq 时,才允许发送成功。
java.expectedLastSequence(105)
可用于保障顺序、并发写入一致性(类似于数据库的 CAS 操作)。
.expectedLastSubjectSequence(long seq)
只针对某个 subject 的顺序做 CAS 控制。
java.expectedLastSubjectSequence(200)
.domain(String domain)
指定 JetStream 所属的 "域" ------ 多租户下,每个租户可以拥有自己的 JetStream 实例。
java.domain("org-A")
必须配合 JetStreamOptions 中开启的域使用。
-
异常情况示例
报错信息 可能原因 解决办法 Wrong last sequence
expectedLastSequence 与实际不一致 刷新缓存或关闭 expected Stream does not match
Stream 不存在或未指定正确 设置 .stream(...)
Duplicate msgId
messageId 已经使用过 生成新的 UUID
-
建议使用场景
业务需求 建议配置 重要写入幂等保障 .messageId(...)
多 stream 下精确投递 .stream(...) + .expectedStream(...)
多租户平台 .domain(...)
并发控制写入顺序 .expectedLastSequence(...)
可观察性 可结合日志打印 msgId、ack seq、耗时等
3.8 JetStream
3.8.1 发布消息
-
同步发布(6种)
javaPublishAck publish(String subject, byte[] data) PublishAck publish(String subject, Headers headers, byte[] data) PublishAck publish(String subject, byte[] data, PublishOptions options) PublishAck publish(String subject, Headers headers, byte[] data, PublishOptions options) PublishAck publish(Message msg) PublishAck publish(Message msg, PublishOptions options)
说明
方法 作用 publish(String, byte[])
最基础的同步发布 publish(String, Headers, byte[])
加上头部(用于元信息、追踪) publish(..., PublishOptions)
加上幂等控制、流检查、Domain 等高级配置 publish(Message)
封装好的 Message
一键发送publish(Message, PublishOptions)
全功能消息发布 -
异步发布(6种)
javaCompletableFuture<PublishAck> publishAsync(String subject, byte[] data) CompletableFuture<PublishAck> publishAsync(String subject, Headers headers, byte[] data) CompletableFuture<PublishAck> publishAsync(String subject, byte[] data, PublishOptions options) CompletableFuture<PublishAck> publishAsync(String subject, Headers headers, byte[] data, PublishOptions options) CompletableFuture<PublishAck> publishAsync(Message msg) CompletableFuture<PublishAck> publishAsync(Message msg, PublishOptions options)
说明:
- 用于 异步非阻塞发布
- 返回一个
CompletableFuture<PublishAck>
,可加回调处理 - 适合高并发、批量发送等场景
示例:
javajetStream.publishAsync(msg) .thenAccept(ack -> log.info("发布成功 seq: {}", ack.getSeqno())) .exceptionally(e -> { log.error("发布失败", e); return null; });
3.8.2 订阅消息
共 8 个订阅方法,支持:
类型 | 方法签名(部分) |
---|---|
Push 简单 | subscribe(subject) |
Push + durable | subscribe(subject, PushSubscribeOptions) |
Push + dispatcher | subscribe(subject, dispatcher, handler, ack, options) |
Pull | subscribe(subject, PullSubscribeOptions) |
Pull + dispatcher | subscribe(subject, dispatcher, handler, PullOptions) |
-
Push 订阅
javaJetStreamSubscription subscribe(String subject) JetStreamSubscription subscribe(String subject, PushSubscribeOptions options) JetStreamSubscription subscribe(String subject, Dispatcher dispatcher, MessageHandler handler, boolean autoAck) JetStreamSubscription subscribe(String subject, Dispatcher dispatcher, MessageHandler handler, boolean autoAck, PushSubscribeOptions options)
- 适合:实时消费、无需手动拉取
- 可选择是否使用
Dispatcher
(多线程消费)
示例(Push):
javaPushSubscribeOptions options = PushSubscribeOptions.builder() .stream("my-stream") .durable("my-durable") .build(); jetStream.subscribe("log.subject", dispatcher, msg -> { log.info("收到:{}", new String(msg.getData())); msg.ack(); }, false, options);
-
Pull 订阅
javaJetStreamSubscription subscribe(String subject, PullSubscribeOptions options) JetStreamSubscription subscribe(String subject, Dispatcher dispatcher, MessageHandler handler, PullSubscribeOptions options)
- 适合:手动拉取、控制节奏、批量处理
- 拉取方式通过
subscription.fetch(...)
示例(Pull):
javaPullSubscribeOptions options = PullSubscribeOptions.builder() .stream("my-stream") .durable("my-pull") .build(); JetStreamSubscription sub = jetStream.subscribe("log.subject", options); List<Message> messages = sub.fetch(10, Duration.ofSeconds(1));
3.8.3 上下文操作
java
StreamContext getStreamContext(String stream) throws IOException, JetStreamApiException
ConsumerContext getConsumerContext(String stream, String durable) throws IOException, JetStreamApiException
-
getStreamContext(...)
获取一个 Stream 的上下文对象,可以进行:
-
消息浏览(
readMessage()
) -
获取状态
-
处理历史数据(快照、元信息等)
-
-
getConsumerContext(...)
获取某个 Consumer 的上下文(行为、进度、状态、ack 延迟等)
示例:
java
StreamContext sc = jetStream.getStreamContext("my-stream");
StreamInfo info = sc.getStreamInfo();
ConsumerContext cc = jetStream.getConsumerContext("my-stream", "my-consumer");
ConsumerInfo ci = cc.getConsumerInfo();