-
如何保证消息不丢失配置
在生产环境中,确保 ActiveMQ 消息不丢失是至关重要的。这涉及到从消息的发送、存储到消费的整个生命周期的各个环节。以下是实现 ActiveMQ 消息不丢失的最佳实践和相关手段:
-
消息发送端 (Producer) 的保障
-
持久化消息 (Persistent Messages):
- 启用持久化投递模式: 这是最基本也是最重要的保障。生产者发送消息时,必须将消息的投递模式设置为
PERSISTENT。这意味着 ActiveMQ Broker 会将消息写入磁盘,即使 Broker 崩溃或重启,消息也不会丢失。 - Spring Boot 配置: 在
JmsMessagingTemplate中设置setDeliveryMode(DeliveryMode.PERSISTENT);(默认就是持久化的,可以不显示设置这个参数)可以确保默认发送的消息是持久化的。 - ActiveMQConnectionFactory 配置:
ActiveMQConnectionFactory上的setAlwaysSyncSend(false)和setUseAsyncSend(true)允许异步发送以提高性能,但要确保消息依然被 Broker 持久化。
- 启用持久化投递模式: 这是最基本也是最重要的保障。生产者发送消息时,必须将消息的投递模式设置为
-
事务性发送 (Transactional Send):
- JMS 事务: 生产者可以使用 JMS 事务来发送消息。在一个事务中发送多条消息,只有当事务提交时,所有消息才会被发送到 Broker。如果事务回滚,所有消息都不会被发送。这确保了消息发送的原子性。
- Spring
JmsTransactionManager: 结合 Spring 的事务管理,可以将消息发送纳入到业务事务中,例如与数据库操作一起提交或回滚。
-
发送确认机制 (Publisher Confirms/Acknowledgement):
- 异步发送的风险: 虽然异步发送能提高吞吐量,但如果 Broker 在收到消息前崩溃,消息可能会丢失。
producerWindowSize: ActiveMQ 客户端的producerWindowSize参数允许生产者在等待 Broker 确认之前发送一定量的消息。当窗口满时,生产者会阻塞直到收到确认。这在性能和可靠性之间提供了平衡。sendTimeout: 设置发送超时,防止生产者无限期等待 Broker 响应。- Broker 确认: 确保 Broker 成功接收并持久化了消息。对于同步发送,Broker 会在持久化后返回确认。对于异步发送,客户端需要处理 Broker 的异步确认(尽管在 JMS API 层面不直接暴露,ActiveMQ 内部会处理)。
-
Broker 端的保障 (存储与高可用)
-
持久化存储 (Persistence):
- KahaDB (推荐): ActiveMQ 默认推荐的持久化存储,它是一个基于文件系统的持久化引擎,性能良好且非常可靠。
- JDBC 持久化: 将消息存储到关系型数据库中。虽然更易于管理和备份,但通常性能不如 KahaDB。
- 配置优化: 确保 Broker 的持久化路径有足够的磁盘空间,并且使用高性能的磁盘(如 SSD)。
-
高可用集群 ( High Availability / Clustering ):
-
Master-Slave (主从) 模式:
- 共享存储主从: 多个 Broker 实例共享同一个持久化存储(如共享文件系统或数据库)。只有一个 Broker 是活跃的 Master,其他是 Slave。当 Master 失败时,一个 Slave 会接管并使用相同的存储。这是防止 Broker 单点故障的常见方式。
- 非共享存储主从 (Replicated LevelDB/KahaDB): ActiveMQ 也可以配置为复制其持久化存储(例如,通过 LevelDB Store With ZooKeeper 或一些第三方复制文件系统)。这种方式更为复杂,但提供了更高的冗余性。
-
Networks of Brokers (Broker 网络):
- 多个独立的 Broker 实例通过网络连接起来,可以路由消息。这提供了水平扩展能力,但每个 Broker 依然有自己的持久化存储。
- 消息的可靠性取决于消息是否路由到正确的 Broker 以及该 Broker 的持久化配置。
- 消息丢失的风险在于,如果消息路由到某个 Broker 后该 Broker 崩溃,而消息尚未被消费且未被正确持久化或复制到其他 Broker。
-
-
死信队列 (Dead Letter Queue - DLQ):
- ActiveMQ 会将无法成功投递的消息(例如,消息过期、消费者无法处理、消息回滚次数超过限制)发送到死信队列 (
ActiveMQ.DLQ)。 - 重要性: DLQ 能够防止消息无限期地阻塞队列,并允许管理员或自动化流程检查、修正和重新处理这些“问题”消息,从而避免消息丢失。
- 配置策略: 可以配置消息重试次数、重试延迟策略,以及达到重试上限后将消息发送到 DLQ 的行为。
- ActiveMQ 会将无法成功投递的消息(例如,消息过期、消费者无法处理、消息回滚次数超过限制)发送到死信队列 (
-
消息消费端 (Consumer) 的保障
-
客户端确认模式 (Client Acknowledgment Mode):
-
AUTO_ACKNOWLEDGE(自动确认): 消费者收到消息后自动确认,最简单但有丢失消息的风险(如果消息处理失败但已自动确认,则消息会丢失)。 -
CLIENT_ACKNOWLEDGE(客户端手动确认): 消费者处理完消息后,需要显式调用message.acknowledge()进行确认。如果处理失败未确认,消息会回滚并重新投递。这提供了更高的可靠性,但增加了编码复杂性。 -
DUPS_OK_ACKNOWLEDGE(延迟确认): 允许 JMS Provider 延迟确认,可能导致消息重复,但性能较好。 -
事务性会话 (Transactional Session):
- 消费者可以在事务性会话中消费消息。在一个事务中处理多条消息,只有当事务提交时,所有消息才会被确认。如果事务回滚,所有消息都会回滚并重新投递。
- Spring
JmsListenerContainerFactory可以配置事务管理器。
-
-
持久订阅者 (Durable Subscribers - 针对 Topic):
- 对于 Topic 消息,如果消费者离线时仍需接收消息,必须使用持久订阅。
- 配置: 需要在
JmsListenerContainerFactory中设置唯一的clientId和setSubscriptionDurable(true),并在@JmsListener中指定subscription名称。 - 重要性: 确保即使消费者应用程序宕机,Broker 也会为该订阅者存储消息,待其上线后重新投递。
-
幂等性处理:
- 即使有了所有的保障措施,由于网络抖动、超时等原因,消息仍可能被重复投递。
- 解决方案: 消费者应用程序必须具备消息处理的幂等性,即多次处理同一条消息不会导致副作用(例如,重复创建订单、重复扣款)。这通常通过在业务逻辑中引入唯一 ID(如消息 ID 或业务 ID)并进行去重检查来实现。
-
异常处理和重试机制:
- 在消费者内部,对消息处理逻辑进行健壮的异常处理。
- 使用 Spring JMS 提供的
errorHandler或自定义重试策略(如FixedBackOff、ExponentialBackOff),允许消息在处理失败后进行有限次数的重试,然后再将其发送到 DLQ。
-
系统层面的保障
-
监控和告警:
- 全面监控 ActiveMQ Broker 的状态、队列/Topic 的消息数量、消费者数量、死信队列、内存和磁盘使用情况等。
- 设置关键指标的告警,以便在出现异常时(如队列堆积、连接中断、Broker 崩溃)能够及时响应。
- 使用 Prometheus、Grafana 等工具构建监控仪表盘。
-
日志记录 :
- 详细的日志记录对于问题排查至关重要。记录消息的发送、接收、处理状态以及任何错误或异常。
-
备份和恢复:
- 定期备份 ActiveMQ 的持久化存储(KahaDB 数据文件)。
- 制定详细的灾难恢复计划,以便在极端情况下能够快速恢复 Broker 和数据。
-
总结 最佳实践 :
- 消息发送: 总是使用持久化消息,并考虑在关键业务场景中使用事务性发送。对于异步发送,关注
producerWindowSize和 Broker 确认机制。 - Broker 端: 使用高可靠的持久化存储(如 KahaDB),并配置高可用集群(共享存储主从是常用且易于维护的方案)。有效利用死信队列。
- 消息消费: 对于 Topic,根据业务需求选择持久订阅。在业务逻辑中实现幂等性。使用客户端手动确认或事务性消费,并结合异常处理和重试机制。
- 全链路监控: 部署全面的监控系统,实现告警和日志记录,并定期备份数据。
通过综合运用上述手段,可以最大程度地保障 ActiveMQ 在生产环境中的消息不丢失,从而确保业务的稳定性和数据的一致性。
-
ActiveMQ默认存储的刷盘策略是怎样的?
ActiveMQ 默认推荐的持久化存储是 KahaDB。
KahaDB 的刷盘机制设计旨在平衡性能和可靠性。它通常不是实时(每条消息)强制刷盘,而是采用一种更高效的策略:
-
Append-only Log (追加日志): KahaDB 使用一种“追加日志”的写入方式。所有消息数据和元数据都被顺序地追加到数据日志文件中。这种顺序写入的性能远高于随机写入,因为它避免了磁盘寻道时间。
-
写缓存 (Write Cache) 和批量刷盘 (Batch Flushing):
-
消息数据首先被写入内存中的缓存。
-
KahaDB 会周期性地将缓存中的数据批量地刷入磁盘。这个刷盘动作不是针对每条消息,而是针对一个批次或在特定事件发生时触发。
-
fsync(强制同步): 只有当数据被fsync到磁盘后,才能保证消息的持久性。KahaDB 会在以下情况下执行fsync:- 事务提交时: 如果消息是在事务中发送的,当事务提交时,KahaDB 会确保相关的事务日志和消息数据被刷盘。
- 达到一定写入量或时间间隔: KahaDB 会根据配置,在写入数据量达到一定阈值或经过一定时间间隔后,自动触发刷盘操作。
- Broker 关闭前: 在 Broker 正常关闭时,所有待刷盘的数据都会被强制刷入磁盘。
-
-
恢复日志 (Recovery Log ) / Journaling:
- KahaDB 核心的持久化原理是基于事务日志 (Journaling) 。所有操作(如消息的发送、消费、删除)都会先写入日志文件。
- 这些日志文件是追加写入的,并且一旦写入日志,即使还没有完全刷盘到数据文件,系统也可以通过重放日志来恢复状态。
- 检查点 (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 崩溃,也能确保消息的持久性和可用性。
-
消息生产者发送消息时使用持久化消息,但是对于topic消息并没有使用持久订阅者的方式,那么在消费者程序宕机重启后,才能收到之前的消息吗?
不能。
如果你使用持久化消息发送到 Topic,但消费者程序使用的是非持久订阅者,那么在消费者程序宕机重启后,无法收到在它离线期间发布到该 Topic 的消息。
原因解释:
-
持久化消息 (Persistent Messages on Topic ): 当生产者发送持久化消息到 Topic 时,ActiveMQ Broker 会将这些消息写入其持久化存储(例如 KahaDB)。这确保了即使 Broker 崩溃并 重启 ,这些消息也不会丢失。这些消息会保留在 Topic 上,等待被订阅者消费。
-
非持久订阅者 (Non-Durable Subscribers for Topic):
- 生命周期: 非持久订阅者的生命周期与客户端的 JMS 连接是紧密绑定的。当客户端应用程序启动并成功连接到 Broker 时,非持久订阅者就“上线”了。当客户端的连接断开(例如,应用程序正常关闭、崩溃、网络中断)时,这个非持久订阅者就会立即“下线”并从 Broker 端消失。
- 消息接收范围: 非持久订阅者只接收在其在线期间发布到 Topic 的消息。
结合起来:
- 生产者发送的持久化消息确实被 Broker 存储了。
- 但是,当你的消费者程序宕机时,它的非持久订阅者会立即消失。
- 在消费者宕机期间,即使有新的持久化消息发布到 Topic,由于没有活跃的非持久订阅者在监听,Broker 不知道应该将这些消息投递给谁。
- 当消费者程序重启并重新连接时,它会创建一个全新的非持久订阅者。这个新的订阅者只会接收从它重新上线那一刻开始发布到 Topic 的新消息。它无法“回溯”并接收在它离线期间发布的消息。
总结:
消息的持久性 (Persistent Messages) 保证了Broker不会丢失消息。 订阅的持久性 (Durable Subscribers) 保证了消费者不会错过在它离线期间发布到 Topic 的消息。
对于 Topic 模式,如果你需要保证消费者在离线后能收到离线期间错过的消息,必须使用持久订阅者。
-
Queue (队列) 模式: 队列的消息本身就是持久的(除非显式设置为非持久),并且队列的消息是独占消费的。无论消费者是否在线,消息都会保留在队列中,直到被一个消费者成功消费。所以,对于队列来说,即使消费者宕机,重启后也能收到之前未被消费的消息,不需要“持久订阅者”的概念。
-
Topic (主题) 模式: Topic 消息是发布/订阅模式,每条消息可以有多个消费者。因此,对于 Topic,只有当一个持久订阅者在线时,Broker 才会为它保存离线期间的消息。
-
怎么区分我的代码是使用的Queue的模式哈市Topic的模式
要区分你的 Spring Boot ActiveMQ 消费者代码是使用 Queue (队列) 模式还是 Topic (主题) 模式,你需要查看两个地方:
JmsListenerContainerFactory的配置 (tyyfepJmsListener)@JmsListener注解的destination名称的约定
让我们逐一分析:
- 检查
JmsListenerContainerFactory的配置
你提供了一个名为 tyyfepJmsListener 的 JmsListenerContainerFactory 的配置:
@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 模式。
- 检查
@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 会根据
ConnectionFactory或JmsListenerContainerFactory的setPubSubDomain设置来决定是队列还是主题。
- ActiveMQ 默认情况下,如果目标名称以
-
你的情况:
- 虽然你的
destination变量名是testTopic,暗示了这是一个 Topic,但更重要的是它的实际值。 - 如果
${activemq.cluster.testTopic}的值是my.application.topic(没有queue.或topic.前缀),那么它会遵循tyyfepJmsListener中setPubSubDomain(true)的配置,被视为一个 Topic。 - 如果它的值是
topic.my.application.topic,则会更明确地被识别为 Topic。 - 如果它的值是
queue.my.application.queue,即使setPubSubDomain(true),也可能会因目标名称的显式前缀而产生冲突或特定行为(尽管在 Spring JMS 中setPubSubDomain的优先级更高)。
- 虽然你的
在你的案例中, factory.setPubSubDomain(true) 是决定性的因素。
综合判断:
基于你提供的 JmsConfig 和消费者代码,你的消费者正在使用 Topic (主题) 模式。
如何确定:
JmsListenerContainerFactory(tyyfepJmsListener) 中设置了factory.setPubSubDomain(true)。这是明确指示使用 Topic 模式的配置。@JmsListener的destination变量名testTopic也暗示了这一点。
因此,如果你希望这些 Topic 消息在消费者离线后不丢失,你确实需要将你的消费者配置为持久订阅者,就像我们之前讨论的那样,通过在 tyyfepJmsListener 中设置 clientId 和 setSubscriptionDurable(true),以及在 @JmsListener 中设置 subscription 属性。
-
为什么默认情况下,创建的toptic 没有持久化订阅
核心原因在于 ActiveMQ 对 Topic 订阅者的两种类型:
-
非持久订阅者 (Non-Durable Subscribers):
- 何时生成: 当你的 Spring Boot 应用程序启动,并且
JmsListener容器连接到 ActiveMQ Broker 并开始监听testClusterTopic时,就会创建非持久订阅者。它是在消费者连接到 Broker 并激活 监听器 时实时创建的。 - 生命周期: 非持久订阅者的生命周期与客户端的 JMS 连接紧密绑定。当客户端的连接断开时(例如,应用程序停止、网络中断),这个非持久订阅者就会自动消失。
- 消息接收: 非持久订阅者只接收在其活跃期间(连接在线时)发布到 Topic 的消息。在它们离线期间发布的消息将不会被接收。
- 控制台显示: ActiveMQ Web 控制台的 "Subscribers" 页面通常不会单独列出非持久订阅者。这是因为它们是临时的,并且它们的数量通常包含在 Topic 的 "Number Of Consumers" 统计中。你可能在 Topic 列表中的 "Number Of Consumers" 列看到了数量,这个数量就包含了你的非持久订阅者。
- 何时生成: 当你的 Spring Boot 应用程序启动,并且
-
持久订阅者 (Durable Subscribers):
- 何时生成: 持久订阅者是在客户端第一次连接到 Broker,并使用唯一的
clientId和subscriptionName创建订阅时生成的。一旦创建,即使客户端离线,该订阅也会在 Broker 上持久存在。 - 生命周期: 持久订阅者具有独立的生命周期,不完全依赖于客户端的连接。它们会在 Broker 上保留,直到被明确地取消订阅(例如,通过代码调用
session.unsubscribe())。 - 消息接收: 持久订阅者即使在客户端离线期间也能接收 Topic 上的消息。Broker 会为离线的持久订阅者存储消息,当客户端重新上线时,它会接收到离线期间错过的消息。
- 控制台显示: ActiveMQ Web 控制台的 "Subscribers" 页面主要就是用来展示这些持久订阅者的,因为它们是 Broker 上持久存在的实体。
- 你的
ActiveMqConsumer示例分析: - 你提供的
@JmsListener配置: -
@JmsListener(destination = "${activemq.cluster.testTopic}", containerFactory = "tyyfepJmsListener") public void onMessage(final Message message) { ... }
- 何时生成: 持久订阅者是在客户端第一次连接到 Broker,并使用唯一的
默认情况下,@JmsListener 创建的是非持久订阅者。因为你没有在 tyyfepJmsListener 的 DefaultJmsListenerContainerFactory 中设置 setClientId() 和 setSubscriptionDurable(true),也没有在 @JmsListener 注解中添加 subscription 属性。
因此,你当前的情况是:
你的 Spring Boot 应用程序成功连接到 ActiveMQ Broker,并且已经作为 testClusterTopic 的一个非持久消费者在活跃地监听消息。ActiveMQ 控制台在 Topic 列表中的 "Number Of Consumers" 列会显示你的消费者数量,但不会在 "Subscribers" 页面中单独列出,因为该页面主要用于管理持久订阅。
如果你希望在 ActiveMQ 控制台的 "Subscribers" 页面看到你的订阅者,你需要将其配置为持久订阅者,具体步骤如下:
主要调整点:
-
消息发送持久化 (JmsMessagingTemplate):
- 对于消息发送(生产者),持久化是通过在发送时指定消息的投递模式 (delivery mode) 来实现的。通常,你需要设置
deliveryMode为PERSISTENT。 - 在
JmsMessagingTemplate中,你可以通过设置默认的投递模式来实现。
- 对于消息发送(生产者),持久化是通过在发送时指定消息的投递模式 (delivery mode) 来实现的。通常,你需要设置
-
消息订阅持久化 (DefaultJmsListenerContainerFactory):
- 对于 Topic 订阅者,持久化订阅需要两个关键配置:
clientId和subscriptionDurable。 clientId: 必须为每个客户端应用程序实例设置一个唯一的 ID。setSubscriptionDurable(true): 启用持久订阅功能。setPubSubDomain(true): 确保是发布/订阅(Topic)模式。- 优化后的
JmsConfig配置如下:
- 对于 Topic 订阅者,持久化订阅需要两个关键配置:
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}
使用说明和注意事项:
-
消息发送持久化 (
JmsMessagingTemplate):- 通过
template.setDeliveryMode(DeliveryMode.PERSISTENT);,所有通过tyyJmsTemplate发送的消息,默认都会以持久化的方式发送。这意味着即使 Broker 崩溃并重启,这些消息也不会丢失,除非它们被消费者消费。 - 如果你在特定场景下需要发送非持久消息,你可以在发送时显式地覆盖投递模式,例如:
template.convertAndSend(destination, message, message -> { message.setJMSDeliveryMode(DeliveryMode.NON_PERSISTENT); return message; });
- 通过
-
消息订阅持久化 (
DefaultJmsListenerContainerFactory和@JmsListener):-
factory.setClientId("your-application-unique-client-id");: 这是持久订阅的核心。每个连接到 ActiveMQ Broker 的应用程序实例,如果它想要创建持久订阅,都必须拥有一个全局唯一的clientId。- 在生产环境中,这个
clientId应该能够区分不同的应用程序实例。例如,可以结合应用程序名称、服务器 IP 地址或一个服务实例 ID 来生成。 - 如果两个不同的应用程序实例使用了相同的
clientId,并且都尝试创建持久订阅,只有第一个连接会成功,第二个会失败。
- 在生产环境中,这个
-
factory.setSubscriptionDurable(true);: 这告诉JmsListenerContainerFactory默认创建持久订阅。 -
@JmsListener(destination = "${activemq.cluster.testTopic}", containerFactory = "tyyfepJmsListener", subscription = "myTestClusterTopicSubscription"): 你需要在你的ActiveMqConsumer类中的@JmsListener注解上添加subscription属性。这个subscription名称对于同一个clientId下的同一个 Topic 必须是唯一的。- 这个
subscription名称将会在 ActiveMQ 控制台的 "Subscribers" 页面中显示。
- 这个
-
-
消息转换器 (
MessageConverter):- 添加
MappingJackson2MessageConverter是一个很好的实践。它允许你发送和接收 Java 对象,这些对象会自动序列化为 JSON 字符串,并存储在 JMSTextMessage中。这样可以避免手动进行字符串序列化和反序列化。 - 确保你的生产者和消费者都使用相同的消息转换器配置,以避免兼容性问题。
- 添加
-
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" 页面看到它。