消息中间件概述
MQ = 消息队列
消息中间件利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。
分布式系统的问题
微服务分布式架构后,链式调用是我们在写程序时候的一般流程,为了完成一个整体功能会将其拆分成多个函数(或子模块),比如模块A调用模块B、模块B调用模块C、模块C调用模块D。但在大型分布式应用中,系统间的RPC交互繁杂,耦合性很大。
面对大流量并发时,容易被冲垮。
RPC接口上基本都是同步调用,整体的服务性能遵循“木桶理论”,即整体系统的耗时取决于链路中最慢的那个接口。比如A调用B/C/D都是50ms,但此时B又调用了B1,花费2000ms,那么直接就拖累了整个服务性能。
消息队列的作用
- 解耦:要做到系统解耦,当新的模块接进来时,可以做到代码改动最小;
- 异步:将非关键调用链路的操作异步化并提升整体系统的吞吐能力;
- 削峰:抵御洪峰流量,保护了主业务。
消息队列调用流程
发送者把消息发送给消息服务器,消息服务器将消息存放在若干队列/主题topic中,在合适的时候,消息服务器回将消息转发给接受者。在这个过程中,发送和接收是异步的,也就是发送无需等待,而且发送者和接受者的生命周期也没有必然的关系;尤其在发布pub/订阅sub模式下,也可以完成一对多的通信,即让一个消息有多个接受者。
消息队列产品种类
- Kafka:编程语言为Scala,大数据领域的主流MQ。
- RabbitMQ:基于erlang语言,不好修改底层和查找问题的原因,不建议选用。
- RocketMQ:编程语言Java,适用于大型项目,适用于集群。
- ActiveMQ:编程语言Java,适用于中小型项目。
无论是什么消息中间件,都需要以下功能:发送与接收API、高可用性、集群和容错配置、持久化、延时发送或定时投递、签收机制、与Spring整合…
ActiveMQ安装与操作
安装
官网地址: http://activemq.apache.org/
下载压缩包apache-activemq-5.14.3-bin.tar.gz
放入opt目录解压即可。
启动:./activemq start
速度极快
也可以让启动的日志信息不在控制台打印,而放到专门的文件中:
./activemq start > /myactivemq/myrunmq.log
ActiveMQ的默认端口为61616
查看ActiveMQ是否成功启动:
[root@hadoop1 bin]# ps -ef|grep activemq | grep -v grep
root 3679 1 5 16:15 pts/0 00:00:11 /opt/jdk1.7.0_79/bin/java -Xms64M -Xmx1G -Djava.util.logging.config.file=logging.properties -Djava.security.auth.login.config=/root/apache-activemq-5.12.0//conf/login.config -Dcom.sun.management.jmxremote -Djava.awt.headless=true -Djava.io.tmpdir=/root/apache-activemq-5.12.0//tmp -Dactivemq.classpath=/root/apache-activemq-5.12.0//conf:/root/apache-activemq-5.12.0//../lib/ -Dactivemq.home=/root/apache-activemq-5.12.0/ -Dactivemq.base=/root/apache-activemq-5.12.0/ -Dactivemq.conf=/root/apache-activemq-5.12.0//conf -Dactivemq.data=/root/apache-activemq-5.12.0//data -jar /root/apache-activemq-5.12.0//bin/activemq.jar start
控制台
关闭防火墙service iptables stop
后访问ActiveMQ管理页面地址:http://IP地址:8161
账户:admin 密码:admin
ActiveMQ的API操作
JMS编码总体规范
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TwsiSiKN-1584687298857)(http://picture.tjtulong.top/JMS%E7%BC%96%E7%A0%81%E6%9E%84%E6%9E%B6.png)]
Destination分为两种:队列(点对点)和主题(一对多)。
队列生产者编码
点对点消息传递的特点为:
- 每个消息只有一个消费者,类似1对1的关系;
- 消息的生产者和消费者之间没有时间上的相关性;
- 消息被消费后队列中不会再存储,所以消费者不会消费到已经被消费掉的消息。
创建Maven工程,pom依赖为:
<dependencies>
<!-- activemq 所需要的jar 包-->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.12.0</version>
</dependency>
<!-- activemq 和 spring 整合的基础包 -->
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>3.16</version>
</dependency>
</dependencies>
**队列的消息生产者代码:**connection–>session–>producer
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class JmsProduce {
// Linux上部署的activemq的IP地址+ActiveMQ的端口号
public static final String ACTIVEMQ_URL = "tcp://192.168.25.131:61616";
// 目的队列名称
public static final String QUEUE_NAME = "queue";
public static void main(String[] args) throws Exception {
// 1.按照给定的url创建连接工厂,这个构造器采用默认的用户名密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 2.通过连接工厂,获得连接connection并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3.创建会话session:第一参数是是否开启事务,第二参数是消息签收的方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4.创建目的地(两种:队列/主题),Destination是Queue和Topic的父类接口
Queue queue = session.createQueue(QUEUE_NAME);
// 5.创建消息的生产者
MessageProducer messageProducer = session.createProducer(queue);
// 6.通过messageProducer生产3条消息发送到消息队列中
for (int i = 1; i < 4; i++) {
// 7.创建消息
TextMessage textMessage = session.createTextMessage("msg--" + i);
// 8.通过messageProducer发送给mq
messageProducer.send(textMessage);
}
// 9.关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println(" **** 消息发送到MQ完成 ****");
}
}
在ActiveMQ控制台上可见:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BiqT4kOG-1584687298862)(http://picture.tjtulong.top/%E6%B6%88%E6%81%AF%E7%94%9F%E4%BA%A7%E8%80%85.JPG)]
- Number Of Pending Messages:等待消费的消息,这个是未出队列的数量,公式=总接收数-总出队列数。
- Number Of Consumers:消费者数量,消费者端的消费者数量。
- Messages Enqueued:进队消息数,进队列的总消息量,包括出队列的。这个数只增不减。
- Messages Dequeued:出队消息数,可以理解为是消费者消费掉的数量。
队列消费者编码
方法一:recieve方法
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class JmsConsumer {
public static final String ACTIVEMQ_URL = "tcp://192.168.25.131:61616";
public static final String QUEUE_NAME = "queue";
public static void main(String[] args) throws Exception {
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue(QUEUE_NAME);
// 创建消息的消费者
MessageConsumer messageConsumer = session.createConsumer(queue);
while (true) {
// reveive():一直等待接收消息,在能够接收到消息之前将一直阻塞,是同步阻塞方式.
// reveive(Long time) : 等待n毫秒之后还没有收到消息,就是结束阻塞。
// 因为消息发送者是 TextMessage,所以消息接受者也要是TextMessage
TextMessage message = (TextMessage) messageConsumer.receive();
if (null != message) {
System.out.println("****消费者的消息:" + message.getText());
} else {
break;
}
}
messageConsumer.close();
session.close();
connection.close();
}
}
控制台变化:
方法二:通过监听的方式消费消息MessageListener
// 消息的消费者 也就是回答消息的系统
public class JmsConsumer {
public static final String ACTIVEMQ_URL = "tcp://192.168.25.131:61616";
public static final String QUEUE_NAME = "queue";
public static void main(String[] args) throws Exception{
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
javax.jms.Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue(QUEUE_NAME);
MessageConsumer messageConsumer = session.createConsumer(queue);
/* 通过监听的方式来消费消息,是异步非阻塞的方式消费消息。
通过messageConsumer的setMessageListener注册一个监听器,当有消息发送来时,系统自动调MessageListener的onMessage方法处理消息
*/
messageConsumer.setMessageListener(new MessageListener() {
public void onMessage(Message message) {
if (null != message && message instanceof TextMessage){
TextMessage textMessage = (TextMessage)message;
try {
System.out.println("****消费者的消息:"+textMessage.getText());
}catch (JMSException e) {
e.printStackTrace();
}
}
}
});
// 实际开发中,我们的程序会一直运行,这句代码都会省略。
System.in.read();
messageConsumer.close();
session.close();
connection.close();
}
}
ActiveMQ好像自带负载均衡,当先启动两个队列(Queue)的消费者时,在启动生产者发出消息,此时的消息平均的被两个消费者消费。 并且消费者不会消费已经被消费的消息(即为已经出队的消息)。
总结:
-
同步阻塞方式(receive)
订阅者或接收者抵用MessageConsumer的receive()方法来接收消息,receive方法在能接收到消息之前(或超时之前)将一直阻塞。
-
异步非阻塞方式(监听器onMessage())
订阅者或接收者通过MessageConsumer的setMessageListener(MessageListener listener)注册一个消息监听器,当消息到达之后,系统会自动调用监听器MessageListener的onMessage(Message message)方法。
Topic模式
发布/订阅消息传递域的特点如下:
- 生产者将消息发布到topic中,每个消息可以有多个消费者,属于1:N的关系;
- 生产者和消费者之间有时间上的相关性,订阅某一个主题的消费者只能消费自它订阅之后发布的消息。
- 生产者生产时,topic不保存消息,它是无状态的不落地,假如无人订阅就去生产,那就是一条废消息,所以,一般先启动消费者再启动生产者。
生产者与消费者的代码只与点对点模式差一行代码:
Topic topic = session.createTopic(TOPIC_NAME);
当启动多个消费者时,每个消费者都能收到自从自己启动后订阅主题的消息。
ActiveMQ控制台:
消费消息的数量 = 在线消费者数量 * 生产消息的数量
Topic与Queue两种模式的对比
JMS规范
JavaEE是一套使用Java进行企业级开发的13 个核心规范工业标准。而JMS(Java Message Service)是13个标准之一,是两个应用程序之间进行异步通信的API。
JMS组成
消息头
JMS常用的消息头:
- JMSDestination:消息目的地
- JMSDeliveryMode:消息持久化模式
- JMSExpiration:消息过期时间
- JMSPriority:消息的优先级
- JMSMessageID:消息的唯一标识符,解决幂等性。
/*
持久模式和非持久模式。
一条持久性的消息:应该被传送“一次仅仅一次”,这就意味着如果JMS提供者出现故障,该消息并不会丢失,它会在服务器恢复之后再次传递。
一条非持久的消息:最多会传递一次,这意味着服务器出现故障,该消息将会永远丢失。
*/
textMessage.setJMSDeliveryMode(0);
/*
可以设置消息在一定时间后过期,默认是永不过期。
消息过期时间,等于Destination的send方法中的timeToLive值加上发送时刻的GMT时间值。
如果timeToLive值等于0,则JMSExpiration被设为0,表示该消息永不过期。
如果发送后,在消息过期时间之后还没有被发送到目的地,则该消息被清除。
*/
textMessage.setJMSExpiration(1000);
/* 消息优先级,从0-9十个级别,0-4是普通消息5-9是加急消息。
JMS不要求MQ严格按照这十个优先级发送消息但必须保证加急消息要先于普通消息到达。默认是4级。
*/
textMessage.setJMSPriority(10);
// 唯一标识每个消息的标识。MQ会给我们默认生成一个,我们也可以自己指定。
textMessage.setJMSMessageID("ABCD");
消息体
5种消息体 | TextMessage | Mapmessage | BytesMessage | StreamMessage | ObjectMessage |
---|---|---|---|---|---|
含义 | 普通字符串消息,包含一个String | Map 类型的消息, [k-> String,v -> Java基本类型] | 二进制数组消息,包含一个byte[] | Java数据流消息,用标准流操作来顺序的填充读取 | 对象消息,包含一个可序列化的Java对象 |
TextMessage与Mapmessage较为常用
// 发送MapMessage消息体。set方法: 添加,get方式:获取
MapMessage mapMessage = session.createMapMessage();
mapMessage.setString("name", "张三"+i);
mapMessage.setInt("age", 18+i);
消息属性
如果需要除消息头字段之外的值,那么可以使用消息属性。
消息属性是以属性名和属性值对的形式制定的。可以将属性是为消息头得扩展,属性指定一些消息头没有包括的附加信息,比如可以在属性里指定消息选择器。消息的属性就像可以分配给一条消息的附加消息头一样。它们允许开发者添加有关消息的不透明附加信息。
消息的持久化
JMS的可靠性主要依靠三点:Persistent持久性 、事务和Acknowledge签收。
点对点的持久化:
持久化的消息,服务器宕机后消息依旧存在,只是没有入队,当服务器再次启动,消息任就会被消费。
但是非持久化的消息,服务器宕机后消息永远丢失。 而当没有注明是否是持久化还是非持久化时,默认是持久化的消息。
// 在队列为目的地的时候持久化消息
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
// 队列为目的地的非持久化消息
messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
详细原理:https://blog.51cto.com/2005713/1965775
topic的持久化:
topic消息持久化,只要消费者向MQ服务器注册过,所有生产者发布成功的消息,该消费者都能收到,不管是MQ服务器宕机还是消费者不在线后上线。
对于消费者,需要先向MQ服务器注册
// 持久化topic 的消息消费者
public class JmsConsummer_persistence {
public static final String ACTIVEMQ_URL = "tcp://192.168.17.3:61616";
public static final String TOPIC_NAME = "topic01";
public static void main(String[] args) throws Exception{
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
// 设置客户端ID。向MQ服务器注册自己的名称
connection.setClientID("xiao");
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC_NAME);
// 创建一个topic订阅者对象。一参是topic,二参是订阅者名称
TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic,"tj");
// 之后再开启连接
connection.start();
Message message = topicSubscriber.receive();
while (null != message){
TextMessage textMessage = (TextMessage)message;
System.out.println(" 收到的持久化 topic :"+textMessage.getText());
message = topicSubscriber.receive();
}
session.close();
connection.close();
}
}
事务
createSession的第一个参数为true为开启事务,开启事务之后必须在将消息提交,才可以在队列中看到消息。
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
提交:
session.commit();
事务开启的意义在于,如果对于多条必须同批次传输的消息,可以使用事务,如果一条传输失败,可以将事务回滚,再次传输,保证数据的完整性。
对于消息消费者来说,开启事务的话,可以避免消息被多次消费,以及后台和服务器数据的不一致性。例如:如果消息消费的createSession设置为ture,但是没有commit
,此时就会造成非常严重的后果,那就是在后台看来消息已经被消费,但是对于服务器来说并没有接收到消息被消费,此时就有可能被多次消费。
消费者签收
签收的几种方式:
- 自动签收(Session.AUTO_ACKNOWLEDGE):该方式是默认的。该种方式,无需我们程序做任何操作,框架会帮我们自动签收收到的消息。
- 手动签收(Session.CLIENT_ACKNOWLEDGE):手动签收。该种方式,需要我们手动调用Message.acknowledge(),来签收消息。如果不签收消息,该消息会被我们反复消费,直到被签收。
Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
- 允许重复消息(Session.DUPS_OK_ACKNOWLEDGE):多线程或多个消费者同时消费到一个消息,因为线程不安全,可能会重复消费。该种方式很少使用到。
TextMessage textMessage = (TextMessage) message;
System.out.println("***消费者接收到的消息:" + textMessage.getText());
/*
设置为Session.CLIENT_ACKNOWLEDGE后,要调用该方法,标志着该消息已被签收(消费)。
如果不调用该方法,该消息的标志还是未消费,下次启动消费者或其他消费者还会收到改消息。
*/
textMessage.acknowledge();
- 对于开启事务时,设置手动签收和自动签收没有多大的意义,都默认自动签收,也就是说事务的优先级更高一些。
小知识点:ActiveMQ的Broker
broker就是实现了用代码形式启动ActiveMQ,将MQ内嵌到Java代码中,可以随时启动,节省资源,提高了可靠性。就是将MQ服务器作为了Java对象。
Spring整合ActiveMQ
pom.xml依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>top.tjtulong</groupId>
<artifactId>ActiveMQ_Api</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!--activemq所需要的jar包-->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.12.0</version>
</dependency>
<!-- activemq 和 spring 整合的基础包 -->
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>3.16</version>
</dependency>
<!-- 嵌入式activemq的broker所需要的依赖包 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.1</version>
</dependency>
<!-- activemq连接池 -->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-pool</artifactId>
<version>5.12.0</version>
</dependency>
<!-- spring支持jms的包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>5.2.1.RELEASE</version>
</dependency>
<!--spring相关依赖包-->
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>4.15</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.23.RELEASE</version>
</dependency>
<!-- Spring核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.3.23.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.23.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.23.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.3.23.RELEASE</version>
</dependency>
</dependencies>
</project>
Spring的ActiveMQ配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--开启包的自动扫描-->
<context:component-scan base-package="com.activemq.demo"/>
<!--配置生产者-->
<bean id="connectionFactory" class="org.apache.activemq.pool.PooledConnectionFactory" destroy-method="stop">
<property name="connectionFactory">
<!--正真可以生产Connection的ConnectionFactory,由对应的JMS服务商提供-->
<bean class="org.apache.activemq.spring.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://192.168.10.130:61616"/>
</bean>
</property>
<property name="maxConnections" value="100"/>
</bean>
<!--队列目的地,点对点的Queue-->
<bean id="destinationQueue" class="org.apache.activemq.command.ActiveMQQueue">
<!--通过构造注入Queue名-->
<constructor-arg index="0" value="spring-active-queue"/>
</bean>
<!--队列目的地,布订阅的主题Topic-->
<bean id="destinationTopic" class="org.apache.activemq.command.ActiveMQTopic">
<constructor-arg index="0" value="spring-active-topic"/>
</bean>
<!--Spring提供的JMS工具类,可以进行消息发送,接收等 -->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<!--传入连接工厂-->
<property name="connectionFactory" ref="connectionFactory"/>
<!--传入目的地-->
<property name="defaultDestination" ref="destinationQueue"/>
<!--消息自动转换器-->
<property name="messageConverter">
<bean class="org.springframework.jms.support.converter.SimpleMessageConverter"/>
</property>
</bean>
</beans>
点对点生产者
@Service
public class SpringProduce {
@Autowired
private JmsTemplate jmsTemplate;
public static void main(String[] args) {
ApplicationContext app = new ClassPathXmlApplicationContext("activemq.xml");
// 从Spring中获取Producer类
SpringProduce springMQ_producer = app.getBean(SpringProduce.class);
springMQ_producer.jmsTemplate.send(
new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage("***Spring和ActiveMQ的整合***");
}
}
);
System.out.println("send task over");
}
}
点对点消费者
public class Spring_MQConsummer {
@Autowired
private JmsTemplate jmsTemplate;
public static void main(String[] args) {
ApplicationContext app = new ClassPathXmlApplicationContext("activemq.xml");
Spring_MQConsummer sm = (Spring_MQConsummer)app.getBean("spring_MQConsummer");
String s = (String) sm.jmsTemplate.receiveAndConvert();
System.out.println(" *** 消费者消息"+s);
}
}