如何在无订阅者时将消息存储到ActiveMQ Artemis多播队列
问题描述
前置背景
刚接触消息队列,正在开发支持私聊和群聊的聊天应用,私聊功能已实现,群聊部分遇到ActiveMQ Artemis多播语义相关问题。发送群消息的代码如下:
public boolean sendGroupMessage(String toGroupId, GroupMessageDTO message) { try { String jsonObj = new ObjectMapper().writer().withDefaultPrettyPrinter().writeValueAsString(message); jmsTemplate.send("group-" + toGroupId, messageCreator -> { TextMessage deliverable = messageCreator.createTextMessage(); deliverable.setText(jsonObj); return deliverable; }); convenienceMethods.addGroupMessageToStore(message); return Boolean.TRUE; } catch (Exception ex) { log.error("Service encountered error trying to send group message: {}", ex.getMessage()); return Boolean.FALSE; } }
传入toGroupId为1234时,Artemis会创建名为group-1234的多播地址,符合预期。
当前问题
多播地址(发布/订阅语义)在无订阅者时,消息不会被存储到任何队列;有STOMP客户端订阅时,消息能正常推送给每个客户端,符合群聊预期,但离线成员无法接收历史消息。
需求期望
- 无订阅者时,发送到多播地址的消息能被保留,当订阅者(群成员)连接时可接收;消息需保留至组内所有成员都接收后才清除。
- 地址需记录所有应订阅的群成员,以实现上述消息留存与推送逻辑。注:应用无JMS监听器配置,所有监听器均为JMS客户端。
现有JMS配置
@Bean public JmsTemplate jmsTemplate() { JmsTemplate template = new JmsTemplate(); template.setConnectionFactory(connectionFactory()); template.setMessageConverter(messageConverter()); template.setPubSubDomain(true); template.setDestinationResolver(destinationResolver()); template.setDeliveryPersistent(true); return template; } @Bean DynamicDestinationResolver destinationResolver() { return new DynamicDestinationResolver() { @Override public Destination resolveDestinationName(Session session, String destinationName, boolean pubSubDomain) throws JMSException { if(destinationName.startsWith("group-")) { pubSubDomain = true; } else { pubSubDomain = false; } return super.resolveDestinationName(session,destinationName,pubSubDomain); } }; }
解决方案
针对需求,需结合ActiveMQ Artemis的持久订阅和地址-队列绑定规则实现,核心是为每个群成员创建专属持久订阅队列,确保离线时消息仍能留存,直到成员接收。
1. 客户端改用持久化订阅
普通发布/订阅模式下无订阅者时消息会直接丢弃,持久订阅会为每个订阅者创建专属持久队列,留存消息直到订阅者确认接收。
STOMP客户端订阅示例
订阅时需指定唯一的客户端ID(对应群成员ID)和持久订阅名称:
SUBSCRIBE destination:/topic/group-1234 id:sub-1 ack:client activemq.subscriptionName:member-5678 activemq.clientID:member-5678 ^@
activemq.clientID:全局唯一标识群成员activemq.subscriptionName:建议与成员ID一致,确保每个成员拥有独立订阅
JMS客户端订阅代码示例
// 连接时设置成员唯一clientID connection.setClientID("member-5678"); // 创建持久订阅 Topic topic = session.createTopic("group-1234"); MessageConsumer consumer = session.createDurableSubscriber(topic, "member-5678");
2. 提前预创建成员队列(可选)
如果群成员固定,可通过Artemis的broker.xml配置地址规则,提前为成员创建绑定队列:
<!-- 配置所有group开头的地址默认规则 --> <address-setting match="group-*"> <default-address-routing-type>MULTICAST</default-address-routing-type> <auto-create-queues>true</auto-create-queues> <auto-delete-queues>false</auto-delete-queues> </address-setting> <!-- 为特定群配置成员队列 --> <address name="group-1234"> <multicast> <queue name="member-5678"/> <queue name="member-9012"/> </multicast> </address>
配置后即使成员未上线,队列已存在,发送到group-1234的消息会路由到所有绑定队列,成员上线后即可消费。
3. 消息留存与清理逻辑
- 持久订阅的消息会留存到成员消费并确认(
ack:client模式下需客户端发送ACK) - 若要实现“所有成员接收后清除”,可在应用层处理:当群内所有成员都确认接收某条消息后,调用Artemis管理API删除对应队列中的该消息;或结合消息过期时间与应用层已读状态管理。
4. 确认JMS模板配置
当前已设置setDeliveryPersistent(true),确保消息持久化存储到磁盘,Broker重启后不会丢失,无需修改。
内容的提问来源于stack exchange,提问作者Bikathi Martin
相关产品推荐
相关产品推荐

