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使用教程
- 本地安装ActiveMQ
访问到http://localhost:8161/admin/queues.jsp出现下图的页面就安装成功了。
(1)xml配置
主要是在spring-activemq.xml中配置连接工厂、消息处理器以及JmsTemplate等:
- <?xml version="1.0" encoding="UTF-8"?>
- <!-- 查找最新的schemaLocation 访问 http://www.springframework.org/schema/ -->
- <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:amq="http://activemq.apache.org/schema/core"
- xmlns:jms="http://www.springframework.org/schema/jms"
- xsi:schemaLocation="http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
- http://www.springframework.org/schema/context
- http://www.springframework.org/schema/context/spring-context-3.2.xsd
- http://www.springframework.org/schema/jms
- http://www.springframework.org/schema/jms/spring-jms-4.1.xsd
- http://activemq.apache.org/schema/core
- http://activemq.apache.org/schema/core/activemq-core-5.8.0.xsd">
- <amq:connectionFactory id="jmsConnectionFactory" brokerURL="tcp://localhost:61616" userName="admin" password="admin"
- useBeanNameAsClientIdPrefix="true"/>
- <!--链接工厂-->
- <bean id="jmsConnectionFactoryExtend" class="org.springframework.jms.connection.CachingConnectionFactory">
- <constructor-arg ref="jmsConnectionFactory"/>
- <property name="sessionCacheSize" value="100"/>
- </bean>
- <!-- 消息处理器 -->
- <bean id="jmsMessageConverter" class="org.springframework.jms.support.converter.SimpleMessageConverter"/>
- <!-- ====Producer side start==== -->
- <!-- 定义JmsTemplate的Queue类型 -->
- <bean id="jmsQueueTemplate" class="org.springframework.jms.core.JmsTemplate">
- <constructor-arg ref="jmsConnectionFactoryExtend"/>
- <!-- 非pub/sub模型(发布/订阅),即队列模式 -->
- <property name="pubSubDomain" value="false"/>
- <property name="messageConverter" ref="jmsMessageConverter"></property>
- </bean>
- <!-- 定义JmsTemplate的Topic类型 -->
- <bean id="jmsTopicTemplate" class="org.springframework.jms.core.JmsTemplate">
- <constructor-arg ref="jmsConnectionFactoryExtend"/>
- <!-- pub/sub模型(发布/订阅) -->
- <property name="pubSubDomain" value="true"/>
- <property name="messageConverter" ref="jmsMessageConverter"></property>
- </bean>
- <!-- 下面的配置仅限topic模式使用的监听器配置,虚拟主题无需在此配置bean-->
- <!-- 应用1-->
- <jms:listener-container destination-type="topic" container-type="default"
- connection-factory="jmsConnectionFactoryExtend" acknowledge="auto">
- <jms:listener destination="restuarant_topic" ref="server1TopicListener1"/>
- </jms:listener-container>
- <jms:listener-container destination-type="topic" container-type="default"
- connection-factory="jmsConnectionFactoryExtend" acknowledge="auto">
- <jms:listener destination="restuarant_topic" ref="server1TopicListener2"/>
- </jms:listener-container>
- <!-- 应用2-->
- <jms:listener-container destination-type="topic" container-type="default"
- connection-factory="jmsConnectionFactoryExtend" acknowledge="auto">
- <jms:listener destination="restuarant_topic" ref="server2TopicListener1"/>
- </jms:listener-container>
- <jms:listener-container destination-type="topic" container-type="default" connection-factory="jmsConnectionFactoryExtend" acknowledge="auto">
- <jms:listener destination="restuarant_topic" ref="server2TopicListener2"/>
- </jms:listener-container>
- <!-- 应用3-->
- <jms:listener-container destination-type="topic" container-type="default"
- connection-factory="jmsConnectionFactoryExtend" acknowledge="auto">
- <jms:listener destination="restuarant_topic" ref="server3TopicListener1"/>
- </jms:listener-container>
- <jms:listener-container destination-type="topic" container-type="default"
- connection-factory="jmsConnectionFactoryExtend" acknowledge="auto">
- <jms:listener destination="restuarant_topic" ref="server3TopicListener2"/>
- </jms:listener-container>
- <bean id="jmsListenerContainerFactory" class="org.springframework.jms.config.DefaultJmsListenerContainerFactory">
- <property name="connectionFactory" ref="jmsConnectionFactoryExtend"/>
- </bean>
- <!-- 监听注解支持 -->
- <jms:annotation-driven/>
- </beans>
使用queue只需要注意配置jmsListenerContainerFactory、监听注解支持、JmsTemplate的Queue类型即可。
(3)发送消息
- @GetMapping("queue")
- @ResponseBody
- public void sendToQueue(FoodRequestEntity food) {
- queueProducer.createMq("restuarant_queue", "查询食物清单,鱼肉!");
- }
- @Component
- public class QueueProducer {
- @Autowired
- QueueSenderUtils utils;
- public void createMq(String quenName, String text) {
- Destination destination = new ActiveMQQueue(quenName);
- String JMSMessageId = null;
- for (int i = 0; i < 10; i++) {
- JMSMessageId = utils.send(destination, "A:"+text, "GroupIdA");
- JMSMessageId = utils.send(destination, "B:"+text, "GroupIdB");
- //延时6秒后再次发送
- //utils.sendDelayMessage(destination, text, 6*1000,6*1000,3);
- }
- }
- }
(4)消息接收
- @Component
- public class QueueListener {
- private Logger logger = Logger.getLogger(QueueListener.class);
- @JmsListener(destination="restuarant_queue",selector="JMSGroupID='GroupIdB'")
- public void recieveTaskMq(Message message, Session session) {
- logger.info("消费者queue1监听到的消息是:" + message + ",并且这条消息已经被消费了");
- }
- @JmsListener(destination="restuarant_queue",selector="JMSGroupID='GroupIdA'" )
- public void recieveTaskMq2(Message message, Session session) {
- logger.info("消费者queue2监听到的消息是:" + message + ",并且这条消息已经被消费了");
- }
- }
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)发送消息
- @GetMapping("topic")
- @ResponseBody
- public void sendToTopic() {
- groupProducer.createMq("restuarant_topic", "查询食物清单,蔬菜!");
- }
- @Component
- public class TopProducer {
- @Autowired
- TopicActiveMqUtils utils;
- public void createMq(String quenName, String text) {
- Destination destination = new ActiveMQTopic(quenName);
- String JMSMessageId = null;
- for (int i = 0; i < 1; i++) {
- JMSMessageId = utils.sendNorMolMessageById(destination, text, "GroupIdA");
- // JMSMessageId = utils.sendNorMolMessageById(destination, text, "GroupIdB");
- //延时6秒后再次发送
- //utils.sendDelayMessage(destination, text, 6*1000,6*1000,3);
- }
- }
- }
- @Component
- public class TopicActiveMqUtils {
- /**
- * 注入JMS
- */
- @Resource(name = "jmsTopicTemplate")
- private JmsTemplate jmsTemplate;
- public <T> String sendNorMolMessageById(Destination destination, String text, String GroupId) {
- // 连接工厂
- ConnectionFactory connectionFactory = jmsTemplate.getConnectionFactory();
- Connection connection = null;
- Session session = null;
- MessageProducer producer = null;
- try {
- // 创建链接
- connection = connectionFactory.createConnection();
- connection.start();
- // 创建session,开启事物
- session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);
- // 创建生产者
- producer = session.createProducer(destination);
- // 设置持久化
- producer.setDeliveryMode(DeliveryMode.PERSISTENT);
- // 设置过期时间
- //producer.setTimeToLive(time);
- TextMessage message = session.createTextMessage(text);
- message.setStringProperty("JMSGroupID", GroupId);
- producer.send(message);
- // 提交
- session.commit();
- return message.getJMSMessageID();
- } catch (JMSException e) {
- throw new RuntimeException(e);
- } finally {
- // 关闭连接
- close(producer, session, connection, connectionFactory);
- }
- }
- private void close(MessageProducer producer, Session session, Connection connection, ConnectionFactory connectionFactory) {
- if (null != connection) {
- try {
- connection.close();
- } catch (JMSException e) {
- e.printStackTrace();
- }
- }
- }
- }
(2)接收消息
- @Component
- public class Server1TopicListener1 implements MessageListener {
- private Logger logger = Logger.getLogger(Server1TopicListener1.class);
- @Override
- public void onMessage(Message message) {
- logger.info("应用1节点1监听到的topic为:" + message + ",并且这条消息已经被消费了");
- }
- }
- @Component
- public class Server1TopicListener2 implements MessageListener {
- private Logger logger = Logger.getLogger(Server1TopicListener2.class);
- @Override
- public void onMessage(Message message) {
- logger.info("应用1节点2监听到的topic为:" + message + ",并且这条消息已经被消费了");
- }
- }
3.3 运行效果
发送一个主题,六个节点都在消费消息。
4、虚拟主题模式(VirtualTopic)
4.1消息模型
虚拟主题模式是topic和queue模式下的结合可以修改ActiveMq本身的配置文件实现。也可以通过发送和接收主题的名称配置来实现。第一种对消息中间件的侵入性太强,升级需要重启MQ会导致消息丢失的问题。推荐使用第二种方式。在这种模式下发送消息使用的是topic的方式发送。接收消息可以使用queue模式下的注解方式接收消息。发送的目标主题名称必须加前缀“VirtualTopic.”例如我们有一个目标主题:restuarant_vrtopic,发送目标则为:“VirtualTopic.restuarant_vrtopic”。接收使用队列方式,接收目标统一加前缀“Consumer.*.”。其中*代表消费者的唯一标识。下面将详细说明:
4.2 消息发送
- @GetMapping("vrtopic")
- @ResponseBody
- public void sendToVrTopic() {
- groupProducer.createMq("VirtualTopic.restuarant_vrtopic", "查询食物清单,蔬菜!");
- }
和topic的区别就是名称的不同。
4.3 接收消息
- //该监听器用来测试虚拟主题,解决多应用多节点部署时的重复消费问题。
- @Component
- public class VirtualTopicListener {
- private Logger logger = Logger.getLogger(VirtualTopicListener.class);
- // 应用1
- @JmsListener(destination="Consumer.A.VirtualTopic.restuarant_vrtopic")
- public void recieveTaskMq1(Message message, Session session) {
- logger.info("消费者应用A1监听到的消息是:" + message + ",并且这条消息已经被消费了");
- }
- @JmsListener(destination="Consumer.A.VirtualTopic.restuarant_vrtopic" )
- public void recieveTaskMq2(Message message, Session session) {
- logger.info("消费者应用A2监听到的消息是:" + message + ",并且这条消息已经被消费了");
- }
- // 应用2
- @JmsListener(destination="Consumer.B.VirtualTopic.restuarant_vrtopic")
- public void recieveTaskMq3(Message message, Session session) {
- logger.info("消费者B1监听到的消息是:" + message + ",并且这条消息已经被消费了");
- }
- @JmsListener(destination="Consumer.B.VirtualTopic.restuarant_vrtopic" )
- public void recieveTaskMq4(Message message, Session session) {
- logger.info("消费者B2监听到的消息是:" + message + ",并且这条消息已经被消费了");
- }
- // 应用3
- @JmsListener(destination="Consumer.C.VirtualTopic.restuarant_vrtopic")
- public void recieveTaskMq5(Message message, Session session) {
- logger.info("消费者C1监听到的消息是:" + message + ",并且这条消息已经被消费了");
- }
- @JmsListener(destination="Consumer.C.VirtualTopic.restuarant_vrtopic")
- public void recieveTaskMq6(Message message, Session session) {
- logger.info("消费者C2监听到的消息是:" + message + ",并且这条消息已经被消费了");
- }
- }
4.4 运行效果
发送了一条虚拟主题,每个应用只有一个节点接收消息。
源代码链接请参考:https://gitee.com/renchunlin66/ActiveMq_virtual_topic