一、MQ前置知识
1、什么是消息中间件?
答:是做客户端与服务器端进行异步通讯的。
2、消息中间件的通讯模式有?
答:点对点、发布与订阅。
3、如果生产者发送几万个消息?消息中间件会宕机掉吗?
答:不会,因为MQ本身就拥有解决高并发能力。
4、什么是JMS?
答:JMS是java消息服务应用程序接口,是java面向消息中间件的API。
JMS是java面向消息中间件的API。
二、ActivityMQ设置持久化机制
消息中间件的持久化:默认消息中间件是没有持久化。
做持久化是:生产者持久化。
只有把MQ停掉了,没有设置持久化的队列数据就会被清空。
设置持久化是在生产者这边设置持久化:
代码:
package com.leeue.activemq.test01;
import javax.jms.Connection;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
/**
* @classDesc: 功能描述:(这是一个生产者 使用activityMQ)
* @author:<a href="leeue@foxmail.com">李月</a>
* @Version:v1.0
* @createTime:2018年9月21日 下午5:51:45
*/
public class ProducerTest {
public static void main(String[] args) throws JMSException {
//1、获取mq连接工程
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_USER,
ActiveMQConnection.DEFAULT_PASSWORD,"tcp://127.0.0.1:61616"); // 这个是MQ真正的后台通讯地址。
//2、创建连接
Connection createConnection = activeMQConnectionFactory.createConnection();
//3、启动连接
createConnection.start();
//4、创建会话工厂 设置成默认的
Session session = createConnection.createSession(Boolean.FALSE,Session.AUTO_ACKNOWLEDGE);
//5.创建队列
Destination destination = session.createQueue("leeueMsg");
//6. 得到生产者
MessageProducer producer = session.createProducer(destination);
/* //7.开始生产 这里设置成不持久化
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);*/
// 设置成持久化模式
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
for(int i = 0 ; i < 5; i++) {
senMsg(session, producer, "我是生产者:"+i);
}
System.out.println("生产者发送消息完毕...");
}
public static void senMsg(Session session, MessageProducer producer,String msg) throws JMSException {
//1.创建文本消息
TextMessage textMessage = session.createTextMessage("Hello MQ"+msg);
//2.发送消息
producer.send(textMessage);
}
}
所以使用MQ的时候,一定要将消息设置成持久化
三、JMS可靠消息 ActivityMQ保证可靠消息
可靠消息:就是拿到消息后可以使用。
JMS可靠消息 在ActivityMQ中有:
1、自动签收。-- 不安全
2、事务签收。
生产者,完成消息发送完成后,必须提交给队列,
如果没有提交操作,队列中是不存在这个消息的。
消费者:获取事务消息后,如果消费者没有提交事务,
默认消费者没有进行消费。-- 默认自动重试机制。
3、手动签收。
消费者没有手动签收消息,默认表示没有进行消费。
手动签收案例
生产者:
package com.leeue.activemq.test01;
import javax.jms.Connection;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
/**
* @classDesc: 功能描述:(这是一个生产者 使用activityMQ)
* @author:<a href="leeue@foxmail.com">李月</a>
* @Version:v1.0
* @createTime:2018年9月21日 下午5:51:45
*/
public class ProducerTest {
public static void main(String[] args) throws JMSException {
//1、获取mq连接工程
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_USER,
ActiveMQConnection.DEFAULT_PASSWORD,"tcp://127.0.0.1:61616"); // 这个是MQ真正的后台通讯地址。
//2、创建连接
Connection createConnection = activeMQConnectionFactory.createConnection();
//3、启动连接
createConnection.start();
//4、创建会话工厂 设置成默认的
// Session session = createConnection.createSession(Boolean.FALSE,Session.AUTO_ACKNOWLEDGE);
//设置成手动签收
Session session = createConnection.createSession(Boolean.FALSE,Session.CLIENT_ACKNOWLEDGE);
//5.创建队列
Destination destination = session.createQueue("leeueMsg");
//6. 得到生产者
MessageProducer producer = session.createProducer(destination);
/* //7.开始生产 这里设置成不持久化
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);*/
// 设置成持久化模式
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
for(int i = 0 ; i < 5; i++) {
senMsg(session, producer, "我是生产者:"+i);
}
System.out.println("生产者发送消息完毕...");
}
public static void senMsg(Session session, MessageProducer producer,String msg) throws JMSException {
//1.创建文本消息
TextMessage textMessage = session.createTextMessage("Hello MQ"+msg);
//2.发送消息
producer.send(textMessage);
}
}
消费者:
package com.leeue.activemq.test01;
import javax.jms.Connection;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
/**
* @classDesc: 功能描述:(这是个消费者)
* @author:<a href="leeue@foxmail.com">李月</a>
* @Version:v1.0
* @createTime:2018年9月22日 下午12:25:29
*/
public class Consumer001 {
public static void main(String[] args) throws JMSException {
// 1、获取mq连接工程
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(
ActiveMQConnection.DEFAULT_USER, ActiveMQConnection.DEFAULT_PASSWORD, "tcp://127.0.0.1:61616"); // 这个是MQ真正的后台通讯地址。
// 2、创建连接
Connection createConnection = activeMQConnectionFactory.createConnection();
// 3、启动连接
createConnection.start();
// 4、创建会话工厂 设置成默认的
/* Session session = createConnection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);*/
// 设置成手动签收
Session session = createConnection.createSession(Boolean.FALSE, Session.CLIENT_ACKNOWLEDGE);
// 5.创建队列
Destination destination = session.createQueue("leeueMsg");
// 6. 创建一个消费者
MessageConsumer consumer = session.createConsumer(destination);
// 7.建立一个while循环一直监听消息
while (true) {
// 监听消息 因为刚刚消费者传送的报文就是 TextMessage格式的,所以这边强转就行了.
TextMessage textMessage = (TextMessage) consumer.receive();
if (textMessage != null) {
System.out.println("消费者获取到消息:" + textMessage.getText());
//手动签收,只有这样才是真正的被消费到了。
textMessage.acknowledge();
} else {
break;
}
}
System.out.println("消费者消费完毕...");
}
}
事务签收案例
生产者:
package com.leeue.activemq.test01;
import javax.jms.Connection;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
/**
* @classDesc: 功能描述:(这是一个生产者 使用activityMQ)
* @author:<a href="leeue@foxmail.com">李月</a>
* @Version:v1.0
* @createTime:2018年9月21日 下午5:51:45
*/
public class ProducerTest {
public static void main(String[] args) throws JMSException {
//1、获取mq连接工程
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_USER,
ActiveMQConnection.DEFAULT_PASSWORD,"tcp://127.0.0.1:61616"); // 这个是MQ真正的后台通讯地址。
//2、创建连接
Connection createConnection = activeMQConnectionFactory.createConnection();
//3、启动连接
createConnection.start();
//4、创建会话工厂 设置成默认的
// Session session = createConnection.createSession(Boolean.FALSE,Session.AUTO_ACKNOWLEDGE);
//4、设置成手动签收
/*Session session = createConnection.createSession(Boolean.FALSE,Session.CLIENT_ACKNOWLEDGE);*/
/**设置成事务签收**/
Session session = createConnection.createSession(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
//5.创建队列
Destination destination = session.createQueue("leeueMsg");
//6. 得到生产者
MessageProducer producer = session.createProducer(destination);
/* //7.开始生产 这里设置成不持久化
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);*/
// 设置成持久化模式
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
for(int i = 0 ; i < 5; i++) {
senMsg(session, producer, "我是生产者:"+i);
//事务签收需要手动提交到MQ
session.commit();
}
System.out.println("生产者发送消息完毕...");
}
public static void senMsg(Session session, MessageProducer producer,String msg) throws JMSException {
//1.创建文本消息
TextMessage textMessage = session.createTextMessage("Hello MQ"+msg);
//2.发送消息
producer.send(textMessage);
}
}
消费者事务签收:
package com.leeue.activemq.test01;
import javax.jms.Connection;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
/**
* @classDesc: 功能描述:(这是个消费者)
* @author:<a href="leeue@foxmail.com">李月</a>
* @Version:v1.0
* @createTime:2018年9月22日 下午12:25:29
*/
public class Consumer001 {
public static void main(String[] args) throws JMSException {
// 1、获取mq连接工程
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(
ActiveMQConnection.DEFAULT_USER, ActiveMQConnection.DEFAULT_PASSWORD, "tcp://127.0.0.1:61616"); // 这个是MQ真正的后台通讯地址。
// 2、创建连接
Connection createConnection = activeMQConnectionFactory.createConnection();
// 3、启动连接
createConnection.start();
// 4、创建会话工厂 设置成默认的
/* Session session = createConnection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);*/
// 设置成手动签收
// Session session = createConnection.createSession(Boolean.FALSE, Session.CLIENT_ACKNOWLEDGE);
Session session = createConnection.createSession(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
// 5.创建队列
Destination destination = session.createQueue("leeueMsg");
// 6. 创建一个消费者
MessageConsumer consumer = session.createConsumer(destination);
// 7.建立一个while循环一直监听消息
while (true) {
// 监听消息 因为刚刚消费者传送的报文就是 TextMessage格式的,所以这边强转就行了.
TextMessage textMessage = (TextMessage) consumer.receive();
if (textMessage != null) {
System.out.println("消费者获取到消息:" + textMessage.getText());
/* //手动签收,只有这样才是真正的被消费到了。
textMessage.acknowledge();*/
session.commit();//每消费一个消息都提交下事务。表示该消息已经被消费,要从队列里面移除掉。
} else {
break;
}
}
System.out.println("消费者消费完毕...");
}
}
四、SpringBoot整合ActivityMQ
1、pom.xml文件
<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.leeue</groupId>
<artifactId>034_springBoot_ActivityMQ</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- spring boot web支持:mvc,aop... -->
<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>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、application.yml文件
spring:
activemq:
broker-url: tcp://127.0.0.1:61616
user: admin
password: admin
# 这个是当前这个队列 生产者
queue: springboot-queue
server:
port: 8080
3、ActivityMQ注册文件:
package com.leeue;
import javax.jms.Queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.command.ActiveMQQueue;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.core.JmsTemplate;
/**
* @classDesc: 功能描述:(MQ的配置加载类)
* @author:<a href="leeue@foxmail.com">李月</a>
* @Version:v1.0
* @createTime:2018年9月27日 下午3:34:33
*/
@Configuration
public class QueueConfig {
@Value("${queue}")
private String queue;
@Bean
public Queue logQueue() {
return new ActiveMQQueue(queue);
}
@Bean
public JmsTemplate jmsTemplate(ActiveMQConnectionFactory activeMQConnectionFactory, Queue queue) {
JmsTemplate jmsTemplate = new JmsTemplate();
jmsTemplate.setDeliveryMode(2);// 进行持久化配置 1表示非持久化,2表示持久化</span>
jmsTemplate.setConnectionFactory(activeMQConnectionFactory);
jmsTemplate.setDefaultDestination(queue); // 此处可不设置默认,在发送消息时也可设置队列
jmsTemplate.setSessionAcknowledgeMode(4);// 客户端签收模式</span>
return jmsTemplate;
}
// 定义一个消息监听器连接工厂,这里定义的是点对点模式的监听器连接工厂
@Bean(name = "jmsQueueListener")
public DefaultJmsListenerContainerFactory jmsQueueListenerContainerFactory(
ActiveMQConnectionFactory activeMQConnectionFactory) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(activeMQConnectionFactory);
// 设置连接数
factory.setConcurrency("1-10");
// 重连间隔时间
factory.setRecoveryInterval(1000L);
factory.setSessionAcknowledgeMode(4);
return factory;
}
}
@Configuration:是SpringBoot用于定义配置类的注解,来替代原来的xml文件。
被注解的类内部会有多个@Bean注解方法,这些方法将会被
AnnotationConfigApplicationContext或
AnnotationConfigWebApplicationContext
类进行扫描,并用于bean的定于,初始化Spring容器。
生产者代码:
package com.leeue;
import javax.jms.Queue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@SpringBootApplication //
@Component //将这个加入Spring容器
@EnableScheduling //启动SpringBoot定时任务
public class Producer {
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate; //消息模版
@Autowired
private Queue queue;
@Scheduled(fixedDelay=3000) //这个是一个定时任务,每隔3秒就调用这个方法一次
public void send() {
jmsMessagingTemplate.convertAndSend(queue,System.currentTimeMillis()+"activityMQ测试消息");
System.out.println("MQ发送了消息......");
}
public static void main(String[] args) {
SpringApplication.run(Producer.class, args);
}
}
消费者代码:
package com.leeue;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
@Component //注入到Spring容器里面去
@SpringBootApplication
public class Consumer {
@JmsListener(destination="${queue}") //这里写你要监听的队列名称
public void receive(String msg) {
System.out.println("消费端接受到生产者:"+msg);
}
public static void main(String[] args) {
SpringApplication.run(Consumer.class,args);
}
}
五、使用ActivityMQ的注意事项
ActivityMQ在消费者出现异常的时候,会默认重试发送消息。
1、消费者获取到消息之后抛出jdbc连接异常?
需要重试,调用第三方接口时,接口暂时无法访问
2、消费者获取到消息之后抛出数据转换异常? 不需要重试,
需要程序员来修改代码,发版后才能修复这个错误
注意事项:如果消费者代码需要发布版本进行解决的问题,不要重试。
使用try-catch,在catch在日志中记录消息报文,可以采用补偿机制
,使用人工进行补偿。使用定时JOB定时去检查脏数据。
方案解决:日志中记录消息报文+定时job去检查脏数据+人工进行补偿。
ActivityMQ默认重试:
六、解决消息中间件幂等性
消费者手动签收的写法:
package com.leeue;
import java.util.concurrent.CountDownLatch;
import javax.jms.TextMessage;
import org.apache.activemq.filter.function.inListFunction;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
@Component //注入到Spring容器里面去
@SpringBootApplication
public class Consumer {
int count = 0;
@JmsListener(destination="${queue}") //这里写你要监听的队列名称
public void receive(TextMessage textMessage) {
try {
String msg = textMessage.getText();//获取消息报文
System.out.println("消费端接受到生产者:"+msg +"当前重试了"+(++count));
int i = 1/0;
textMessage.acknowledge();//手动签收
} catch (Exception e) {
//在catch日志记录消息报文,可以采用补偿机制(使用人工进行补偿) 采用定时JOB健康检查脏数据。
}
}
public static void main(String[] args) {
SpringApplication.run(Consumer.class,args);
}
}
消费者异常的时候手动重试写法:
package com.leeue;
import java.util.concurrent.CountDownLatch;
import javax.jms.JMSException;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.filter.function.inListFunction;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
@Component //注入到Spring容器里面去
@SpringBootApplication
public class Consumer {
int count = 0;
@JmsListener(destination="${queue}") //这里写你要监听的队列名称
public void receive(TextMessage textMessage,Session session) throws JMSException {
try {
String msg = textMessage.getText();//获取消息报文
System.out.println("消费端接受到生产者:"+msg +"当前重试了"+(++count));
int i = 1/0;
textMessage.acknowledge();//手动签收
} catch (Exception e) {
//在catch日志记录消息报文,可以采用补偿机制(使用人工进行补偿) 采用定时JOB健康检查脏数据。
session.recover(); //手动重试
}
}
public static void main(String[] args) {
SpringApplication.run(Consumer.class,args);
}
}
消费者没有及时签收的情况下,MQ有自动重试机制,会造成重复消费。
解决MQ幂等性问题:使用全局ID区分消息。消息ID是唯一的。
1、使用全局ID+redis表 你的业务来解决。
2、使用消息ID
3、使用业务逻辑唯一ID
以上三个ID都是唯一的。 避免第三次重试。
MQ幂等性问题是在网络延迟、或者其他情况下,产生重试机制的时候,
才会产生幂等性问题发送。(也就是重复消费问题)。
七、消费者集群
消费者集群生产者发送消息的时候,是不会产生重复消费的。
因为生产者有自己的记忆。不需要担心幂等性问题。
如果MQ集群,需要考虑幂等性问题。如果生产者集群:考虑幂等性问题。