You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在无订阅者时将消息存储到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客户端订阅时,消息能正常推送给每个客户端,符合群聊预期,但离线成员无法接收历史消息。

需求期望

  1. 无订阅者时,发送到多播地址的消息能被保留,当订阅者(群成员)连接时可接收;消息需保留至组内所有成员都接收后才清除。
  2. 地址需记录所有应订阅的群成员,以实现上述消息留存与推送逻辑。注:应用无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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 18:48:18