三、JetStream详解

一,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快速入门

  1. 引入依赖

    xml 复制代码
    <dependency>        
        <groupId>io.nats</groupId>       
        <artifactId>jnats</artifactId>       
        <version>2.21.1</version>
    </dependency>
  2. 编写配置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();
        }
    
    }
  3. 编写消息发送者

    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());
        }
    
    }
  4. 编写消费者

    • 编写sub消费者

      java 复制代码
      package 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消费者

      java 复制代码
      package 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("线程被中断");
                  }
              }
          }
      }
  5. 封装JetStreamResourceManager用于统一管理stream和consumer的生命周期

    java 复制代码
    package 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);
            }
        }
    }
  6. 测试接口

    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(拒绝新消息)
  1. name --- 流名称(必须项)

    • 类型String

    • 作用:唯一标识这个消息流(stream),后续消费者、监控、管理都依赖这个名字。

    • 是否必须:✅ 必须设置

    • 命名规范 :只能使用字母、数字、-_,不能重复。

    • 创建成功后,jetstreamctl stream info user-events 可查看状态。

    • 如果你尝试使用已存在的 nameaddStream 会报错:stream name already in use

      故我们的stream流只需要创建一次即可

  2. subjects --- 流绑定的主题(subject)

    • 类型String...List<String>

    • 作用:定义这个 Stream 会"捕获"哪些主题的消息(支持通配符)

    • 默认值:无,必须设置,否则不会接收任何消息

    • 可用的通配符:

      通配符 含义 示例
      * 匹配一个片段 user.* 可匹配 user.login, user.logout
      > 匹配剩余所有 user.> 可匹配 user.a, user.a.b, user.x.y.z
  3. storageType --- 消息存储方式

    • 类型StorageType 枚举
    • 可选值
      • Memory:内存中存储,重启丢失
      • File:落盘,支持持久化
  4. retentionPolicy --- 消息保留策略

    • 类型RetentionPolicy 枚举

    • 作用:控制消息保留直到什么条件下可以被删除

      含义
      Limits(默认) 根据 maxMessages, maxBytes, maxAge 清除
      Interest 只保留还有"消费者订阅兴趣"的消息
      WorkQueue 每条消息只保留给一个 consumer,适合任务队列

      默认选 Limits,做任务队列(每条只消费一次)可用 WorkQueue

  5. maxMessages, maxBytes --- 消息数量和体积上限

    • 控制消息最多保留多少条或多少字节
    • 超过限制后按 discardPolicy 删除
    • 如果两者都配置,哪个先到就先触发删除
  6. maxAge --- 每条消息最大保留时间

    • 超过这个时间的消息会被自动删除
    • 不管是否消费,只要时间到了就删
    • maxMsgs, maxBytes 可叠加使用
  7. discardPolicy --- 达到上限后丢弃策略

    选项 含义
    Old(默认) 丢最旧的消息(先进先出)
    New 拒绝新消息(发送失败)
  8. replicas --- 副本数(高可用)

    • 需要 NATS 集群和 JetStream replication 启用
    • 设置为 3 则有主副本+两个备份,容灾能力提升
  9. 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
  1. durable --- Durable 消费者名称

    • 类型String

    • 作用:唯一标识该消费者,用于记录消费进度(offset)

    • 是否必须:推荐设置(尤其是需要消息持久消费时)

    • 影响

      • Durable consumer 可以断线重连,恢复未确认消息
      • 非 durable(临时)消费者断线后,消费状态丢失,消息会重新发送
      场景 有 durable 无 durable
      消费者断开重连 恢复未确认偏移 重头开始接收
      服务重启 消费进度保存 消费状态丢失
  2. ackPolicy --- 消息确认策略

    • 类型AckPolicy 枚举
    • 作用:控制消息确认方式,确保消息不丢失或重复
    含义
    Explicit 手动调用 msg.ack()
    None 不需要确认,性能最高但消息可能丢失
    All 对批量消息的自动确认
  3. ackWait --- ack 超时重投

    • 类型Duration
    • 作用:消息投递后,未收到 ack 多少时间后重发消息
    • 默认:30秒
  4. deliverPolicy --- 消费起点策略

    • 类型DeliverPolicy 枚举
    • 作用:控制消费者从流中哪个位置开始接收消息
    含义
    All 从流中最早消息开始(历史消息都投)
    Last 从最近一条消息开始(不重投历史)
    New 只接收新产生的消息
    ByStartSequence 从指定序号开始
    ByStartTime 从指定时间开始
  5. filterSubject --- 过滤主题

    • 类型String
    • 作用:消费者只接收流中匹配此 subject 的消息
  6. maxDeliver --- 最大重投次数

    • 类型int
    • 作用:消息最多投递次数,超过则丢弃或发送到死信流(DLQ)
  7. replayPolicy --- 消息重放速度

    • 类型ReplayPolicy 枚举
    • 作用:消息投递时的速度
    含义
    Instant 快速投递(默认)
    Original 以消息原始生产速率投递
  8. 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);

常用配置项详解

  1. domain(String domain)

    JetStream 支持"多域(domain)"架构:你可以把不同 Stream/Consumer 部署在不同 JetStream 域中,实现资源隔离。

    • 默认域为 null(全局)
    • 使用前确保你的 NATS 服务端开启了 JetStream 域支持(--js-domain
  2. prefix(String prefix)

    设置 JetStream 所使用的 API 路由前缀。例如,在多租户或 NATS 代理服务下,会改变 API 路由规则。

    • 默认值为空(即 $JS.API.*
    • 注意:这个 prefix 只影响 JetStream 的管理请求,不影响消息 subject。
  3. requestTimeout(Duration timeout)

    设置 JetStream 客户端发起请求(如添加 Stream、获取 info)的最大等待时间。

    • 默认约为 2 秒
    • 服务端压力大或慢时可适当加长

什么是 JetStream 的多租户(Domain)与 API 前缀(Prefix)

  1. 什么是 JetStream 的"多租户"(domain)

    JetStream 是一个持久化消息平台,它允许你创建 Stream、Consumer、消息历史和快照。在复杂的系统中,比如:

    • 微服务架构
    • SaaS 多租户系统
    • 多个项目团队共用一个 NATS 集群

    你可能希望不同租户(用户)拥有彼此隔离的 JetStream 环境 ,互不影响。JetStream 支持这种"逻辑隔离"能力,就是 domain(域)机制


    JetStream Domain 是什么?

    • JetStream domain 是一种逻辑命名空间,每个域相当于一套独立的 JetStream 实例(API 路由、资源空间都隔离)。

    • 你可以在一个 NATS 集群中启用多个 JetStream 域,每个域都有自己独立的 stream、consumer 等。

    一句话理解domain = JetStream 的命名空间。


    如何启用?:服务端配置(需要 NATS 启动时加参数)

    bash 复制代码
    nats-server -js -sd ./store -js-domain tenant-a

    这样你启动了一个名为 tenant-a 的 JetStream 域。

    你也可以在多个服务上分别设置 -js-domain tenant-atenant-b,实现每个租户一个 JetStream 后端实例


    客户端访问特定 domain:

    java 复制代码
    JetStreamOptions options = JetStreamOptions.builder()
        .domain("tenant-a")
        .build();
    
    JetStream js = connection.jetStream(options);
  2. 什么是 JetStream 的 Prefix?

    JetStream 的 API 路由在 NATS 上是通过特定主题实现的,比如:

    bash 复制代码
    $JS.API.STREAM.INFO.stream-name

    这个是默认的 JetStream API 路由格式。

    如果你有代理层、网关层,或者需要将 JetStream 的 API 隐藏在特定路径下,比如:

    bash 复制代码
    mytenant.$JS.API.STREAM.INFO.stream-name

    你可以通过设置 prefix 来实现这个目标。

3.4 JetStreamManagement

  1. 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)
  2. 如何获得 JetStreamManagement 实例

    你需要有一个 Connection 实例,然后调用:

    java 复制代码
    JetStreamManagement jsm = connection.jetStreamManagement();
  3. JetStreamManagement 高阶用法

    • 获取 Stream 的详细状态

      java 复制代码
      StreamInfo 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,检查是否消费滞后、是否需要扩容。

    • 获取/分页所有流和消费者列表

      java 复制代码
      List<String> streams = jsm.getStreamNames(); // 默认100条 获取所有 Stream 名称
      List<String> consumers = jsm.getConsumerNames("orders"); //获取某个流下的所有 Consumer:
    • 强制删除消费者或 Stream(无视状态)

      java 复制代码
      jsm.deleteStream("test-stream");
      jsm.deleteConsumer("orders", "slow-consumer");

      如果你遇到"无法删除 consumer,因为尚有未确认消息"等错误,用 JetStream CLI 或服务端配置可启用"强删":

      bash 复制代码
      nats stream delete orders --force
    • 消息清理:删除序列前的所有消息

      java 复制代码
      jsm.purgeStream("orders");  // 清空整个 stream
      
      PurgeOptions options = PurgeOptions.builder()
          .sequence(1050)  // 删除序号 < 1050 的所有消息
          .build();
      
      jsm.purgeStream("orders", options);

3.5 PushSubscribeOptions

  1. 什么是 PushSubscribeOptions?

    在 JetStream 中,消息消费者有两种模式:

    模式 描述
    Pull 模式 客户端主动拉取消息
    Push 模式 服务端主动推送消息给客户端

    当你使用 Push 模式订阅jetStream.subscribe(...))时,必须指定 PushSubscribeOptions

    它包含了如下关键参数:

    java 复制代码
    PushSubscribeOptions options = PushSubscribeOptions.builder()
        .stream("stream-name")
        .durable("consumer-name")
        .configuration(ConsumerConfiguration) // 自定义消费者行为
        .build();
  2. 常见用途

    功能 是否需要 PushSubscribeOptions
    使用 durable consumer ✅ 是
    配置 ack 策略、重试次数 ✅ 是(通过 ConsumerConfiguration)
    多个 stream 下的 subject ✅ 明确指定 stream
    并发订阅 ✅ 否则可能报错(找不到 stream)
  3. 主要参数详解

    .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)

    内嵌设置消费者的行为,比如:

    java 复制代码
    ConsumerConfiguration config = ConsumerConfiguration.builder()
        .ackPolicy(AckPolicy.Explicit)
        .deliverPolicy(DeliverPolicy.New)
        .maxDeliver(5)
        .deliverSubject("inbox.orders")
        .build();

    再组合进来:

    java 复制代码
    PushSubscribeOptions options = PushSubscribeOptions.builder()
        .stream("orders-stream")
        .durable("orders-processor")
        .configuration(config)
        .build();
  4. 常见错误解析

    错误信息 原因 解决办法
    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

  1. 什么是 PullSubscribeOptions?

    在 JetStream 的 拉取消费模式 中,你需要先通过:

    java 复制代码
    JetStreamSubscription subscription = jetStream.subscribe(subject, pullSubscribeOptions);

    注册一个「拉取式消费者(pull consumer)」,然后使用:

    java 复制代码
    subscription.fetch(10, Duration.ofSeconds(2));

    主动拉取消息。

    PullSubscribeOptions 就是在这个订阅动作中,指定该消费者的详细配置。

  2. 使用场景

    使用目的 是否需要 PullSubscribeOptions
    ✅ 设置 durable(持久订阅) 必须
    ✅ 配置 maxDeliver、ack 策略等 推荐(通过 ConsumerConfiguration
    ✅ 拉取模式注册多个消费者 强烈建议
    ❌ 临时拉取,不需要 durable 可以省略(但容易混乱)
  3. 各项参数详解

    .stream(String streamName)

    明确绑定这个订阅属于哪个 Stream。

    arduino 复制代码
    .stream("orders-stream")

    必须指定,如果多个 stream 有重叠 subject,系统无法自动判断。


    .durable(String durableName)

    指定一个持久化的消费者名称,用于:

    • 保持消费进度(ack)
    • 消费断开后恢复
    • 防止"Consumer already configured as push" 报错
    java 复制代码
    .durable("inventory-puller")

    拉取模式必须设置 durable,否则报错:

    java 复制代码
    JetStreamApiException: consumer must be durable for pull mode

    .configuration(ConsumerConfiguration)

    设置这个拉取式消费者的详细行为(ack 策略、最大消息数、延迟等)

    java 复制代码
    ConsumerConfiguration cc = ConsumerConfiguration.builder()
        .ackPolicy(AckPolicy.Explicit)
        .deliverPolicy(DeliverPolicy.New)
        .maxDeliver(5)
        .build();

    然后:

    java 复制代码
    .configuration(cc)
  4. 常见错误解析

    报错信息 原因 解决方案
    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")
  5. Push 和 Pull 对比(订阅参数)

    模式 使用类 是否必须 durable 使用场景
    Push PushSubscribeOptions 可选但推荐 实时推送,适合低延迟
    Pull PullSubscribeOptions ✅ 必须 控制节奏、批处理、高并发拉

3.7 PublishOptions

  1. 什么是 PublishOptions

    在 JetStream 中,默认的 jetStream.publish(subject, data)最简单的同步发布

    而如果你想要:

    需求 是否用 PublishOptions
    指定消息属于哪个 Stream(多 Stream 情况) ✅ 推荐
    带上 MsgId 做幂等控制 ✅ 推荐
    关联到某个 expected sequence(CAS) ✅ 必须
    发布目标为 domain 中的 JetStream ✅ 必须
    幂等、分区、幂等性策略 ✅ 必须

    就需要 PublishOptions

  2. 创建 PublishOptions 的常见方式

    java 复制代码
    PublishOptions 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();
  3. 主要参数详解

    .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 中开启的域使用。

  4. 异常情况示例

    报错信息 可能原因 解决办法
    Wrong last sequence expectedLastSequence 与实际不一致 刷新缓存或关闭 expected
    Stream does not match Stream 不存在或未指定正确 设置 .stream(...)
    Duplicate msgId messageId 已经使用过 生成新的 UUID

  5. 建议使用场景

    业务需求 建议配置
    重要写入幂等保障 .messageId(...)
    多 stream 下精确投递 .stream(...) + .expectedStream(...)
    多租户平台 .domain(...)
    并发控制写入顺序 .expectedLastSequence(...)
    可观察性 可结合日志打印 msgId、ack seq、耗时等

3.8 JetStream

3.8.1 发布消息

  1. 同步发布(6种)

    java 复制代码
    PublishAck 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) 全功能消息发布
  2. 异步发布(6种)

    java 复制代码
    CompletableFuture<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>,可加回调处理
    • 适合高并发、批量发送等场景

    示例:

    java 复制代码
    jetStream.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)
  1. Push 订阅

    java 复制代码
    JetStreamSubscription 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):

    java 复制代码
    PushSubscribeOptions 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);
  2. Pull 订阅

    java 复制代码
    JetStreamSubscription subscribe(String subject, PullSubscribeOptions options)
    JetStreamSubscription subscribe(String subject, Dispatcher dispatcher, MessageHandler handler, PullSubscribeOptions options)
    • 适合:手动拉取、控制节奏、批量处理
    • 拉取方式通过 subscription.fetch(...)

    示例(Pull):

    java 复制代码
    PullSubscribeOptions 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
  1. getStreamContext(...)

    获取一个 Stream 的上下文对象,可以进行:

    • 消息浏览(readMessage()

    • 获取状态

    • 处理历史数据(快照、元信息等)

  2. 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();

相关推荐
求知摆渡4 分钟前
共享代码不是共享风险——公共库解耦的三种进化路径
java·后端·架构
brzhang15 分钟前
前端死在了 Python 朋友的嘴里?他用 Python 写了个交互式数据看板,着实秀了我一把,没碰一行 JavaScript
前端·后端·架构
该用户已不存在44 分钟前
不知道这些工具,难怪的你的Python开发那么慢丨Python 开发必备的6大工具
前端·后端·python
Xy9101 小时前
开发者视角:App Trace 一键拉起(Deep Linking)技术详解
java·前端·后端
嘻嘻哈哈开森1 小时前
技术分享:深入了解 PlantUML
后端·面试·架构
vvw&1 小时前
Linux 中的 .bashrc 是什么?配置详解
linux·运维·服务器·chrome·后端·ubuntu·centos
厚道1 小时前
Elasticsearch 的存储原理
后端·elasticsearch
不甘打工的程序猿1 小时前
nacos-client模块学习《心跳维持》
后端·架构
方块海绵1 小时前
mysql 中使用 json 类型的字段
后端