目录
1、ActiveMQ
1.1、什么是消息中间件
消息中间件适用于需要可靠的数据传送的分布式环境。采用消息中间件机制的系统中,不同的对象之间通过传递消息来激活对方的事件,完成相应的操作。发送者将消息发送给消息服务器,消息服务器将消息存放在若干队列中,在合适的时候再将消息转发给接收者。消息中间件能在不同平台之间通信,它常被用来屏蔽掉各种平台及协议之间的特性,实现应用程序之间的协同,其优点在于能够在客户和服务器之间提供同步和异步的连接,并且在任何时刻都可以将消息进行传送或者存储转发,这也是它比远程过程调用RPC更进一步的原因。
1.2、为什么使用?
- 业务解耦,代码解耦:例如,订单业务需要发送短信、推送app通知信息、扣除会员积分、发送email等,如果把这些全部写在订单业务代码中,订单代码将会变得十分臃肿,不利于修改维护,事物管理十分麻烦,使用中间件就不会有上述问题。
- 同步变异步,加快业务响应时间,相对于RPC来说,异步通信使得生产者和消费者得以充分执行自己的逻辑而无需等待。
- 流量消峰,消息存储堆积也是消息中间件的核心,可堆积大量的消息,当上游系统的吞吐能力远高于下游系统,在流量洪峰时可能会冲垮下游系统,消息中间件可以在峰值时堆积消息,而在峰值过去后下游系统慢慢消费消息解决流量洪峰的问题,典型的场景就是秒杀系统的设计。
- 消息的顺序性,消息中间件采用的是队列技术,消息队列可以保证消息的先进先出,能保证消息顺序执行。
- 消息的可靠性,消息中间件有消费确认机制(ACK),在收到成功被消费的确认消息之后才会把消息从队列中删除,并且消息中间件有本地刷盘存储功能。
解决分布式事物复杂性。
1.3、安装使用(Linux)
1、下载:http://activemq.apache.org/
2、下载完成后解压到自定义文件夹中
[root@ROOT opt]# tar -zxvf apache-activemq-5.15.13-bin.tar.gz
3、启动activemq
进入bin目录,启动activemq命令,其后端ip为:9247
[root@ROOT bin]# ./activemq start
进程查询:
ps -ef|grep activemq|grep -v grep
4、访问前台管理页面
地址:http://ip:8161/admin/
用户名:admin / admin
进入conf目录查看配置文件
注意:关闭windows、linux系统防火墙
2、HelloWorld
2.1、queue
队列模式,消费者跟生产者为一对一,生产一条消息,消费者就会消费一条消息。不存在消息丢失。
2.1.1、消息生产
package com.moon.study;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
/**
* mq启动demo
* */
public class JmsProducer {
//activemq 地址
public static final String DEFAULT_BROKER_URL = "tcp://192.168.198.222:61616";
//队列名称
public static final String QUEUE_NAME = "QUEUE1";
public static void main(String[] args) throws JMSException {
//1、获取工厂
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(DEFAULT_BROKER_URL);
//2、建立连接
Connection connection = factory.createConnection();
//启动连接
connection.start();
//3、创建会话 两个参数,第一个事务, 第二个签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4、创建目的地(这里有两种:队列、主题,这里用主题)
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建消息的生产者
MessageProducer producer = session.createProducer(queue);
//6、通过MessageProducer生产3条消息
for (int i = 0; i < 3 ; i++) {
//7、创建消息
TextMessage message = session.createTextMessage("message--" + i);
//8、通过MessageProducer发布消息
producer.send(message);
}
//9、关闭资源
producer.close();
session.close();
connection.close();
System.out.println("*** 消息发送完成 ***");
}
}
2.1.2、消息消费
package com.moon.study;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
import java.io.IOException;
/**
* mq启动demo
* */
public class JmsConsumer {
//activemq 地址
public static final String DEFAULT_BROKER_URL = "tcp://192.168.198.222:61616";
//队列名称
public static final String QUEUE_NAME = "QUEUE1";
public static void main(String[] args) throws JMSException, IOException {
//1、获取工厂
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(DEFAULT_BROKER_URL);
//2、建立连接
Connection connection = factory.createConnection();
//启动连接
connection.start();
//3、创建会话 两个参数,第一个事务, 第二个签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4、创建目的地(这里有两种:队列、主题,这里用主题)
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建消息的消费者
MessageConsumer consumer = session.createConsumer(queue);
//6、消费消息
// (1) 使用receive方法获取消息
/*
while (true)
{
//receive在没有接受到消息的时候会一致阻塞:同步阻塞
TextMessage message = (TextMessage) consumer.receive();
if (message != null)
{
System.out.println("获取到消息:" + message.getText());
}
else
{
break;
}
}*/
// (2) 通过异步非阻塞的方式来监听消息
// 订阅者通过注册一个消息监听器,当消息到底之后,系统自动调用MessageListener.onMessage方法
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
if (null != message || message instanceof TextMessage)
{
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("MessageListener:" + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
});
System.in.read();
//9、关闭资源
consumer.close();
session.close();
connection.close();
}
}
2.2、topic
该模式为发布\订阅,生产者发布消息,消费者订阅消息,订阅者可为多个。消费者无法接收在订阅生产者之前发布的消息,因此消息会丢失。
2.2.1、消息消费
package com.moon.study;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
import java.io.IOException;
/**
* mq启动demo
* */
public class JmsConsumer_topic {
//activemq 地址
public static final String DEFAULT_BROKER_URL = "tcp://192.168.198.222:61616";
//队列名称
public static final String TOPIC_NAME = "TOPIC1";
public static void main(String[] args) throws JMSException, IOException {
//1、获取工厂
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(DEFAULT_BROKER_URL);
//2、建立连接
Connection connection = factory.createConnection();
//启动连接
connection.start();
//3、创建会话 两个参数,第一个事务, 第二个签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4、创建目的地(这里有两种:队列、主题,这里用主题)
Topic topic = session.createTopic(TOPIC_NAME);
//5、创建消息的消费者
MessageConsumer consumer = session.createConsumer(topic);
//6、消费消息
// (1) 使用receive方法获取消息
/*
while (true)
{
//receive在没有接受到消息的时候会一致阻塞:同步阻塞
TextMessage message = (TextMessage) consumer.receive();
if (message != null)
{
System.out.println("获取到消息:" + message.getText());
}
else
{
break;
}
}*/
// (2) 通过异步非阻塞的方式来监听消息
// 订阅者通过注册一个消息监听器,当消息到底之后,系统自动调用MessageListener.onMessage方法
consumer.setMessageListener((message)-> {
if (null != message || message instanceof TextMessage)
{
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("MessageListener:" + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
});
System.in.read();
//9、关闭资源
consumer.close();
session.close();
connection.close();
}
}
2.2.2、消息生产
package com.moon.study;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
/**
* mq启动demo
* */
public class JmsProducer_topic {
//activemq 地址
public static final String DEFAULT_BROKER_URL = "tcp://192.168.198.222:61616";
//队列名称
public static final String TOPIC_NAME = "TOPIC1";
public static void main(String[] args) throws JMSException {
//1、获取工厂
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(DEFAULT_BROKER_URL);
//2、建立连接
Connection connection = factory.createConnection();
//启动连接
connection.start();
//3、创建会话 两个参数,第一个事务, 第二个签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4、创建目的地(这里有两种:队列、主题,这里用主题)
Topic topic = session.createTopic(TOPIC_NAME);
//5、创建消息的生产者
MessageProducer producer = session.createProducer(topic);
//6、通过MessageProducer生产3条消息
for (int i = 0; i < 3 ; i++) {
//7、创建消息
TextMessage message = session.createTextMessage("message--" + i);
//8、通过MessageProducer发布消息
producer.send(message);
}
//9、关闭资源
producer.close();
session.close();
connection.close();
System.out.println("*** 消息发送完成 ***");
}
}
2.3、总结
1. 开发步骤
2.两种消费方式
同步阻塞方式:订阅者调用MessageConsumer的receive()方法来接收消息,
receive方法能在接收到消息之前(或超时之前)将一直阻塞。
while (true)
{
//receive在没有接受到消息的时候会一致阻塞:同步阻塞
TextMessage message = (TextMessage) consumer.receive();
if (message != null)
{
System.out.println("获取到消息:" + message.getText());
}
else
{
break;
}
}
异步非阻塞方式:订阅者通过调用MessageConsumer的SetMessageListener (MessageListener listener)注册一个监听器,当消息到达之后,系统将自动调用监听器MessageListener的onMessage方法。
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
if (null != message || message instanceof TextMessage)
{
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("MessageListener:" + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
});
3、两种模式的对比
3、Jms(Java Message Server)
3.1、什么是jms
3.2、消息中间件产品对比mq
3.3、jms组成结构和消息特点
3.3.1、组成结构
- jms provider:实现jms接口和规范的消息中间件,也就是MQ服务器
- jms producer:消息生产者,创建和发送jms消息的客户端
- jms consumer:消息消费者,接受和消费jms消息的客户端
- jms message:jms 消息,由消息头,消息属性和消息体组成
3.3.2、Message
3.3.2.1 消息头
- JMSDestination:消息目的地
- JMSDeliveryModle:消息投递模式,是持久还是非持久
- JMSExpiration:消息过期时间,默认为0,永不过期
- JMSPriority:消息优先级(0-9),默认为4,4以上为加急消息,4以下为普通消息
- JMSMessageID:消息唯一标识
3.3.2.2、消息体
- 封装消息的具体数据
- 5种消息体格式
- 发送和接受消息体的类型必须一致
3.3.2.3、消息属性
如果需要除消息头以外的值,那么就可以使用消息属性。通常用作识别、去重、重点标识作用
3.4、消息的可靠性
3.4.1 消息的持久性(persistent)
- 持久的Queue: 默认是持久化
- 持久的topic
3.4.1 .1 topic 持久化代码
消费者
package com.moon.study;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
import java.io.IOException;
/**
* topic 消费者持久化topicSubscriber
* */
public class JmsConsumer_topic_persistent {
//activemq 地址
public static final String DEFAULT_BROKER_URL = "tcp://192.168.198.222:61616";
//队列名称
public static final String TOPIC_NAME = "topicSubscriber";
public static void main(String[] args) throws JMSException, IOException {
//1、获取工厂
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(DEFAULT_BROKER_URL);
//2、建立连接
Connection connection = factory.createConnection();
connection.setClientID("z1");
//3、创建会话 两个参数,第一个事务, 第二个签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4、创建目的地(这里有两种:队列、主题,这里用主题)
Topic topic = session.createTopic(TOPIC_NAME);
TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic, "remark...");
//5、启动
connection.start();
//6、接受消息
topicSubscriber.setMessageListener(message ->
{
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("接收到持久化消息。。" + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
});
System.in.read();
topicSubscriber.close();
session.close();
connection.close();
}
}
生产者
package com.moon.study;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
/**
* mq启动demo
* */
public class JmsProducer_topic_persistent {
//activemq 地址
public static final String DEFAULT_BROKER_URL = "tcp://192.168.198.222:61616";
//队列名称
public static final String TOPIC_NAME = "topicSubscriber";
public static void main(String[] args) throws JMSException {
//1、获取工厂
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(DEFAULT_BROKER_URL);
//2、建立连接
Connection connection = factory.createConnection();
//3、创建会话 两个参数,第一个事务, 第二个签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4、创建目的地(这里有两种:队列、主题,这里用主题)
Topic topic = session.createTopic(TOPIC_NAME);
//5、创建消息的生产者
MessageProducer producer = session.createProducer(topic);
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
//启动连接
connection.start();
//6、通过MessageProducer生产3条消息
for (int i = 0; i < 3 ; i++) {
//7、创建消息
TextMessage message = session.createTextMessage("topicSubscriberMessage--" + i);
//8、通过MessageProducer发布消息
producer.send(message);
}
//9、关闭资源
producer.close();
session.close();
connection.close();
System.out.println("*** 消息发送完成 ***");
}
}
3.4.2、消息的事务
createSession的第一个参数为true 为开启事务,开启事务之后必须在将消息提交,才可以在队列中看到消息
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
提交:
session.commit();
事务开启的意义在于,如果对于多条必须同批次传输的消息,可以使用事务,如果一条传输失败,可以将事务回滚,再次传输,保证数据的完整性。
对于消息消费者来说,开启事务的话,可以避免消息被多次消费,以及后台和服务器数据的不一致性。举个栗子:
如果消息消费的 createSession 设置为 ture ,但是没有 commit ,此时就会造成非常严重的后果,那就是在后台看来消息已经被消费,但是对于服务器来说并没有接收到消息被消费,此时就有可能被多次消费。
3.4.3、消息签收Acknowledge
-
非事务
Session.AUTO_ACKNOWLEDGE 自动签收,默认
Session.CLIENT_ACKNOWLEDGE 手动签收
手动签收需要acknowledge
textMessage.acknowledge();而对于开启事务时,设置手动签收和自动签收没有多大的意义,都默认自动签收,也就是说事务的优先级更高一些。
Session session = connection.createSession(true,Session.AUTO_ACKNOWLEDGE);
//Session session = connection.createSession(true,Session.CLIENT_ACKNOWLEDGE); // 也是自动签收
session.commit();
但是开启事务没有commit 任就会重复消费
4、broker
broker 就是实现了用代码形式启动 ActiveMQ 将 MQ 内嵌到 Java 代码中,可以随时启动,节省资源,提高了可靠性。
就是将 MQ 服务器作为了 Java 对象
使用多个配置文件启动 activemq
cp activemq.xml activemq02.xml
// 以active02 启动mq 服务器
./activemq start xbean:file:/myactivemq/apache-activemq-5.15.9/conf/activemq02.xml
把小型 activemq 服务器嵌入到 java 代码: 不在使用linux 的服务器
需要的包:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.5</version>
</dependency>
代码实现:
public class Embebroker {
public static void main(String[] args) throws Exception {
// broker 服务
BrokerService brokerService = new BrokerService();
// 把小型 activemq 服务器嵌入到 java 代码
brokerService.setUseJmx(true);
// 原本的是 192.…… 是linux 上的服务器,而这里是本地windows 的小型mq 服务器
brokerService.addConnector("tcp://localhost:61616");
brokerService.start();
}
}
5、Spring整合activeMQ
5.1、队列(Queue)
maven依赖
<?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>com.moon.study</groupId>
<artifactId>mq-spring</artifactId>
<version>1.0-SNAPSHOT</version>
<name>mq-spring</name>
<properties>
<spring.version>4.3.7.RELEASE</spring.version>
</properties>
<dependencies>
<!-- activeMQ jms 的支持 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>4.3.23.RELEASE</version>
</dependency>
<!-- pool 池化包相关的支持 -->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-pool</artifactId>
<version>5.15.9</version>
</dependency>
<!-- Spring依赖 -->
<!-- 1.Spring核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring 上下文-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- 日志相关依赖 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.16</version>
</dependency>
</dependencies>
</project>
spring application.xml配置
<?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"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/mvc">
<context:component-scan base-package="com.moon.study"/>
<bean id="jmsFactory" class="org.apache.activemq.pool.PooledConnectionFactory" destroy-method="stop">
<property name="connectionFactory">
<bean class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://192.168.198.222:61616"></property>
</bean>
</property>
<property name="maxConnections" value="100"></property>
</bean>
<!-- 队列目的地 -->
<bean id="destinationQueue" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg index="0" value="spring-active-queue"></constructor-arg>
</bean>
<!-- jms 的工具类 -->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="jmsFactory"/>
<property name="defaultDestination" ref="destinationQueue"/>
<property name="messageConverter">
<bean class="org.springframework.jms.support.converter.SimpleMessageConverter"/>
</property>
</bean>
</beans>
5.1.1、生产者
package com.moon.study;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;
@Service
public class SpringMQ_Producer {
@Autowired
JmsTemplate jmsTemplate;
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
SpringMQ_Producer springMQ_producer = applicationContext.getBean(SpringMQ_Producer.class);
springMQ_producer.producerMessage("SpringMQ_Producer:发送消息。。。。");
System.out.println("发送成功。。");
}
private void producerMessage (String messageText)
{
jmsTemplate.send((message) -> {
return message.createTextMessage(messageText);
});
}
}
5.1.2、消费者
package com.moon.study;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;
@Service
public class SpringMQ_Consumer {
@Autowired
JmsTemplate jmsTemplate;
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
SpringMQ_Consumer springMQ_consumer = applicationContext.getBean(SpringMQ_Consumer.class);
String message = springMQ_consumer.consumerMessage();
System.out.println("接受消息:" + message);
}
private String consumerMessage ()
{
return (String) jmsTemplate.receiveAndConvert();
}
}
- 异步非阻塞监听
配置监听程序: DefaultMessageListenerContainer可实异步非阻塞监听,当启动生产者生产消息时,程序自动消费消息。
<!--配置监听程序-->
<bean id="defaultMessageListenerContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="jmsFactory"/>
<property name="destination" ref="destinationTopic"/>
<!--消息监听实体-->
<property name="messageListener" ref="topicMessageListener"/>
</bean>
bean topicMessageListener:获取消息进行操作
package com.moon.study;
import org.springframework.stereotype.Component;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
@Component
public class TopicMessageListener implements MessageListener {
@Override
public void onMessage(Message message) {
if (null != message && message instanceof TextMessage)
{
TextMessage textMessage = (TextMessage) message;
System.out.println(textMessage);
}
}
}
5.2、主题
topic与queue的代码差距极小,只需将队列目的地由queue换成topic
<!-- 队列目的地 -->
<bean id="destinationQueue" class="org.apache.activemq.command.ActiveMQTempTopic">
<constructor-arg index="0" value="spring-active-topic"></constructor-arg>
</bean>
6、SpringBoot整合activeMQ
6.1、队列
maven依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.moon.study</groupId>
<artifactId>mq-boot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mq-boot</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml
server:
port: 8080
## activemq配置
spring:
activemq:
broker-url: tcp://192.168.198.222:61616
user: admin
password: admin
jms:
pub-sub-domain: false # 是否topic, false:queue/true:topic
#自己定义队列名称
myqueue: boot-activemq-queue
6.1.1、生产者
配置类
package com.moon.study.mqboot.config;
import org.apache.activemq.command.ActiveMQQueue;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.stereotype.Component;
@Component
@EnableJms
public class ActiveMQConfig {
//队列名称
@Value("${myqueue}")
private String myQueue;
@Bean
public ActiveMQQueue getActiveMQQueue()
{
return new ActiveMQQueue(myQueue);
}
}
生产消息服务类
package com.moon.study.mqboot.active;
import org.apache.activemq.command.ActiveMQQueue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.stereotype.Service;
@Service
public class BootProducer {
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
@Autowired
private ActiveMQQueue activeMQQueue;
public void sendMessage ()
{
jmsMessagingTemplate.convertAndSend(activeMQQueue, "-----boot-activemq-producer-message----");
}
}
6.1.2、定时推送消息
定时任务
/**
* 定时生产消息
* */
@Scheduled(fixedDelay = 3000l)
public void sendMessageSchedule()
{
jmsMessagingTemplate.convertAndSend(activeMQQueue, "-----boot-activemq-producer-message----");
System.out.println("sendMessageSchedule----已推送");
}
启动类添加@EnableScheduling开关注解
@SpringBootApplication
@EnableScheduling
public class MqBootApplication {
public static void main(String[] args) {
SpringApplication.run(MqBootApplication.class, args);
}
}
6.1.2、消费者
- maven依赖与yml配置与生产者相同,注意避免与生产者server.port重复
- 消费代码
package com.moon.study.mqbootcomsumer.active;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Service;
import javax.jms.JMSException;
import javax.jms.TextMessage;
@Service
public class QueueConsumer {
@JmsListener(destination = "${myqueue}")
public void receiveMessage (TextMessage textMessage) throws JMSException
{
System.out.println("------接受消息:" + textMessage.getText());
}
}
6.2、主题
6.2.1、生产者
代码与队列基本相同,主要区别是目的地改为队列,yml配置 jms:pub-sub-domain改为true
package com.moon.study.mqbootproducer.config;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.command.ActiveMQTopic;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.jms.Queue;
import javax.jms.Topic;
@Configuration
public class ActiveMQConfig {
@Value("${mytopic}")
private String mytopic;
@Bean
public Topic getTopic()
{
return new ActiveMQTopic(mytopic);
}
}
6.2.2、消费者
代码与队列基本相同,主要区别是目的地改为主题,yml配置 jms:pub-sub-domain改为true
package com.moon.study.mqbootcomsumer.active;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Service;
import javax.jms.JMSException;
import javax.jms.TextMessage;
@Service
public class TopicConsumer {
@JmsListener(destination = "${mytopic}")
public void receiveMessage (TextMessage textMessage) throws JMSException
{
System.out.println("------mytopic接受消息:" + textMessage.getText());
}
}
7、协议
7.1、 ActiveMQ支持的协议
支持的协议有 TCP 、 UDP、NIO、SSL、HTTP(S) 、VM 这是activemq 的activemq.xml 中配置文件设置协议的地方
<transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumCon nections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnect ions=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConn ections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnect ions=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnection s=1000&wireFormat.maxFrameSize=104857600"/>
</transportConnectors>
默认是使用 openwire 也就是 tcp 连接 默认的Broker 配置,TCP 的Client 监听端口 61616 ,在网络上传输数据,必须序列化数据,消息是通过一个 write protocol 来序列化为字节流。默认情况 ActiveMQ 会把 wire protocol 叫做 Open Wire ,它的目的是促使网络上的效率和数据快速交互 。
使用tcp 的一些优化方案:tcp://hostname:port?key=value
它的参数详情参考:http://activemq.apache.org/tcp-transport-reference
7.2、协议
NIO 协议为ActiveMQ 提供更好的性能
适合NIO 使用的场景:
- 当有大量的Client 连接到Broker 上 , 使用NIO 比使用 tcp 需要更少的线程数量,所以使用 NIO
- 可能对于 Broker 有一个很迟钝的网络传输, NIO 的性能高于 TCP
连接形式:
nio://hostname:port?key=value
各种协议对比 : http://activemq.apache.org/configuring-version-5-transports.html
修改 activemq.xml 使之支持 NIO 协议:
<transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumCon nections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnect ions=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConn ections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnect ions=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnection s=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="nio" uri="nio://0.0.0.0:61618?trace=true"/> <!-- 这是添加的 -->
</transportConnectors>
而使用 NIO 协议,代码修改量极小,只需同时将消息生产者和消费者的 URL 修改即可:
//public static final String ACTIVEMQ_URL = "tcp://192.168.17.3:61616";
public static final String ACTIVEMQ_URL = "nio://192.168.17.3:61618";
修改之后即可正确运行
7.3、NIO 增强
URI 格式以 nio 开头,表示这个端口使用 tcp 协议为基础的NIO 网络 IO 模型,但这样设置让它只支持 tcp 、 nio 的连接协议。如何让它支持多种协议?
Starting with version 5.13.0, ActiveMQ supports wire format protocol
detection. OpenWire, STOMP, AMQP, and MQTT can be automatically
detected. This allows one transport to be shared for all 4 types of
clients.
使用 : auto+nio+ssl
官网介绍 : http://activemq.apache.org/auto
使用 auto 的方式就相当于四合一协议 : STOMP AMQP MQTT TCP NIO
<transportConnector name="auto+nio" uri="auto+nio://localhost:5671"/>
auto 就像是一个网络协议的适配器,可以自动检测协议的类型,并作出匹配
<transportConnector name="auto" uri="auto://localhost:5671?auto.protocols=default,stomp"/>
配置文件修改:
……
<transportConnector name="auto+nio" uri="auto+nio://0.0.0.0:61608?maximumConnections=1000
&wireFormat.maxFrameSize=104857600&org.apache.activemq.transport.nio.SelectorManager.corelPoolSize=20
&org.apache.activemq.transport.nio.SelectorManager.maximumPoolSize=50"/>
连接:
消息发送成功
同样代码只需修改 URI
public static final String ACTIVEMQ_URL = "nio://192.168.17.3:61608";
对于 NIO 和 tcp 的代码相同,但不代表使用其他协议代码相同,因为底层配置不同,其他协议如果使用需要去修改代码
8、持久化
8.1、ActiveMQ 的可持久化
- 将MQ 收到的消息存储到文件、硬盘、数据库 等、 则叫MQ 的持久化,这样即使服务器宕机,消息在本地还是有,仍就可以访问到。
官网 : http://activemq.apache.org/persistence - ActiveMQ 支持的消息持久化机制: 带复制功能的 LeavelDB 、 KahaDB 、 AMQ 、 JDBC
- 持久化就是高可用的机制,即使服务器宕机了,消息也不会丢失
8.2、持久化存储方式
- AMQ: 是文件存储形式,写入快、易恢复 默认 32M 在 ActiveMQ 5.3 之后不再适用
- KahaDB : 5.4 之后基于日志文件的持久化插件,默认持久化插件,提高了性能和恢复能力
KahaDB 的属性配置 : http://activemq.apache.org/kahadb
它使用一个事务日志和 索引文件来存储所有的地址
db-<数字>.log: 存储数据,一个存满会再次创建 db-2 db-3 …… ,当不会有引用到数据文件的内容时,文件会被删除或归档
db.data: 是一个BTree 索引,索引了消息数据记录的消息,是消息索引文件,它作为索引指向了 db-.log 里的消息
一点题外话:就像mysql 数据库,新建一张表,就有这个表对应的 .MYD 文件,作为它的数据文件,就有一个 .MYI 作为索引文件。
db.free : 存储空闲页 ID 有时会被清除
db.redo :当 KahaDB 消息存储在强制退出后启动,用于恢复 BTree 索引
lock : 顾名思义就是锁
四类文件+一把锁 ==》 KahaDB
- LeavelDB : 希望作为以后的存储引擎,5.8 以后引进,也是基于文件的本地数据存储形式,但是比 KahaDB 更快
它比KahaDB 更快的原因是她不使用BTree 索引,而是使用本身自带的 LeavelDB 索引
题外话:为什么LeavelDB 更快,并且5.8 以后就支持,为什么还是默认 KahaDB 引擎,因为 activemq 官网本身没有定论,LeavelDB 之后又出了可复制的LeavelDB 比LeavelDB 更性能更优越,但需要基于 Zookeeper 所以这些官方还没有定论,任就使用 KahaDB - JDBC持久化。
8.3、JDBC持久化配置mysql
使用JDBC 的持久化
- 修改配置文件,默认 kahaDB
修改之前:
<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>
修改之后:
<persistenceAdapter>
<jdbcPersistenceAdapter dataSource="#mysql-ds"/>
</persistenceAdapter>
- 在activemq 的lib 目录下添加 jdbc 的jar 包 (connector.jar 我使用5.1.49 版本)
- 修改配置文件 : activemq.xml 使其连接自己windows 上的数据库,并在本地创建名为activemq 的数据库
<bean id="mysql-ds" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.198.1:3306/activemq?relaxAutoCommit=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="poolPreparedStatements" value="true"/>
</bean>
- 让linux 上activemq 可以访问到 mysql ,之后产生消息。
ActiveMQ 启动后会自动在 mysql 的activemq 数据库下创建三张表:activemq_msgs 、activemq_acks、activemq_lock
activemq_acks:用于存储订阅关系。如果是持久化Topic,订阅者和服务器的订阅关系在这个表保存
activemq_lock:在集群环境中才有用,只有一个Broker可以获得消息,称为Master Broker
activemq_msgs:用于存储消息,Queue和Topic都存储在这个表中
点对点会在数据库的数据表 ACTIVEMQ_MSGS 中加入消息的数据,且在点对点时,消息被消费就会从数据库中删除
但是对于主题,订阅方式接受到的消息,会在 ACTIVEMQ_MSGS 存储消息,即使MQ 服务器下线,并在 ACTIVEMQ_ACKS 中存储消费者信息 。 并且存储以 activemq 为主,当activemq 中的消息被删除后,数据库中的也会自动被删除。
8.4、修改配置坑
无法正常启动是可以先在data/activemq.log问价查看启动日志,分析失败原因
- 失败1:没有远程访问的权限,需要你给指定用户设置访问权限才能远程访问该数据库,报错如下图,
解决方法:sql赋权限,root用户名,192.168.198.222虚拟机ip,123密码
GRANT ALL PRIVILEGES ON *.* TO 'root'@'192.168.198.222' IDENTIFIED BY '123456' WITH GRANT OPTION;
- 失败二:虚拟机无法访问3306端口号
使用wget ip:port 测试是否能访问端口
如果失败,添加端口入站规则。
8.5、jdbc改进:加入高速缓存机制Journal
该机制相当与在mysql与mq之间加了个Journal缓存,当同时产生10000条消息时,先把消息存在缓存中,如果缓存中的消息被消费90%,剩下的没来得及消费10%则会被存储在mysql中。
高速缓存在 activemq.xml 中的配置:
8.6、持久化总结
9、多节集群
如何保证高可用 ==》 搭建集群
ZooKeeper + Replicated LevelDB Store ==》
集群 http://activemq.apache.org/replicated-leveldb-store
这幅图的意思就是 当 Master 宕机后,zookeper 监测到没有心跳信号, 则认为 master 宕机了,然后选举机制会从剩下的 Slave 中选出一个作为 新的 Master
搭建大概流程:
9.1、搭建zookeper 集群,搭建 activemq 集群
集群搭建:
-
新建 /mq_cluster 将原始的解压文件复制三个,修改端口 (jetty.xml)
-
增加IP 到域名的映射(/etc/hosts 文件)
-
修改 为相同的borkername
-
改为 replicated LevelDB (3个都配,这里列举一个)
<persistenceAdapter>
<replicatedLevelDB
directory="{activemq.data}/leveldb"
replicas="3"
bind="tcp://0.0.0.0:63631"
zkAddress="localhost:2191,localhost:2192,localhost:2193"
zkPassword="123456"
sync="local_disk"
zkPath="/activemq/leveldb-stores"
hostname="wh-mq-server"
/>
</persistenceAdapter>
- 修改activemq 端口 02 节点 =》 61617 03 节点 =》 61618
想要启动replica leavel DB 必须先启动所有的zookeper 服务,zookeper 的单机伪节点安装这里不细说了,主要说zookeper 复制三份后改配置文件,并让之自动生成 myid 文件,并将zk的端口改为之前表格中对应的端口 。这是conf 下的配置文件
其具体配置为:
tickTime=2000
initLimit=10
syncLimit=5
clientPort=2191 // 自行设置
server.1=192.168.17.3:2888:3888
server.2=192.168.17.3:2887:3887
server.3=192.168.17.3:286:3886
dataDir=/zk_server/data/log1 // 自行设置
我设置了三个,此时方便起见可以写批处理脚本
#!/bin/sh // 注意这个必须写在第一行
cd /zk_server/zk_01/bin
./zkServer.sh start
cd /zk_server/zk_02/bin
./zkServer.sh start
cd /zk_server/zk_03/bin
./zkServer.sh start
编写这个 zk_batch.sh 之后, 使用
chmod 700 zk_batch.sh
命令即可让它变为可执行脚本, ./zk_batch.sh start 即可 (即启动了三个zk 的服务)
同理可以写一个批处理关闭zk 服务的脚本和 批处理开启mq 服务 关闭 mq 服务的脚本。
完成上述之后连接zk 的一个客户端
./zkCli.sh -server 127.0.0.1:2191
连接之后:
表示连接成功
查看我的三个节点: 我的分别是 0…… 3 …… 4 …… 5
查看我的节点状态
get /activemq/leveldb-stores/00000000003
此次验证表明 00000003 的节点状态是master (即为63631 的那个mq 服务) 而其余的(00000004 00000005) activemq 的节点是 slave
如此集群顺利搭建成功 !
此次测试表明只有 8161 的端口可以使用 经测试只有 61 可以使用,也就是61 代表的就是master
9.2、测试集群可用性:
首先:
修改代码
public static final String ACTIVEMQ_URL = "failover:(tcp://192.168.17.3:61616,tcp://192.168.17.3:61617,tcp://192.168.17.3:61618)?randomize=false";
public static final String QUEUE_NAME = "queue_cluster";
测试:
测试通过连接上集群的 61616
MQ服务收到三条消息:
消息接收
MQ 服务也将消息出队
以上代表集群可以正常使用
此时真正的可用性测试:
杀死 8061 端口的进程 !!!
刷新页面后 8161 端口宕掉,但是 8162 端口又激活了
当 61616 宕机,代码不变发消息 自动连接到 61617 了
这样! 集群的可用性测试成功!
10、面试题
1. 引入消息队列如何保证其高可用
- 持久化
- 事务
- 签收
- 集群
2. 异步投递Asyn Sends
对于一个慢消费者,使用同步有可能造成堵塞,消息消费较慢时适合用异步发送消息
activemq 支持同步异步 发送的消息,默认异步。当你设定同步发送的方式和 未使用事务的情况下发持久化消息,这时是同步的。
如果没有使用事务,且发送的是持久化消息,每次发送都会阻塞一个生产者直到 broker 发回一个确认,这样做保证了消息的安全送达,但是会阻塞客户端,造成很大延时 。
在高性能要求下,可以使用异步提高producer 的性能。但会消耗较多的client 端内存,也不能完全保证消息发送成功。在 useAsyncSend = true 情况下容忍消息丢失。
// 开启异步投递
activeMQConnectionFactory.setUseAsyncSend(true);
如何在投递快还可以保证消息不丢失 ?
异步发送消息丢失的情况场景是: UseAsyncSend 为 true 使用 producer(send)持续发送消息,消息不会阻塞,生产者会认为所有的 send 消息均会被发送到 MQ ,如果MQ 突然宕机,此时生产者端尚未同步到 MQ 的消息均会丢失 。
故 正确的异步发送方法需要接收回调
同步发送和异步发送的区别就在于 :
同步发送send 不阻塞就代表消息发送成功
异步发送需要接收回执并又客户端在判断一次是否发送
在代码中接收回调的函数 :
activeMQConnectionFactory.setUseAsyncSend(true);
……
for (int i = 1; i < 4 ; i++) {
textMessage = session.createTextMessage("msg--" + i);
textMessage.setJMSMessageID(UUID.randomUUID().toString()+"-- orderr");
String msgid = textMessage.getJMSMessageID();
messageProducer.send(textMessage, new AsyncCallback() {
@Override
public void onSuccess() {
// 发送成功怎么样
System.out.println(msgid+"has been successful send ");
}
@Override
public void onException(JMSException e) {
// 发送失败怎么样
System.out.println(msgid+" has been failure send ");
}
});
}
3. 延迟投递和定时投递
① 在配置文件中设置定时器开关 为 true
② 代码编写
Java 代码中封装的辅助消息类型 ScheduleMessage
可以设置的 常用参数 如下:
long delay = 3 * 1000 ;
long perid = 4 * 1000 ;
int repeat = 7 ;
for (int i = 1; i < 4 ; i++) {
TextMessage textMessage = session.createTextMessage("delay msg--" + i);
// 消息每过 3 秒投递,每 4 秒重复投递一次 ,一共重复投递 7 次
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY,delay);
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD,perid);
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT,repeat);
messageProducer.send(textMessage);
}
4. ActiveMQ的消息重发机制
那些情况会引起消息的重发?
请说说重发时间间隔和次数?
默认:间隔:1s、重发次数:6
有毒消息Poison ACK 的理解?
死信队列的一些设置
修改消费代码块,当嫌6 次太多,设置为 3次
// 三次的意思是不计算本来发送的第一次 ,之后再次发送的第三次就被废弃
RedeliveryPolicy redeliveryPolicy = new RedeliveryPolicy();
redeliveryPolicy.setMaximumRedeliveries(3);
activeMQConnectionFactory.setRedeliveryPolicy(redeliveryPolicy);
在spring 中使用 死信机制
在业务逻辑中,如果一个订单系统没有问题,则使用正常的业务队列,当出现问题,则加入死信队列 ,此时可以选择人工干预还是机器处理 。
死信队列默认是全部共享的,但是也可以设置独立的死信队列
独立的死信队列配置
5.死信队列DLQ
什么时DLQ
死信队列配置
共享死信队列配置:
独立死信队列配置:
6、如何保证消息不被重复消费,幂等性的问题
- 如果消息是做数据库的插入操作,给这个消息一个唯一的主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据 。
- 如果不是,可以用redis 等的第三方服务,给消息一个全局 id ,只要消费过的消息,将 id ,message 以 K-V 形式写入 redis ,那消费者开始消费前,先去 redis 中查询有没消费的记录即可。