springboot2 (4) 消息队列

321 阅读6分钟

1. 概念入门

概念: 消息队列 Message Queue 简称MQ,即存放消息的FIFO队列,主要用于对进程或线程间通信进行解耦,对高并发任务进行削峰,提高程序性能:

  • 消息队列使用原则:尽可能提高消息入队速度,灵活调整消息出队速度。
  • 消息队列技术特点:
    • 消息不丢失:MQ采取put-get-delete模式,仅在消息被完整处理后才会将其删除。
    • 进程无关联:MQ下游进程崩溃,上游进程仍可继续put,等待下游恢复。
    • 处理不重复:MQ中的一个消息仅被处理一次,被某个下游进程获取时会锁定。
    • 处理可延时:MQ中的消息可以被延时处理,更加灵活。

2. ActiveMQ

流程: activemq官网 是apache公司的一个消息队列产品:

  • 启动 %ACTIVEMQ_HOME%\bin\win64\activemq.bat
  • 访问 127.0.0.1:8161 进入AMQ管理界面,账密都是 admin
  • 点击 Manage ActiveMQ broker 管理AMQ的经纪人(管控台)。
  • 创建队列:点击 queues 选项卡,输入以 queue 后缀的队列名,点击 create
    • Name:队列名称
    • Number Of Pending Messages:待消费消息数。
    • Number Of Consumers:消费者数。
    • Messages Enqueued:总消息数,包括待消费和已消费的,只增不减。
    • Messages Dequeued:已消费消息数。
  • 向队列发送消息:点击 send 选项卡,输入目标队列名和消息内容,点击 send
    • destination:目标队列名。
    • message body:消息内容。

2.1 springboot整合

流程: 新建springboot-jar项目 springboot2-activemq

  • 配置pom依赖:spring-boot-starter-activemq/pooled-jms

  • 主配开启AMQ:均以 spring.activemq 前缀:

    • broker-url=tcp://localhost:61616:连接AMQ经纪人,默认端口61616。
    • broker-url=failover:(tcp://localhost:61616,tcp://localhost:61617):连接AMQ经纪人集群。
    • user=admin:TCP账号为admin。
    • password=admin:TCP连接密码为admin。
    • pool.enabled=true:开启AMQ池。
    • pool.max-connections=50:AMQ池最大容量为50个连接。
  • 如果单独使用订阅者需配置spring.jms.pub-sub-domain=true

    • 如果消费者和订阅者整合 这个注解需删除。
  • 启动类上添加 @EnableJms 支持JMS。 源码: /springboot/

  • res:pom.xml

        <!--spring-boot-starter-activemq-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-activemq</artifactId>
        </dependency>

        <!--pooled-jms-->
        <dependency>
            <groupId>org.messaginghub</groupId>
            <artifactId>pooled-jms</artifactId>
            <version>1.0.4</version>
        </dependency>
  • res:application.properties
# 整合activeMQ
spring.activemq.broker-url=tcp://localhost:61616
spring.activemq.user=admin
spring.activemq.password=admin
spring.activemq.pool.enabled=true
spring.activemq.pool.max-connections=50
#spring.jms.pub-sub-domain=true

2.2 生产消费模型

流程: 生产者生产消息,所有消费者轮流消费该消息:

  • 开发生产者类 c.y.s.producer.ProducerA:注入 o.s.y.c.JmsMessagingTemplate 类:
    • new ActiveMQQueue(queueName):根据队列名创建 Destination 对象,名不存在自动创建。
    • jmsMessagingTemplate.convertAndSend(destination, msg):向目标队列发送消息。
  • 开发动作类 c.y.s.controller.ProducerController:注入生产者类并调用其发送消息方法。
  • 开发消费者类 c.y.s.consumer.ConsumerA/ConsumerB
    • 消费方法标记 @JmsListener:当 destination 指定的队列有消息时执行。
    • 消费方法对形参 ActiveMQTextMessage 调用 getText() 可接收字符串消息。
  • psm测试:producer/send-to-queue 源码: /springboot/
  • src:c.y.s.producer.ProducerA
package com.yap.z11springboot2.activeMQ.subscriber.producer;

import org.apache.activemq.command.ActiveMQQueue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.stereotype.Component;

import javax.jms.Destination;

/**
 * @author yap
 */
@Component
public class ProducerA {

    private JmsMessagingTemplate jmsMessagingTemplate;

    @Autowired
    public ProducerA(JmsMessagingTemplate jmsMessagingTemplate) {
        this.jmsMessagingTemplate = jmsMessagingTemplate;
    }

    public void sendToQueue(String queueName, final Object msg) {
        Destination dest = new ActiveMQQueue(queueName);
        jmsMessagingTemplate.convertAndSend(dest, msg);
    }
}
  • src:c.y.s.controller.ProducerController
package com.yap.z11springboot2.activeMQ.subscriber.controller;


import com.yap.z11springboot2.activeMQ.subscriber.producer.ProducerA;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * @author yap
 */
@RestController
@RequestMapping("api/producer")
public class ProducerController {

    private ProducerA producerA;

    @Autowired
    public ProducerController(ProducerA producerA) {
        this.producerA = producerA;
    }

    @RequestMapping("send-to-queue")
    public Object sendToQueue(String msg) throws InterruptedException {
        for (int i = 0, j = 10; i < j; i++) {
            TimeUnit.SECONDS.sleep(1L);
            producerA.sendToQueue("start.queue", msg + "-" + i);
        }
        return "sendToQueue() success";
    }
}
  • src:c.y.s.consumer.ConsumerA
package com.yap.z11springboot2.activeMQ.subscriber.consumer;

import org.apache.activemq.command.ActiveMQTextMessage;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

import javax.jms.JMSException;

/**
 * @author yap
 */
@Component
public class ConsumerA {

    @JmsListener(destination = "start.queue",containerFactory = "jmsListenerContainerQueue")
    public void spendFromQueue(ActiveMQTextMessage msg) {
        try {
            System.out.println("consumerA spend: " + msg.getText());
        } catch (JMSException e) {
            e.printStackTrace();
        }
    }
}
  • src:c.y.s.consumer.ConsumerB
package com.yap.z11springboot2.activeMQ.subscriber.consumer;

import org.apache.activemq.command.ActiveMQTextMessage;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

import javax.jms.JMSException;

/**
 * @author yap
 */
@Component
public class ConsumerA {

    @JmsListener(destination = "start.queue",containerFactory = "jmsListenerContainerQueue")
    public void spendFromQueue(ActiveMQTextMessage msg) {
        try {
            System.out.println("consumerA spend: " + msg.getText());
        } catch (JMSException e) {
            e.printStackTrace();
        }
    }
}

2.3 发布订阅模型

流程: 发布者发布消息,所有订阅者同时消费该消息:

  • 开发监听配置类 c.y.s.config.JmsListenerConfig 并IOC o.s.y.c.JmsListenerContainerFactory
    • <bean> 的id固定为 jmsListenerContainerTopic
    • <bean> 的class建议使用 o.s.y.c.DefaultJmsListenerContainerFactory 类。
    • bean.setConnectionFactory(connectionFactory):设置用于获取JMS的工厂,传入方法入参即可。
    • bean.setPubSubDomain(true):设置支持发布订阅模型。
  • 开发发布者类 c.y.s.publish.PublishA:注入 o.s.y.c.JmsMessagingTemplate 类:
    • new ActiveMQTopic(topicName):根据主题名创建 Destination 对象,名不存在自动创建。
    • jmsMessagingTemplate.convertAndSend(destination, msg):向目标主题发送消息。
  • 开发动作类 c.y.s.controller.PublishController:注入发布者类并调用其发送消息方法。
  • 开发订阅者类 c.y.s.subscriber.SubscriberA/SubscriberB
    • 消费方法标记 @JmsListener:当 destination 指定的队列有消息时执行。
    • @JmsListener 添加 containerFactory="jmsListenerContainerTopic" 以支持发布订阅模型。
    • 消费方法对形参 ActiveMQTextMessage 调用 getText() 可接收字符串消息。
  • psm测试:publish/send-to-topic 源码: /springboot/
  • src:c.y.s.config.JmsListenerConfig
package com.yap.z11springboot2.activeMQ.subscriber.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.stereotype.Component;

import javax.jms.ConnectionFactory;

/**
 * @author yap
 */
@Configuration
@Component
public class JmsListenerConfig {

    @Bean
    public JmsListenerContainerFactory<?> jmsListenerContainerFactory(ConnectionFactory connectionFactory) {
        DefaultJmsListenerContainerFactory bean = new DefaultJmsListenerContainerFactory();
        bean.setConnectionFactory(connectionFactory);
        bean.setPubSubDomain(true);
        return bean;
    }


    //需要给queue定义独立的jmsListenerContainerQueue
    @Bean
    public JmsListenerContainerFactory<?> jmsListenerContainerQueue(ConnectionFactory connectionFactory) {
        DefaultJmsListenerContainerFactory bean = new DefaultJmsListenerContainerFactory();
        bean.setConnectionFactory(connectionFactory);
        return bean;
    }

}

  • src:c.y.s.publish.PublishA
package com.yap.z11springboot2.activeMQ.subscriber.publish;

import org.apache.activemq.command.ActiveMQTopic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.stereotype.Component;

import javax.jms.Destination;

/**
 * @author yap
 */
@Component
public class PublishA {

    private JmsMessagingTemplate jmsMessagingTemplate;

    @Autowired
    public PublishA(JmsMessagingTemplate jmsMessagingTemplate) {
        this.jmsMessagingTemplate = jmsMessagingTemplate;
    }

    public void sendToTopic(String topicName, final Object msg) {
        Destination dest = new ActiveMQTopic(topicName);
        jmsMessagingTemplate.convertAndSend(dest, msg);
    }
}
  • src:c.y.s.controller.PublishController
package com.yap.z11springboot2.activeMQ.subscriber.controller;

import com.yap.z11springboot2.activeMQ.subscriber.publish.PublishA;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * @author yap
 */
@RestController
@RequestMapping("api/publish")
public class PublishController {

    private PublishA publishA;

    @Autowired
    public PublishController(PublishA publishA) {
        this.publishA = publishA;
    }

    @RequestMapping("send-to-topic")
    public Object sendToTopic(String msg) throws InterruptedException {
        for (int i = 0, j = 10; i < j; i++) {
            TimeUnit.SECONDS.sleep(1L);
            publishA.sendToTopic("start.topic", msg + "-" + i);
        }
        return "sendToTopic() success";
    }
}
  • src:c.y.s.subscriber.SubscriberA
package com.yap.z11springboot2.activeMQ.subscriber.subscriber;

import org.apache.activemq.command.ActiveMQTextMessage;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

import javax.jms.JMSException;

/**
 * @author yap
 */
@Component
public class SubscriberA {

    @JmsListener(destination = "start.topic", containerFactory = "jmsListenerContainerFactory")
    public void spendFromTopic(ActiveMQTextMessage msg) {
        try {
            System.out.println("subscriberA spend: " + msg.getText());
        } catch (JMSException e) {
            e.printStackTrace();
        }
    }
}
  • src:c.y.s.subscriber.SubscriberB
package com.yap.z11springboot2.activeMQ.subscriber.subscriber;

import org.apache.activemq.command.ActiveMQTextMessage;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

import javax.jms.JMSException;

/**
 * @author yap
 */
@Component
public class SubscriberB {

    @JmsListener(destination = "start.topic", containerFactory = "jmsListenerContainerFactory")
    public void spendFromTopic(ActiveMQTextMessage msg) {
        try {
            System.out.println("subscriberB spend: " + msg.getText());
        } catch (JMSException e) {
            e.printStackTrace();
        }
    }
}

3. RocketMQ

概念: rocketmq官网 阿里巴巴开源的一款高性能,高吞吐量的分布式MQ,源于jms规范但不遵守jms规范:

  • RMQ安装:下载RMQ并配置环境变量 ROCKETMQ_HOMEpath
    • z-res/rocketmq-all-4.7.1-bin-release.zip
  • cmd: mqnamesrv.cmd -n localhost:9876 启动RMQ服务,端口默认9876。
  • cmd: mqbroker.cmd -n localhost:9876 autoCreateTopicEnable=true 启动broker且自动创建topic。
  • RMQ可视化界面:解压缩 z-res/rocketmq-externals-master.zip 并进入子工程 rocketmq-console
    • 修改主配 server.port=12581:配置RMQ可视化界面端口,默认8080,和tomcat冲突。
    • 修改主配 rocketmq.config.namesrvAddr=localhost:9876:配置RMQ服务端地址,默认 *
    • cmd打包 mvn clean package -Dmaven.test.skip=true,并在target文件夹中找到该jar包。
    • cmd运行 java -jar rocketmq-console-ng-1.0.1.jar
    • cli访问 http://localhost:12581 进入RMQ管理界面。
  • 创建springboot-jar项目 springboot2-rocketmq
    • 配置pom依赖:rocketmq-client/rocketmq-common,版本需要对应本机RMQ服务。 源码: /springboot/
  • res:pom.xml
		<!--rocketmq-client-->
		<dependency>
			<groupId>org.apache.rocketmq</groupId>
			<artifactId>rocketmq-client</artifactId>
			<version>4.7.1</version>
		</dependency>

		<!--rocketmq-common-->
		<dependency>
			<groupId>org.apache.rocketmq</groupId>
			<artifactId>rocketmq-common</artifactId>
			<version>4.7.1</version>
		</dependency>

3.1 生产者类

概念: 开发生产者类 c.y.s.producer.ProducerA 的初始化方法:

  • 标记 @PostConstruct 以使方法在对象执行构造器方法后调用,且只执行一次。
  • new DefaultMQProducer(groupName):创建生产者实例,并纳入指定的生产者组中,生产者组自动创建。
  • producer.setNamesrvAddr(nameSrvAddr):指定RMQ服务地址,集群以分号分隔。
  • producer.setVipChannelEnabled(false):关闭RMQ默认开启的端口为10909的VIP通道,该通道未启动时报错。
  • producer.start():启动生产者实例。
  • 封装方法如 getProducer() 以对外提供该生产者实例。 源码: /springboot/
  • src:c.y.s.producer.ProducerA
package com.yap.springboot2rocketmq.producer;

import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * @author yap
 */
@Component
public class ProducerA {

    private DefaultMQProducer producer;

    @PostConstruct
    public void init() {
        try {
            producer = new DefaultMQProducer("producer-group-a");
            producer.setNamesrvAddr("localhost:9876");
            producer.setVipChannelEnabled(false);
            producer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public DefaultMQProducer getProducer() {
        return producer;
    }
}

3.2 消费者类

概念: 开发消费者类 c.y.s.consumer.ConsumerA 的初始化方法:

  • 标记 @PostConstruct 以使方法在对象执行构造器方法后调用,且只执行一次。
  • new DefaultMQPushConsumer(groupName):创建消费者实例,并纳入指定的消费者组中,消费者组自动创建。
  • producer.setNamesrvAddr(nameSrvAddr):指定RMQ服务地址,集群以分号分隔。
  • consumer.subscribe(topic, tag):订阅指定主题中的指定标签:
    • 多标签用 || 分隔,null* 表示订阅所有标签。
    • 若启动broker的时候附带了 autoCreateTopicEnable=true 则topic和tag可以自动创建。
  • consumer.registerMessageListener():设置监听,参数可直接使用Lambda表达式:
    • new String(messageExt.getBody()):获取消息体中内容的字符串形式。
    • ConsumeConcurrentlyStatus.RECONSUME_LATER:Lambda表达式返回值,表示稍后再试。
    • consumer.start():启动消费者实例。 源码: /springboot/
  • src:c.y.s.consumer.ConsumerA
package com.yap.springboot2rocketmq.consumer;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * @author yap
 */
@Component
public class ConsumerA {

    @PostConstruct
    public void init() {
        DefaultMQPushConsumer consumer;
        try {
            consumer = new DefaultMQPushConsumer("consumer-group-a");
            consumer.setNamesrvAddr("localhost:9876");
            consumer.subscribe("topic-a", "tag-a");
            consumer.registerMessageListener((MessageListenerConcurrently) (messageExtList, context) -> {
                try {
                    for (MessageExt messageExt : messageExtList) {
                        System.out.println("ConsumerA spend: " + new String(messageExt.getBody()));
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            });
            consumer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • src:c.y.s.consumer.ConsumerB
package com.yap.springboot2rocketmq.consumer;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * @author yap
 */
@Component
public class ConsumerB {

    @PostConstruct
    public void init() {
        DefaultMQPushConsumer consumer;
        try {
            consumer = new DefaultMQPushConsumer("consumer-group-b");
            consumer.setNamesrvAddr("localhost:9876");
            consumer.subscribe("topic-a", "tag-a");
            consumer.registerMessageListener((MessageListenerConcurrently) (messageExtList, context) -> {
                try {
                    for (MessageExt messageExt : messageExtList) {
                        System.out.println("ConsumerB spend: " + new String(messageExt.getBody()));
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            });
            consumer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.3 动作类

概念: 开发动作类 c.y.s.controller.RocketMqController:注入生产者类:

  • 在动作方法中接收主题 topic,标签 tag 和消息内容 msg
  • 构建 o.a.r.c.m.Message 对象以封装 topictagmsg
    • msg.getBytes(RemotingHelper.DEFAULT_CHARSET)):设置UTF8编码。
  • 调用 producer.send() 将消息发送到broker并返回一个 SendResult 对象:
    • sendResult.getMsgId():返回消息ID。
    • sendResult.getSendStatus():返回发送状态如 SEND_OK
  • psm测试接口:api/rocketmq/send 源码: /springboot/
  • src:c.y.s.controller.RocketMqController
package com.yap.springboot2rocketmq.controller;

import com.yap.springboot2rocketmq.producer.ProducerA;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author yap
 */
@RestController
@RequestMapping("api/rocketmq")
public class RocketMqController {

    private ProducerA producerA;

    @Autowired
    public RocketMqController(ProducerA producerA) {
        this.producerA = producerA;
    }

    @RequestMapping("send")
    public String send(String topic, String tag, String msg) throws Exception {
        SendResult sendResult = producerA.getProducer().send(
                new Message(topic, tag, msg.getBytes(RemotingHelper.DEFAULT_CHARSET)));
        System.out.println("msgId: " + sendResult.getMsgId());
        System.out.println("sendStatus: " + sendResult.getSendStatus());
        return "success";
    }
}