ActiveMq点对点、发布订阅、虚拟主题特性研究

1、研究背景

JMS规范定义了Java中访问消息中间件的接口,但没有给予实现,具体实现交给消息中间件,比如:

ActiveMQ就是一个JMS Provider。一般情况下ActiveMQ支持两种消息传送模型:点对点消息通信模型(queue)和发布订阅模型(topic)。点对点模式下,Queue中的消息只被消费一次,其他的消费者就不能再消费消息了。发布订阅模式下所有的消费者都可以订阅到消息。

(1)如果单个应用单个节点部署的话这两种模式都可以使用。

如果单个应用多节点部署,那我们可以采用queue模式。因为topic模式下每个节点都会订阅消息,造成重复消费问题。

(2)如果多个应用多节点部署,queue模式会造成某些应用无法消费到消息。Topic模式下又会造成每个应用重复消费的问题。

(3)我们希望每个应用只要有一个节点能够消费到消息。就好像在多应用多节点部署的情况下,对于每一个应用来说需要发布订阅模式。而对于每个应用的每个节点来说,又希望具有类似点对点的消费模式,每个应用的每个节点只需要有一个消费到消息即可。这就引入了ActiveMQ的高级特性虚拟主题特性(VirtualTopic)。

本次研究基于springMVC+springJMS+ActiveMQ进行。

2、点对点模式(Queue)

      2.1消息模型

     如下图所示点对点模式(queue)中所有节点只能够消费一次消息。

     2.2使用教程

  1. 本地安装ActiveMQ

      访问到http://localhost:8161/admin/queues.jsp出现下图的页面就安装成功了。

       

(1)xml配置

主要是在spring-activemq.xml中配置连接工厂、消息处理器以及JmsTemplate

  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <!-- 查找最新的schemaLocation 访问 http://www.springframework.org/schema/ -->  
  3. <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context"  
  4.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:amq="http://activemq.apache.org/schema/core"  
  5.        xmlns:jms="http://www.springframework.org/schema/jms"  
  6.        xsi:schemaLocation="http://www.springframework.org/schema/beans  
  7.         http://www.springframework.org/schema/beans/spring-beans-3.2.xsd  
  8.         http://www.springframework.org/schema/context  
  9.         http://www.springframework.org/schema/context/spring-context-3.2.xsd  
  10.         http://www.springframework.org/schema/jms  
  11.         http://www.springframework.org/schema/jms/spring-jms-4.1.xsd  
  12.         http://activemq.apache.org/schema/core  
  13.         http://activemq.apache.org/schema/core/activemq-core-5.8.0.xsd">  
  14.     <amq:connectionFactory id="jmsConnectionFactory" brokerURL="tcp://localhost:61616" userName="admin" password="admin"  
  15.                            useBeanNameAsClientIdPrefix="true"/>  
  16.     <!--链接工厂-->  
  17.     <bean id="jmsConnectionFactoryExtend" class="org.springframework.jms.connection.CachingConnectionFactory">  
  18.         <constructor-arg ref="jmsConnectionFactory"/>  
  19.         <property name="sessionCacheSize" value="100"/>  
  20.     </bean>  
  21.   
  22.     <!-- 消息处理器 -->  
  23.     <bean id="jmsMessageConverter" class="org.springframework.jms.support.converter.SimpleMessageConverter"/>  
  24.   
  25.     <!-- ====Producer side start==== -->  
  26.   
  27.     <!-- 定义JmsTemplate的Queue类型 -->  
  28.  <bean id="jmsQueueTemplate" class="org.springframework.jms.core.JmsTemplate">  
  29.         <constructor-arg ref="jmsConnectionFactoryExtend"/>  
  30.         <!-- 非pub/sub模型(发布/订阅),即队列模式 -->  
  31.         <property name="pubSubDomain" value="false"/>  
  32.         <property name="messageConverter" ref="jmsMessageConverter"></property>  
  33.     </bean>  
  34.   
  35.     <!-- 定义JmsTemplate的Topic类型 -->  
  36.     <bean id="jmsTopicTemplate" class="org.springframework.jms.core.JmsTemplate">  
  37.         <constructor-arg ref="jmsConnectionFactoryExtend"/>  
  38.         <!-- pub/sub模型(发布/订阅) -->  
  39.         <property name="pubSubDomain" value="true"/>  
  40.         <property name="messageConverter" ref="jmsMessageConverter"></property>  
  41.     </bean>  
  42.   
  43.   
  44.     <!--    下面的配置仅限topic模式使用的监听器配置,虚拟主题无需在此配置bean-->  
  45.     <!--    应用1-->  
  46.     <jms:listener-container destination-type="topic" container-type="default"  
  47.                             connection-factory="jmsConnectionFactoryExtend" acknowledge="auto">  
  48.         <jms:listener destination="restuarant_topic" ref="server1TopicListener1"/>  
  49.     </jms:listener-container>  
  50.     <jms:listener-container destination-type="topic" container-type="default"  
  51.                             connection-factory="jmsConnectionFactoryExtend" acknowledge="auto">  
  52.         <jms:listener destination="restuarant_topic" ref="server1TopicListener2"/>  
  53.     </jms:listener-container>  
  54.     <!--    应用2-->  
  55.     <jms:listener-container destination-type="topic" container-type="default"  
  56.   connection-factory="jmsConnectionFactoryExtend" acknowledge="auto">  
  57.   <jms:listener destination="restuarant_topic" ref="server2TopicListener1"/>  
  58.     </jms:listener-container>  
  59.     <jms:listener-container destination-type="topic" container-type="default"  connection-factory="jmsConnectionFactoryExtend" acknowledge="auto">  
  60.  <jms:listener destination="restuarant_topic" ref="server2TopicListener2"/>  
  61.     </jms:listener-container>  
  62.     <!--    应用3-->  
  63.     <jms:listener-container destination-type="topic" container-type="default"  
  64.   connection-factory="jmsConnectionFactoryExtend" acknowledge="auto">  
  65.   <jms:listener destination="restuarant_topic" ref="server3TopicListener1"/>  
  66.     </jms:listener-container>  
  67.     <jms:listener-container destination-type="topic" container-type="default"  
  68. connection-factory="jmsConnectionFactoryExtend" acknowledge="auto">  
  69.    <jms:listener destination="restuarant_topic" ref="server3TopicListener2"/>
  70.     </jms:listener-container>  
  71.   
  72.   
  73.     <bean id="jmsListenerContainerFactory" class="org.springframework.jms.config.DefaultJmsListenerContainerFactory">  
  74.         <property name="connectionFactory" ref="jmsConnectionFactoryExtend"/>  
  75.     </bean>  
  76.   
  77.     <!-- 监听注解支持 -->  
  78.     <jms:annotation-driven/>  
  79. </beans>  

 

 

使用queue只需要注意配置jmsListenerContainerFactory、监听注解支持、JmsTemplate的Queue类型即可。

(3)发送消息

  1. @GetMapping("queue")  
  2.   @ResponseBody  
  3.   public void sendToQueue(FoodRequestEntity food) {  
  4.   
  5.       queueProducer.createMq("restuarant_queue""查询食物清单,鱼肉!");  
  6.   } 

 

  1. @Component  
  2. public class QueueProducer {  
  3.   
  4.     @Autowired  
  5.     QueueSenderUtils utils;  
  6.   
  7.     public void createMq(String quenName, String text) {  
  8.         Destination destination = new ActiveMQQueue(quenName);  
  9.         String JMSMessageId = null;  
  10.         for (int i = 0; i < 10; i++) {  
  11.             JMSMessageId = utils.send(destination, "A:"+text, "GroupIdA");  
  12.             JMSMessageId = utils.send(destination, "B:"+text, "GroupIdB");  
  13.             //延时6秒后再次发送  
  14.             //utils.sendDelayMessage(destination, text, 6*1000,6*1000,3);  
  15.         }  
  16.     }  
  17.   
  18. }

(4)消息接收

  1. @Component  
  2. public class QueueListener  {  
  3.     private  Logger logger = Logger.getLogger(QueueListener.class);  
  4.   
  5.     @JmsListener(destination="restuarant_queue",selector="JMSGroupID='GroupIdB'")  
  6.     public void recieveTaskMq(Message message, Session session) {  
  7.         logger.info("消费者queue1监听到的消息是:" + message + ",并且这条消息已经被消费了");  
  8.     }  
  9.   
  10.     @JmsListener(destination="restuarant_queue",selector="JMSGroupID='GroupIdA'" )  
  11.     public void recieveTaskMq2(Message message, Session session) {  
  12.         logger.info("消费者queue2监听到的消息是:" + message + ",并且这条消息已经被消费了");  
  13.     }  
  14.   
  15.   
  16. }

     

2.3消息分组特性

        上面的例子就用到了queue模式的分组消息特性,如果只有一个应用多节点部署的情况下,可以采用queue的选择器selector来做分组。

        这种模式下每个节点的消费之可以消费固定的生产者生产的消息,但是也有个缺点,如果某个节点挂了,那么其对应的生产者消息也将无法消费。

         Message Group是针对queue,对topic无感!如果在queue模式下,一个生产者对应多个消费者,每生产一条消息,会被消费随即 抢到,如果我们不希望这样,只希望固定的消息被固定的消费者消费,那么就采用group对消息进行一个类似标记的作用。分组要依赖消息选择器,selector。

     2.4运行效果

发送20条消息:

处理了20条消息:

3、发布订阅模式(Topic)

3.1消息模型

如下图所示为发布订阅模式下的消息消费模型,对于每一个应用的每一个节点都可以接收到消息。

3.2 使用教程

生产者使用jmsTopicTemplate发送消息,消费者无法使用像queue模式那样的注解监听消息(根据目前的研究,无法使用注解)。只能在xml中配置监听器。

(1)发送消息

  1.   @GetMapping("topic")  
  2.   @ResponseBody  
  3.   public void sendToTopic() {  
  4.       groupProducer.createMq("restuarant_topic""查询食物清单,蔬菜!");  
  5.   }  

 

  1. @Component  
  2. public class TopProducer {  
  3.   
  4.     @Autowired  
  5.     TopicActiveMqUtils utils;  
  6.   
  7.     public void createMq(String quenName, String text) {  
  8.         Destination destination = new ActiveMQTopic(quenName);  
  9.         String JMSMessageId = null;  
  10.         for (int i = 0; i < 1; i++) {  
  11.             JMSMessageId = utils.sendNorMolMessageById(destination, text, "GroupIdA");  
  12. //            JMSMessageId = utils.sendNorMolMessageById(destination, text, "GroupIdB");  
  13.             //延时6秒后再次发送  
  14.             //utils.sendDelayMessage(destination, text, 6*1000,6*1000,3);  
  15.         }  
  16.     }  
  17.   
  18. }

 

 

  1. @Component  
  2. public class TopicActiveMqUtils {  
  3.     /** 
  4.      * 注入JMS 
  5.      */  
  6.     @Resource(name = "jmsTopicTemplate")  
  7.     private JmsTemplate jmsTemplate;  
  8.   
  9.   
  10.     public <T> String sendNorMolMessageById(Destination destination, String text, String GroupId) {  
  11.         // 连接工厂  
  12.         ConnectionFactory connectionFactory = jmsTemplate.getConnectionFactory();  
  13.         Connection connection = null;  
  14.         Session session = null;  
  15.         MessageProducer producer = null;  
  16.         try {  
  17.             // 创建链接  
  18.             connection = connectionFactory.createConnection();  
  19.             connection.start();  
  20.             // 创建session,开启事物  
  21.             session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);  
  22.             // 创建生产者  
  23.             producer = session.createProducer(destination);  
  24.             // 设置持久化  
  25.             producer.setDeliveryMode(DeliveryMode.PERSISTENT);  
  26.             // 设置过期时间  
  27.             //producer.setTimeToLive(time);  
  28.             TextMessage message = session.createTextMessage(text);  
  29.             message.setStringProperty("JMSGroupID", GroupId);  
  30.             producer.send(message);  
  31.             // 提交  
  32.             session.commit();  
  33.             return message.getJMSMessageID();  
  34.         } catch (JMSException e) {  
  35.             throw new RuntimeException(e);  
  36.         } finally {  
  37.             // 关闭连接  
  38.             close(producer, session, connection, connectionFactory);  
  39.         }  
  40.   
  41.     }  
  42.   
  43.     private void close(MessageProducer producer, Session session, Connection connection, ConnectionFactory connectionFactory) {  
  44.         if (null != connection) {  
  45.             try {  
  46.                 connection.close();  
  47.               } catch (JMSException e) {  
  48.                 e.printStackTrace();  
  49.             }  
  50.         }  
  51.     }  
  52.   

 

 

(2)接收消息

  1. @Component  
  2. public class Server1TopicListener1 implements MessageListener {  
  3.     private Logger logger = Logger.getLogger(Server1TopicListener1.class);  
  4.     @Override  
  5.     public void onMessage(Message message) {  
  6.         logger.info("应用1节点1监听到的topic为:" + message + ",并且这条消息已经被消费了");  
  7.     }  

 

 

  1. @Component  
  2. public class Server1TopicListener2  implements MessageListener {  
  3.     private Logger logger = Logger.getLogger(Server1TopicListener2.class);  
  4.     @Override  
  5.     public void onMessage(Message message) {  
  6.         logger.info("应用1节点2监听到的topic为:" + message + ",并且这条消息已经被消费了");  
  7.     }  

3.3  运行效果

发送一个主题,六个节点都在消费消息。

4、虚拟主题模式(VirtualTopic)

4.1消息模型

虚拟主题模式是topic和queue模式下的结合可以修改ActiveMq本身的配置文件实现。也可以通过发送和接收主题的名称配置来实现。第一种对消息中间件的侵入性太强,升级需要重启MQ会导致消息丢失的问题。推荐使用第二种方式。在这种模式下发送消息使用的是topic的方式发送。接收消息可以使用queue模式下的注解方式接收消息。发送的目标主题名称必须加前缀“VirtualTopic.”例如我们有一个目标主题:restuarant_vrtopic,发送目标则为:“VirtualTopic.restuarant_vrtopic”。接收使用队列方式,接收目标统一加前缀“Consumer.*.”。其中*代表消费者的唯一标识。下面将详细说明:

 

4.2 消息发送

  1. @GetMapping("vrtopic")  
  2. @ResponseBody  
  3. public void sendToVrTopic() {  
  4.     groupProducer.createMq("VirtualTopic.restuarant_vrtopic""查询食物清单,蔬菜!");  
  5.  

     和topic的区别就是名称的不同。

4.3 接收消息

  1. //该监听器用来测试虚拟主题,解决多应用多节点部署时的重复消费问题。  
  2. @Component  
  3. public class VirtualTopicListener {  
  4.     private Logger logger = Logger.getLogger(VirtualTopicListener.class);  
  5.   
  6. //    应用1  
  7.     @JmsListener(destination="Consumer.A.VirtualTopic.restuarant_vrtopic")  
  8.     public void recieveTaskMq1(Message message, Session session) {  
  9.         logger.info("消费者应用A1监听到的消息是:" + message + ",并且这条消息已经被消费了");  
  10.     }  
  11.   
  12.     @JmsListener(destination="Consumer.A.VirtualTopic.restuarant_vrtopic" )  
  13.     public void recieveTaskMq2(Message message, Session session) {  
  14.         logger.info("消费者应用A2监听到的消息是:" + message + ",并且这条消息已经被消费了");  
  15.     }  
  16.   
  17.     //    应用2  
  18.     @JmsListener(destination="Consumer.B.VirtualTopic.restuarant_vrtopic")  
  19.     public void recieveTaskMq3(Message message, Session session) {  
  20.         logger.info("消费者B1监听到的消息是:" + message + ",并且这条消息已经被消费了");  
  21.     }  
  22.   
  23.     @JmsListener(destination="Consumer.B.VirtualTopic.restuarant_vrtopic" )  
  24.     public void recieveTaskMq4(Message message, Session session) {  
  25.         logger.info("消费者B2监听到的消息是:" + message + ",并且这条消息已经被消费了");  
  26.     }  
  27.   
  28.     //    应用3  
  29.     @JmsListener(destination="Consumer.C.VirtualTopic.restuarant_vrtopic")  
  30.     public void recieveTaskMq5(Message message, Session session) {  
  31.         logger.info("消费者C1监听到的消息是:" + message + ",并且这条消息已经被消费了");  
  32.     }  
  33.   
  34.     @JmsListener(destination="Consumer.C.VirtualTopic.restuarant_vrtopic")  
  35.     public void recieveTaskMq6(Message message, Session session) {  
  36.         logger.info("消费者C2监听到的消息是:" + message + ",并且这条消息已经被消费了");  
  37.     }  
  38. }

4.4 运行效果

发送了一条虚拟主题,每个应用只有一个节点接收消息。

源代码链接请参考:https://gitee.com/renchunlin66/ActiveMq_virtual_topic

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值