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并IOCo.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_HOME和path: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/
- 配置pom依赖:
- 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对象以封装topic,tag和msg: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";
}
}