ActiveMQ最佳实践

153 阅读19分钟
  1. 如何保证消息不丢失配置

在生产环境中,确保 ActiveMQ 消息不丢失是至关重要的。这涉及到从消息的发送、存储到消费的整个生命周期的各个环节。以下是实现 ActiveMQ 消息不丢失的最佳实践和相关手段:

  1. 消息发送端 (Producer) 的保障

  1. 持久化消息 (Persistent Messages):

    1. 启用持久化投递模式: 这是最基本也是最重要的保障。生产者发送消息时,必须将消息的投递模式设置为 PERSISTENT。这意味着 ActiveMQ Broker 会将消息写入磁盘,即使 Broker 崩溃或重启,消息也不会丢失。
    2. Spring Boot 配置:JmsMessagingTemplate 中设置 setDeliveryMode(DeliveryMode.PERSISTENT); (默认就是持久化的,可以不显示设置这个参数)可以确保默认发送的消息是持久化的。
    3. ActiveMQConnectionFactory 配置: ActiveMQConnectionFactory 上的 setAlwaysSyncSend(false)setUseAsyncSend(true) 允许异步发送以提高性能,但要确保消息依然被 Broker 持久化。
  2. 事务性发送 (Transactional Send):

    1. JMS 事务: 生产者可以使用 JMS 事务来发送消息。在一个事务中发送多条消息,只有当事务提交时,所有消息才会被发送到 Broker。如果事务回滚,所有消息都不会被发送。这确保了消息发送的原子性。
    2. Spring JmsTransactionManager 结合 Spring 的事务管理,可以将消息发送纳入到业务事务中,例如与数据库操作一起提交或回滚。
  3. 发送确认机制 (Publisher Confirms/Acknowledgement):

    1. 异步发送的风险: 虽然异步发送能提高吞吐量,但如果 Broker 在收到消息前崩溃,消息可能会丢失。
    2. producerWindowSize ActiveMQ 客户端的 producerWindowSize 参数允许生产者在等待 Broker 确认之前发送一定量的消息。当窗口满时,生产者会阻塞直到收到确认。这在性能和可靠性之间提供了平衡。
    3. sendTimeout 设置发送超时,防止生产者无限期等待 Broker 响应。
    4. Broker 确认: 确保 Broker 成功接收并持久化了消息。对于同步发送,Broker 会在持久化后返回确认。对于异步发送,客户端需要处理 Broker 的异步确认(尽管在 JMS API 层面不直接暴露,ActiveMQ 内部会处理)。
  1. Broker 端的保障 (存储与高可用)

  1. 持久化存储 (Persistence):

    1. KahaDB (推荐): ActiveMQ 默认推荐的持久化存储,它是一个基于文件系统的持久化引擎,性能良好且非常可靠。
    2. JDBC 持久化: 将消息存储到关系型数据库中。虽然更易于管理和备份,但通常性能不如 KahaDB。
    3. 配置优化: 确保 Broker 的持久化路径有足够的磁盘空间,并且使用高性能的磁盘(如 SSD)。
  2. 高可用集群 ( High Availability / Clustering ):

    1. Master-Slave (主从) 模式:

      • 共享存储主从: 多个 Broker 实例共享同一个持久化存储(如共享文件系统或数据库)。只有一个 Broker 是活跃的 Master,其他是 Slave。当 Master 失败时,一个 Slave 会接管并使用相同的存储。这是防止 Broker 单点故障的常见方式。
      • 非共享存储主从 (Replicated LevelDB/KahaDB): ActiveMQ 也可以配置为复制其持久化存储(例如,通过 LevelDB Store With ZooKeeper 或一些第三方复制文件系统)。这种方式更为复杂,但提供了更高的冗余性。
    2. Networks of Brokers (Broker 网络):

      • 多个独立的 Broker 实例通过网络连接起来,可以路由消息。这提供了水平扩展能力,但每个 Broker 依然有自己的持久化存储。
      • 消息的可靠性取决于消息是否路由到正确的 Broker 以及该 Broker 的持久化配置。
      • 消息丢失的风险在于,如果消息路由到某个 Broker 后该 Broker 崩溃,而消息尚未被消费且未被正确持久化或复制到其他 Broker。
  3. 死信队列 (Dead Letter Queue - DLQ):

    1. ActiveMQ 会将无法成功投递的消息(例如,消息过期、消费者无法处理、消息回滚次数超过限制)发送到死信队列 (ActiveMQ.DLQ)。
    2. 重要性: DLQ 能够防止消息无限期地阻塞队列,并允许管理员或自动化流程检查、修正和重新处理这些“问题”消息,从而避免消息丢失。
    3. 配置策略: 可以配置消息重试次数、重试延迟策略,以及达到重试上限后将消息发送到 DLQ 的行为。
  1. 消息消费端 (Consumer) 的保障

  1. 客户端确认模式 (Client Acknowledgment Mode):

    1. AUTO_ACKNOWLEDGE (自动确认): 消费者收到消息后自动确认,最简单但有丢失消息的风险(如果消息处理失败但已自动确认,则消息会丢失)。

    2. CLIENT_ACKNOWLEDGE (客户端手动确认): 消费者处理完消息后,需要显式调用 message.acknowledge() 进行确认。如果处理失败未确认,消息会回滚并重新投递。这提供了更高的可靠性,但增加了编码复杂性。

    3. DUPS_OK_ACKNOWLEDGE (延迟确认): 允许 JMS Provider 延迟确认,可能导致消息重复,但性能较好。

    4. 事务性会话 (Transactional Session):

      • 消费者可以在事务性会话中消费消息。在一个事务中处理多条消息,只有当事务提交时,所有消息才会被确认。如果事务回滚,所有消息都会回滚并重新投递。
      • Spring JmsListenerContainerFactory 可以配置事务管理器。
  2. 持久订阅者 (Durable Subscribers - 针对 Topic):

    1. 对于 Topic 消息,如果消费者离线时仍需接收消息,必须使用持久订阅。
    2. 配置: 需要在 JmsListenerContainerFactory 中设置唯一的 clientIdsetSubscriptionDurable(true),并在 @JmsListener 中指定 subscription 名称。
    3. 重要性: 确保即使消费者应用程序宕机,Broker 也会为该订阅者存储消息,待其上线后重新投递。
  3. 幂等性处理:

    1. 即使有了所有的保障措施,由于网络抖动、超时等原因,消息仍可能被重复投递。
    2. 解决方案: 消费者应用程序必须具备消息处理的幂等性,即多次处理同一条消息不会导致副作用(例如,重复创建订单、重复扣款)。这通常通过在业务逻辑中引入唯一 ID(如消息 ID 或业务 ID)并进行去重检查来实现。
  4. 异常处理和重试机制:

    1. 在消费者内部,对消息处理逻辑进行健壮的异常处理。
    2. 使用 Spring JMS 提供的 errorHandler 或自定义重试策略(如 FixedBackOffExponentialBackOff),允许消息在处理失败后进行有限次数的重试,然后再将其发送到 DLQ。
  1. 系统层面的保障

  1. 监控和告警:

    1. 全面监控 ActiveMQ Broker 的状态、队列/Topic 的消息数量、消费者数量、死信队列、内存和磁盘使用情况等。
    2. 设置关键指标的告警,以便在出现异常时(如队列堆积、连接中断、Broker 崩溃)能够及时响应。
    3. 使用 Prometheus、Grafana 等工具构建监控仪表盘。
  2. 日志记录

    1. 详细的日志记录对于问题排查至关重要。记录消息的发送、接收、处理状态以及任何错误或异常。
  3. 备份和恢复:

    1. 定期备份 ActiveMQ 的持久化存储(KahaDB 数据文件)。
    2. 制定详细的灾难恢复计划,以便在极端情况下能够快速恢复 Broker 和数据。
  1. 总结 最佳实践

  • 消息发送: 总是使用持久化消息,并考虑在关键业务场景中使用事务性发送。对于异步发送,关注 producerWindowSize 和 Broker 确认机制。
  • Broker 端: 使用高可靠的持久化存储(如 KahaDB),并配置高可用集群(共享存储主从是常用且易于维护的方案)。有效利用死信队列
  • 消息消费: 对于 Topic,根据业务需求选择持久订阅。在业务逻辑中实现幂等性。使用客户端手动确认或事务性消费,并结合异常处理和重试机制
  • 全链路监控: 部署全面的监控系统,实现告警和日志记录,并定期备份数据。

通过综合运用上述手段,可以最大程度地保障 ActiveMQ 在生产环境中的消息不丢失,从而确保业务的稳定性和数据的一致性。

  1. ActiveMQ默认存储的刷盘策略是怎样的?

ActiveMQ 默认推荐的持久化存储是 KahaDB

KahaDB 的刷盘机制设计旨在平衡性能和可靠性。它通常不是实时(每条消息)强制刷盘,而是采用一种更高效的策略:

  1. Append-only Log (追加日志): KahaDB 使用一种“追加日志”的写入方式。所有消息数据和元数据都被顺序地追加到数据日志文件中。这种顺序写入的性能远高于随机写入,因为它避免了磁盘寻道时间。

  2. 写缓存 (Write Cache) 和批量刷盘 (Batch Flushing):

    1. 消息数据首先被写入内存中的缓存。

    2. KahaDB 会周期性地将缓存中的数据批量地刷入磁盘。这个刷盘动作不是针对每条消息,而是针对一个批次或在特定事件发生时触发。

    3. fsync (强制同步): 只有当数据被 fsync 到磁盘后,才能保证消息的持久性。KahaDB 会在以下情况下执行 fsync

      • 事务提交时: 如果消息是在事务中发送的,当事务提交时,KahaDB 会确保相关的事务日志和消息数据被刷盘。
      • 达到一定写入量或时间间隔: KahaDB 会根据配置,在写入数据量达到一定阈值或经过一定时间间隔后,自动触发刷盘操作。
      • Broker 关闭前: 在 Broker 正常关闭时,所有待刷盘的数据都会被强制刷入磁盘。
  3. 恢复日志 (Recovery Log ) / Journaling:

    1. KahaDB 核心的持久化原理是基于事务日志 (Journaling) 。所有操作(如消息的发送、消费、删除)都会先写入日志文件。
    2. 这些日志文件是追加写入的,并且一旦写入日志,即使还没有完全刷盘到数据文件,系统也可以通过重放日志来恢复状态。
    3. 检查点 (Checkpoints): KahaDB 会定期创建检查点,将内存中的索引和元数据写入磁盘,并标记哪些日志文件可以被清除。

总结来说:

ActiveMQ KahaDB 的刷盘机制是基于日志和批量刷盘的,而不是每条消息都立即强制刷盘。

  • 优点: 这种机制提供了很好的写入性能,因为它避免了频繁的磁盘随机 I/O。

  • 潜在风险与规避:

    • 瞬时消息丢失: 在极少数情况下,如果 Broker 在消息写入内存缓存但尚未被 fsync 到磁盘之前突然崩溃(例如断电),那么这部分未刷盘的消息可能会丢失。

    • 规避方式:

      • 高可用集群 ( Master-Slave with shared storage): 这是最主要的保障,即使一个 Broker 崩溃,另一个 Broker 也可以立即接管并访问相同的持久化存储,从而确保消息不丢失。
      • 配置 activemq.xml 中的 journalMaxFileLength checksum 等: 调整 KahaDB 的参数可以影响日志文件大小和刷盘频率,但通常默认配置在性能和可靠性之间有较好的平衡。
      • 使用 NIO 2: 某些 ActiveMQ 版本支持更先进的 NIO 2 I/O 模式,这可以提供更细粒度的控制和潜在的性能改进。

对于大多数生产环境,KahaDB 的默认配置已经能够提供非常高的持久性保证。结合主从高可用配置,即使是极端情况下的 Broker 崩溃,也能确保消息的持久性和可用性。

  1. 消息生产者发送消息时使用持久化消息,但是对于topic消息并没有使用持久订阅者的方式,那么在消费者程序宕机重启后,才能收到之前的消息吗?

不能。

如果你使用持久化消息发送到 Topic,但消费者程序使用的是非持久订阅者,那么在消费者程序宕机重启后,无法收到在它离线期间发布到该 Topic 的消息。

原因解释:

  1. 持久化消息 (Persistent Messages on Topic ): 当生产者发送持久化消息到 Topic 时,ActiveMQ Broker 会将这些消息写入其持久化存储(例如 KahaDB)。这确保了即使 Broker 崩溃并 重启 ,这些消息也不会丢失。这些消息会保留在 Topic 上,等待被订阅者消费。

  2. 非持久订阅者 (Non-Durable Subscribers for Topic):

    1. 生命周期: 非持久订阅者的生命周期与客户端的 JMS 连接是紧密绑定的。当客户端应用程序启动并成功连接到 Broker 时,非持久订阅者就“上线”了。当客户端的连接断开(例如,应用程序正常关闭、崩溃、网络中断)时,这个非持久订阅者就会立即“下线”并从 Broker 端消失。
    2. 消息接收范围: 非持久订阅者只接收在其在线期间发布到 Topic 的消息。

结合起来:

  • 生产者发送的持久化消息确实被 Broker 存储了。
  • 但是,当你的消费者程序宕机时,它的非持久订阅者会立即消失。
  • 在消费者宕机期间,即使有新的持久化消息发布到 Topic,由于没有活跃的非持久订阅者在监听,Broker 不知道应该将这些消息投递给谁。
  • 当消费者程序重启并重新连接时,它会创建一个全新的非持久订阅者。这个新的订阅者只会接收从它重新上线那一刻开始发布到 Topic 的新消息。它无法“回溯”并接收在它离线期间发布的消息。

总结:

消息的持久性 (Persistent Messages) 保证了Broker不会丢失消息。 订阅的持久性 (Durable Subscribers) 保证了消费者不会错过在它离线期间发布到 Topic 的消息。

对于 Topic 模式,如果你需要保证消费者在离线后能收到离线期间错过的消息,必须使用持久订阅者

  • Queue (队列) 模式: 队列的消息本身就是持久的(除非显式设置为非持久),并且队列的消息是独占消费的。无论消费者是否在线,消息都会保留在队列中,直到被一个消费者成功消费。所以,对于队列来说,即使消费者宕机,重启后也能收到之前未被消费的消息,不需要“持久订阅者”的概念。

  • Topic (主题) 模式: Topic 消息是发布/订阅模式,每条消息可以有多个消费者。因此,对于 Topic,只有当一个持久订阅者在线时,Broker 才会为它保存离线期间的消息。

  1. 怎么区分我的代码是使用的Queue的模式哈市Topic的模式

要区分你的 Spring Boot ActiveMQ 消费者代码是使用 Queue (队列) 模式还是 Topic (主题) 模式,你需要查看两个地方:

  1. JmsListenerContainerFactory 的配置 ( tyyfepJmsListener )
  2. @JmsListener 注解的 destination 名称的约定

让我们逐一分析:

  1. 检查 JmsListenerContainerFactory 的配置

你提供了一个名为 tyyfepJmsListenerJmsListenerContainerFactory 的配置:

@Bean(name = "tyyfepJmsListener")
public JmsListenerContainerFactory<?> newiotV2JmsListenerFactory(@Qualifier("tyyfepFactory") PooledConnectionFactory connectionFactory) {
    DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setPubSubDomain(true); // <-- 重点在这里
    return factory;
}

关键点在于这一行: factory.setPubSubDomain(true);

  • factory.setPubSubDomain(true) :

    • 表示这个 JmsListenerContainerFactory 是为 发布/订阅 (Pub/Sub) 域 配置的。
    • 在 JMS 中,发布/订阅域就是 Topic (主题) 模式。
    • 这意味着所有使用这个 factory@JmsListener 都会默认被视为 Topic 的消费者。
  • factory.setPubSubDomain(false) (或不设置,因为默认是 false):

    • 表示这个 JmsListenerContainerFactory 是为 点对点 ( Point-to-Point ) 域 配置的。
    • 在 JMS 中,点对点域就是 Queue (队列) 模式。
    • 如果你不设置 setPubSubDomain,或者显式设置为 false,那么监听器将默认监听队列。

结论: 根据你的 tyyfepJmsListener 配置,factory.setPubSubDomain(true); 明确表示你的消费者是工作在 Topic 模式

  1. 检查 @JmsListener 注解的 destination 名称的约定

虽然 JmsListenerContainerFactory 的配置是主要的决定因素,但目标名称的约定也可以提供线索,尤其是在 ActiveMQ 中。

你的消费者代码:

@JmsListener(destination = "${activemq.cluster.testTopic}", containerFactory = "tyyfepJmsListener")
public void onMessage(final Message message) { ... }

这里 destination 的值来自 ${activemq.cluster.testTopic}

  • 命名约定:

    • ActiveMQ 默认情况下,如果目标名称以 queue. 开头,它会被视为一个队列。
    • 如果目标名称以 topic. 开头,它会被视为一个主题。
    • 如果目标名称没有这些前缀,ActiveMQ 会根据 ConnectionFactoryJmsListenerContainerFactorysetPubSubDomain 设置来决定是队列还是主题。
  • 你的情况:

    • 虽然你的 destination 变量名是 testTopic,暗示了这是一个 Topic,但更重要的是它的实际值。
    • 如果 ${activemq.cluster.testTopic} 的值是 my.application.topic (没有 queue.topic. 前缀),那么它会遵循 tyyfepJmsListenersetPubSubDomain(true) 的配置,被视为一个 Topic。
    • 如果它的值是 topic.my.application.topic,则会更明确地被识别为 Topic。
    • 如果它的值是 queue.my.application.queue,即使 setPubSubDomain(true),也可能会因目标名称的显式前缀而产生冲突或特定行为(尽管在 Spring JMS 中 setPubSubDomain 的优先级更高)。

在你的案例中, factory.setPubSubDomain(true) 是决定性的因素。

综合判断:

基于你提供的 JmsConfig 和消费者代码,你的消费者正在使用 Topic (主题) 模式

如何确定:

  1. JmsListenerContainerFactory (tyyfepJmsListener) 中设置了 factory.setPubSubDomain(true)。这是明确指示使用 Topic 模式的配置。
  2. @JmsListenerdestination 变量名 testTopic 也暗示了这一点。

因此,如果你希望这些 Topic 消息在消费者离线后不丢失,你确实需要将你的消费者配置为持久订阅者,就像我们之前讨论的那样,通过在 tyyfepJmsListener 中设置 clientIdsetSubscriptionDurable(true),以及在 @JmsListener 中设置 subscription 属性。

  1. 为什么默认情况下,创建的toptic 没有持久化订阅

核心原因在于 ActiveMQ 对 Topic 订阅者的两种类型:

  1. 非持久订阅者 (Non-Durable Subscribers):

    1. 何时生成: 当你的 Spring Boot 应用程序启动,并且 JmsListener 容器连接到 ActiveMQ Broker 并开始监听 testClusterTopic 时,就会创建非持久订阅者。它是在消费者连接到 Broker 并激活 监听器 时实时创建的
    2. 生命周期: 非持久订阅者的生命周期与客户端的 JMS 连接紧密绑定。当客户端的连接断开时(例如,应用程序停止、网络中断),这个非持久订阅者就会自动消失。
    3. 消息接收: 非持久订阅者只接收在其活跃期间(连接在线时)发布到 Topic 的消息。在它们离线期间发布的消息将不会被接收。
    4. 控制台显示: ActiveMQ Web 控制台的 "Subscribers" 页面通常不会单独列出非持久订阅者。这是因为它们是临时的,并且它们的数量通常包含在 Topic 的 "Number Of Consumers" 统计中。你可能在 Topic 列表中的 "Number Of Consumers" 列看到了数量,这个数量就包含了你的非持久订阅者。
  2. 持久订阅者 (Durable Subscribers):

    1. 何时生成: 持久订阅者是在客户端第一次连接到 Broker,并使用唯一的 clientIdsubscriptionName 创建订阅时生成的。一旦创建,即使客户端离线,该订阅也会在 Broker 上持久存在。
    2. 生命周期: 持久订阅者具有独立的生命周期,不完全依赖于客户端的连接。它们会在 Broker 上保留,直到被明确地取消订阅(例如,通过代码调用 session.unsubscribe())。
    3. 消息接收: 持久订阅者即使在客户端离线期间也能接收 Topic 上的消息。Broker 会为离线的持久订阅者存储消息,当客户端重新上线时,它会接收到离线期间错过的消息。
    4. 控制台显示: ActiveMQ Web 控制台的 "Subscribers" 页面主要就是用来展示这些持久订阅者的,因为它们是 Broker 上持久存在的实体。
    5.   你的 ActiveMqConsumer 示例分析:
    6.   你提供的 @JmsListener 配置:
    7. @JmsListener(destination = "${activemq.cluster.testTopic}", containerFactory = "tyyfepJmsListener")
      public void onMessage(final Message message) { ... }
      

默认情况下,@JmsListener 创建的是非持久订阅者。因为你没有在 tyyfepJmsListenerDefaultJmsListenerContainerFactory 中设置 setClientId()setSubscriptionDurable(true),也没有在 @JmsListener 注解中添加 subscription 属性。

因此,你当前的情况是:

你的 Spring Boot 应用程序成功连接到 ActiveMQ Broker,并且已经作为 testClusterTopic 的一个非持久消费者在活跃地监听消息。ActiveMQ 控制台在 Topic 列表中的 "Number Of Consumers" 列会显示你的消费者数量,但不会在 "Subscribers" 页面中单独列出,因为该页面主要用于管理持久订阅。

如果你希望在 ActiveMQ 控制台的 "Subscribers" 页面看到你的订阅者,你需要将其配置为持久订阅者,具体步骤如下:

主要调整点:

  1. 消息发送持久化 (JmsMessagingTemplate):

    1. 对于消息发送(生产者),持久化是通过在发送时指定消息的投递模式 (delivery mode) 来实现的。通常,你需要设置 deliveryModePERSISTENT
    2. JmsMessagingTemplate 中,你可以通过设置默认的投递模式来实现。
  2. 消息订阅持久化 (DefaultJmsListenerContainerFactory):

    1. 对于 Topic 订阅者,持久化订阅需要两个关键配置:clientIdsubscriptionDurable
    2. clientId: 必须为每个客户端应用程序实例设置一个唯一的 ID。
    3. setSubscriptionDurable(true): 启用持久订阅功能。
    4. setPubSubDomain(true): 确保是发布/订阅(Topic)模式。
    5.   优化后的 JmsConfig 配置如下:
package com.wx.embc.iotJoin.config;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.pool.PooledConnectionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.jms.support.converter.MappingJackson2MessageConverter; // 导入消息转换器
import org.springframework.jms.support.converter.MessageConverter; // 导入消息转换器
import org.springframework.jms.support.converter.MessageType; // 导入消息转换器

import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode; // 导入 DeliveryMode

@Configuration
public class JmsConfig {

    // 建议为每个应用实例设置一个唯一的客户端ID,例如从配置文件读取或基于主机名
    @Value("${spring.application.name:default-app-name}")
    private String applicationName;

    @Bean(name = "tyyfepFactory")
    public PooledConnectionFactory newiotV2Factory(
            @Value("${tyyfep.activemq.url}") String brokerUrl,
            @Value("${tyyfep.activemq.user}") String username,
            @Value("${tyyfep.activemq.password}") String password) {
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory();
        factory.setBrokerURL(brokerUrl);
        factory.setUserName(username);
        factory.setPassword(password);
        factory.setAlwaysSyncSend(false);
        factory.setUseAsyncSend(true);
        // 可以根据需要调整异步发送的缓冲区大小和阈值,以优化性能
        // factory.setProducerWindowSize(1024000); // 1MB 生产者窗口大小
        // factory.setAlwaysSyncSend(false); // 确保异步发送被使用

        PooledConnectionFactory pcf = new PooledConnectionFactory();
        pcf.setConnectionFactory(factory);
        pcf.setMaxConnections(100);
        // 后台对象清理时,休眠时间超过了3000毫秒的对象为过期
        pcf.setTimeBetweenExpirationCheckMillis(3000);
        return pcf;
    }

    /**
     * 配置 Topic 监听器容器工厂,支持持久订阅
     * @param connectionFactory 连接工厂
     * @return JmsListenerContainerFactory
     */
    @Bean(name = "tyyfepJmsListener")
    public JmsListenerContainerFactory<?> newiotV2JmsListenerFactory(@Qualifier("tyyfepFactory") PooledConnectionFactory connectionFactory) {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setPubSubDomain(true); // 启用发布/订阅(Topic)模式

        // *** 配置持久订阅的关键点 ***
        // 1. 设置客户端ID:必须是唯一的,通常与应用程序实例绑定
        // 如果你的应用会有多个实例运行,需要确保每个实例的 clientId 是唯一的
        // 例如:factory.setClientId(applicationName + "-" + UUID.randomUUID().toString());
        // 或者从环境变量/配置文件中获取一个更稳定的实例ID
        factory.setClientId(applicationName + "-consumer-client"); // 使用应用名称作为基础,增加可识别性

        // 2. 启用持久订阅
        factory.setSubscriptionDurable(true);
        // 3. 设置消息转换器(可选但推荐,Spring Boot 默认会配置)
        factory.setMessageConverter(jacksonJmsMessageConverter());

        return factory;
    }

    /**
     * 配置 JmsMessagingTemplate 用于消息发送
     * @param connectionFactory 连接工厂
     * @return JmsMessagingTemplate
     */
    @Bean(name = "tyyJmsTemplate")
    public JmsMessagingTemplate jmsTemplate2(@Qualifier("tyyfepFactory") PooledConnectionFactory connectionFactory) {
        JmsMessagingTemplate template = new JmsMessagingTemplate(connectionFactory);

        // *** 配置消息发送持久化的关键点 ***
        // 设置默认投递模式为持久化
        template.setDeliveryMode(DeliveryMode.PERSISTENT);
        // 设置消息转换器,确保发送和接收消息的格式一致
        template.setMessageConverter(jacksonJmsMessageConverter());

        return template;
    }

    /**
     * 配置一个通用的 Jackson 消息转换器,用于 Spring JMS
     * 这可以确保 JSON 消息在发送和接收时正确地序列化和反序列化
     * @return MessageConverter
     */
    @Bean
    public MessageConverter jacksonJmsMessageConverter() {
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        converter.setTargetType(MessageType.TEXT); // 通常将Java对象转换为文本消息(JSON字符串)
        converter.setTypeIdPropertyName("_type"); // 在消息头中添加类型信息,方便反序列化
        return converter;
    }
}

额外的 application.properties 配置示例:

# 为应用程序设置一个名称,用于生成唯一的客户端ID
spring.application.name=test-iot-join-app

# 如果你运行多个实例,可以考虑更唯一的ID
# tyyfep.activemq.consumer-client-id=${spring.application.name}-${random.uuid}

使用说明和注意事项:

  1. 消息发送持久化 ( JmsMessagingTemplate ):

    1. 通过 template.setDeliveryMode(DeliveryMode.PERSISTENT);,所有通过 tyyJmsTemplate 发送的消息,默认都会以持久化的方式发送。这意味着即使 Broker 崩溃并重启,这些消息也不会丢失,除非它们被消费者消费。
    2. 如果你在特定场景下需要发送非持久消息,你可以在发送时显式地覆盖投递模式,例如:template.convertAndSend(destination, message, message -> { message.setJMSDeliveryMode(DeliveryMode.NON_PERSISTENT); return message; });
  2. 消息订阅持久化 ( DefaultJmsListenerContainerFactory @JmsListener ):

    1. factory.setClientId("your-application-unique-client-id"); : 这是持久订阅的核心。每个连接到 ActiveMQ Broker 的应用程序实例,如果它想要创建持久订阅,都必须拥有一个全局唯一clientId

      • 在生产环境中,这个 clientId 应该能够区分不同的应用程序实例。例如,可以结合应用程序名称、服务器 IP 地址或一个服务实例 ID 来生成。
      • 如果两个不同的应用程序实例使用了相同的 clientId ,并且都尝试创建持久订阅,只有第一个连接会成功,第二个会失败。
    2. factory.setSubscriptionDurable(true); : 这告诉 JmsListenerContainerFactory 默认创建持久订阅。

    3. @JmsListener(destination = "${activemq.cluster.testTopic}", containerFactory = "tyyfepJmsListener", subscription = "myTestClusterTopicSubscription") : 你需要在你的 ActiveMqConsumer 类中的 @JmsListener 注解上添加 subscription 属性。这个 subscription 名称对于同一个 clientId 下的同一个 Topic 必须是唯一的。

      • 这个 subscription 名称将会在 ActiveMQ 控制台的 "Subscribers" 页面中显示。
  3. 消息转换器 ( MessageConverter ):

    1. 添加 MappingJackson2MessageConverter 是一个很好的实践。它允许你发送和接收 Java 对象,这些对象会自动序列化为 JSON 字符串,并存储在 JMS TextMessage 中。这样可以避免手动进行字符串序列化和反序列化。
    2. 确保你的生产者和消费者都使用相同的消息转换器配置,以避免兼容性问题。
  4. applicationName 的使用: 通过 @Value("${spring.application.name:default-app-name}") private String applicationName; 获取 Spring Boot 应用程序的名称,并将其用于构建 clientId,这是一种推荐的做法,因为它使得 clientId 具有可读性并且通常在应用级别是唯一的。如果部署多个实例,你可能需要更进一步,例如结合 random.uuid 来确保每个实例的 clientId 都不同。

完成这些修改后,当你的 Spring Boot 应用程序启动并连接到 ActiveMQ Broker 时:

  • 所有通过 tyyJmsTemplate 发送的消息默认将是持久化的
  • testClusterTopic 的监听器将创建持久订阅,并且你可以在 ActiveMQ 控制台的 "Subscribers" 页面看到它。