一、docker 安装 ActiveMQ
1.在docker环境中执行:
// 搜索activemq镜像
docker search activemq
// 拉取activemq镜像
docker pull webcenter/activemq
// 查看拉取后的activemq镜像
docker images
// 创建数据文件夹和日志文件夹
mkdir -p ./activemq/soft/activemq
mkdir -p ./activemq/soft/activemq/log
// docker执行命令,名称,后台启动,绑定端口,开机启动,数据卷绑定
docker run --name=activemq -itd -p 8161:8161 -p 61616:61616 --restart=always -v /home/docker/activemq/soft/activemq:/data/activemq -v /home/docker/activemq/soft/activemq/log:/var/log/activemq webcenter/activemq:latest
// 默认登陆用户名密码
用户名密码admin/admin
2.访问页面
访问路径:http://ip:8161。
二、ActiveMQ介绍
1.ActiveMQ基于JMS协议,组成部分:
JMS Provider:生产者,支持事务来保证发送可靠性。
JMS Message:JMS 的消息,主要类型有Text,Object,Map,Bytes,Stream五种类型。由消息头,消息属性,消息体组成。
JMS Consumer:消费者,支持事务和确认机制来保证消费可靠性。同步:使用recive()方法阻塞接受消息(客户端拉)。异步:使用监听方式接受消息(服务器推)。
JMS Domains:消息传递域,支持P2P(点对点传输),只存在一个队列,生产者发送到队列,消费者从队列中消费;支持pub/sub订阅消费模式,生产者发送到Topic,生产者订阅topic进行消费(消费者在订阅之前是收不到消息的。在订阅之后在线的状态可以收到消息,如果想离线后依然能接收到消息,需要设置成持久订阅)。
Connection Factory:连接工厂,创建连接,通过连接可以创建session对话,再创建生产者和消费者。(springboot中整合为jmstemplate模板,可以直接生产和消费消息);
JMS Connection:封装了客户与 JMS 提供者之间的一个虚拟的连接。
JMS Session:是生产者和消费者的一个单线程上下文。会话用于创建消息生产者(Producer)、消息消费者(Consumer),和消息(Message)等。会话提供了一个事务性的上下文,一组发送和接收被组合到了一个原子操作中。
2.消息结构
1.消息头:
属性 | 含义 |
---|---|
Destination | 目的地,主要是queue和topic |
DeliveryMode | 传递模式,在send或者jmstemplate中设置。分为持久模式和非持久模式 |
Expiration | 消息过期/到期时间,在send或者jmstemplate中设置 |
Priority | 消息优先级,有 0-9 十个级别,0-4是普通消息,5-9是加急消息。JMS 不要求 JMS Provider 严格按着十个优先级发送消息,但必须保证加急消息要先于普通消息到达。默认是第4级 |
MessageID | 由生产者自动分配,唯一的ID,以ID:开头 |
Timestamp | 生产者发送消息到消息call或者return返回的时间差 |
JMSType | JMS 消息类型的识别符 |
CorrelationID | JMS 相关性 id,由客户端设置。用来连接到另外一个消息,典型的应用是在回复消息中连接到原消息 |
ReplyTo | 回复,由客户端设置。提供本信息回复消息的目的地址 |
Redelivered | 重发,由JMS Provider(供应商)设置 |
2.消息体:JMS API 定义了 5 种消息体格式,也叫消息类型,可以使用不同的形式发送接收数据,并可以兼容现有的消息格式。包括:TextMessage、MapMessage、BytesMessage、StreamMessage 和 ObjectMessage。它们都是 Message 接口的子类。
3.消息属性:自定义属性,JMS定义的属性,供应商特点的属性。
三、ActiveMQ + SpringBoot的使用
ActiveMQ配置:
spring:
activemq:
broker-url: tcp://192.168.99.100:61616
user: admin
password: admin
in-memory: false # 基于外部mq模式
pool:
enable: true #开启链接池
max-connections: 10 #最大链接数
package com.zwfw.framework.activemq.config;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQSession;
import org.apache.activemq.RedeliveryPolicy;
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.config.JmsListenerContainerFactory;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.support.converter.MappingJackson2MessageConverter;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.converter.MessageType;
import javax.jms.DeliveryMode;
import javax.jms.Session;
@Configuration
public class ActivemqConfig {
@Value("${spring.activemq.broker-url}")
private String brokerUrl;
@Value("${spring.activemq.user}")
private String username;
@Value("${spring.activemq.password}")
private String password;
/**
* 消息重发策略配置
*/
@Bean
public RedeliveryPolicy redeliveryPolicy() {
RedeliveryPolicy redeliveryPolicy = new RedeliveryPolicy();
//是否在每次尝试重新发送失败后,增长这个等待时间
redeliveryPolicy.setUseExponentialBackOff(true);
//重发次数,默认为6次-设置为3次
redeliveryPolicy.setMaximumRedeliveries(3);
//重发时间间隔单位毫秒,默认为1秒
redeliveryPolicy.setInitialRedeliveryDelay(1000L);
//第一次失败后重新发送之前等待500毫秒,第二次失败再等待500 * 2毫秒
redeliveryPolicy.setBackOffMultiplier(2);
// 是否避免消息碰撞
redeliveryPolicy.setUseCollisionAvoidance(false);
// 设置重发最大拖延时间-1表示无延迟限制
redeliveryPolicy.setMaximumRedeliveryDelay(-1);
return redeliveryPolicy;
}
/**
* 消息工厂配置
*/
@Bean
public ActiveMQConnectionFactory activeMqConnectionFactory() {
ActiveMQConnectionFactory activeMqConnectionFactory = new ActiveMQConnectionFactory(username, password, brokerUrl);
activeMqConnectionFactory.setRedeliveryPolicy(redeliveryPolicy());
return activeMqConnectionFactory;
}
@Bean(name = "jmsTemplate")
public JmsTemplate jmsTemplate() {
JmsTemplate jmsTemplate = new JmsTemplate();
// 设置连接工厂
jmsTemplate.setConnectionFactory(activeMqConnectionFactory());
//deliveryMode, priority, timeToLive 的开关,要生效,必须配置为true,默认false
jmsTemplate.setExplicitQosEnabled(true);
//定义持久化后节点挂掉以后,重启可以继续消费 1表示非持久化,2表示持久化
jmsTemplate.setDeliveryMode(DeliveryMode.PERSISTENT);
/**
* 如果不启用事务,则会导致XA事务失效;
* 作为生产者如果需要支持事务,则需要配置SessionTransacted为true
*/
jmsTemplate.setSessionTransacted(false);
//消息的应答方式,需要手动确认,此时SessionTransacted必须被设置为false,且为Session.CLIENT_ACKNOWLEDGE模式
/**
* 当关闭事务时候,下面设置才有效
* Session.AUTO_ACKNOWLEDGE 消息自动签收
* Session.CLIENT_ACKNOWLEDGE 客户端调用acknowledge方法手动签收
* Session.DUPS_OK_ACKNOWLEDGE 不必必须签收,消息可能会重复发送
*/
jmsTemplate.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);
jmsTemplate.setMessageConverter(jacksonJmsMessageConverter());
return jmsTemplate;
}
/**
* topic模式的ListenerContainer
* topic下没有消息回执一说,确认消息之存在queue模式
* 浏览只是针对 Queue 的概念,Topic 没有浏览。浏览是指获取消息而消息依然保持在 broker 中,而消息的接收会把消息从 broker 中移除。
*/
@Bean
public JmsListenerContainerFactory<?> jmsListenerContainerTopic() {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setPubSubDomain(true);
factory.setConnectionFactory(activeMqConnectionFactory());
factory.setMessageConverter(jacksonJmsMessageConverter());
return factory;
}
/**
* queue模式的ListenerContainer
* 监听容器配置,使用jackson的消息转换器
* 1 不开启事务,手动确认,自动确认
* 2 开启事务,是自动应答,当客户端消费有异常抛出,会进行重试模式,按照上面重试配置次数重试后,如果还是失败,则会进入死信队列
* @return
*/
@Bean
public JmsListenerContainerFactory<?> jmsListenerContainerQueue() {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
// 关闭事务
factory.setSessionTransacted(false);
// 设置手动确认,默认配置中Session是开启了事务的,事务优先级大于客户端确认,即使我们设置了手动Ack也是无效的
factory.setSessionAcknowledgeMode(ActiveMQSession.INDIVIDUAL_ACKNOWLEDGE);
factory.setConnectionFactory(activeMqConnectionFactory());
factory.setMessageConverter(jacksonJmsMessageConverter());
return factory;
}
/**
* queue模式的ListenerContainer
* 监听容器配置,使用自带的消息转换器
*/
@Bean
public JmsListenerContainerFactory<?> jmsListenerContainerQueueNoConver() {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
// 关闭事务
factory.setSessionTransacted(false);
// 设置手动确认,默认配置中Session是开启了事物的,即使我们设置了手动Ack也是无效的
factory.setSessionAcknowledgeMode(ActiveMQSession.INDIVIDUAL_ACKNOWLEDGE);
factory.setConnectionFactory(activeMqConnectionFactory());
return factory;
}
/**
* 自定义消息转换器
* @return
*/
@Bean
public MessageConverter jacksonJmsMessageConverter() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
// MappingJackson2MessageConverter只支持TEXT和byte类型的转换,
// 见org.springframework.jms.support.converter.MappingJackson2MessageConverter.toMessage
converter.setTargetType(MessageType.TEXT);
// 可以为任何字符,但必需要配置,在下文中的setTypeIdOnMessage方法中会用上
converter.setTypeIdPropertyName("_type");
return converter;
}
}
queue和topic配置
package com.zwfw.framework.activemq.queue;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.command.ActiveMQTopic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.jms.Queue;
import javax.jms.Topic;
@Configuration
public class QueueConfig {
/**
* 声明普通队列
*/
@Bean
public Queue conmonQueue(){
return new ActiveMQQueue("common.queue");
}
/**
* 声明延时队列
*/
@Bean
public Queue delayQueue(){
return new ActiveMQQueue("delay.queue");
}
/**
* 声明广播类型队列
*/
@Bean
public Topic topicQueue(){
return new ActiveMQTopic("topic.queue");
}
}
上面是mq的基本配置,配置了几个监听容器:queue模式下的带事务和不带事务手动确认的容器和topic监听的容器,这些容器在后续监听类中配置用得上。
P2P模式(图是借鉴来的):
前言:该模式,点对点传输。生产者传输消息到队列,消费者从队列中消费。生产者可配置事务,如果开启事务发送,只有在commit之后,队列中才会有消息入列,如果rollback则不会进入队列;客户端一般有两种方式,一种是事务,第二种是确认机制:开启事务,不需要设置确认机制(事务优先级大于确认机制,设置了手动确认等也无效),默认就是自动确认,当事务方法中抛出异常,则消息不会被消费,会进入重试,重试次数到了之后,会进入activemq的DLQ队列(死信)。
生产者代码和说明:
@RestController
public class SendController {
@Autowired
private Queue conmonQueue;
@Autowired
private JmsTemplate jmsTemplate;
/**
* 单条数据发送,事务模式
*/
@RequestMapping("/commonQueue")
public void commonQueue() throws InterruptedException {
for (int i = 0; i < 20; i++) {
Book book = new Book();
book.setName("三体" + i).setAuthor("刘慈欣" + i).setType("科幻" + i);
// 设置事务模式发送
jmsTemplate.setSessionTransacted(true);
jmsTemplate.convertAndSend(conmonQueue,book);
if (i == 12) {
throw new RuntimeException();
}
}
}
/**
* 批量发送,事务模式
*/
@RequestMapping("/commonQueue/{num}")
public void commonQueueTranscate(@PathVariable("num") Integer num) throws Exception {
MessageProducer pd = null;
Session session = null;
Connection connection = null;
try {
ConnectionFactory connectionFactory = jmsTemplate.getConnectionFactory();
connection = connectionFactory.createConnection();
connection.start();
// 开启事务,只能设置AUTO_ACKNOWLEDGE,其他模式无效且不受控制
session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
pd = session.createProducer(conmonQueue);
for (int i = 0; i < 20; i++) {
Book book = new Book();
book.setName("三体" + i).setAuthor("刘慈欣" + i).setType("科幻" + i);
// 用内置的消息转换器
TextMessage message = session.createTextMessage(JSON.toJSONString(book));
// 用jackson的消息转化器
// Message message = jmsTemplate.getMessageConverter().toMessage(book, session);
pd.send(message);
System.out.println("send book" + i + " to queue");
}
// 测试判断,偶数提交,奇数回滚
if (num % 2 == 0) {
session.commit();
}else {
session.rollback();
}
} catch (JMSException e) {
throw new RuntimeException(e);
}finally {
pd.close();
session.close();
connection.close();
}
}
}
事务:使用springboot整合后,基本上是使用jmstemplate模版进行消息的发送。在事务环境下,好像只能发送单条并且发送成功后确认单条数据。这样数据量大的话感觉会影响效率。如果在批量发送环境下,用jmstemplate发送我还没有找到合适的方法 ………所以用session会话的模式,设置事务进行批量发送。
序列化:在配置文件中,定义了jackson的序列化方式,如果不定义,就是使用默认的org.springframework.jms.support.converter.SimpleMessageConverter.toMessage序列化。在里面根据你发送消息的类型来序列化。
如果使用的是自定义的jackson序列化,发送的jmstemplate模板也需要注入jackson的序列化配置,在监听容器配置中,也需要注入jackson的序列化配置。否则,如果发送的jmstemplate没有注入,或者用的会话模式session.createTextMessage来发送的消息(自带的序列化),在监听收到消息后会序列化失败,原因是,使用了jackson配置发送的消息,在内部会调用setTypeIdOnMessage,里面会塞入设置过的typeIdPropertyName。当消费者反序列化的时候,则会调用getJavaTypeForMessage方法,里面会判断有没有这个属性,如果没有则抛出异常。
总而言之,发送端和接收端的序列化配置必需同步。
当客户端监听配置的反序列化是jackson后,jmstemplate也要注入jackson配置。如果想要批量发送消息,可以使用下面的模版来构造一个消息对象,通过配置jackson后的jmstemplate方法是调用了setTypeIdOnMessage方法,在反序列化的时候不会出现上面异常问题:
Message message = jmsTemplate.getMessageConverter().toMessage(book, session);
消费者代码和说明:
@Component
public class ActiveListener {
/**
* 将开启事务时候,方法内有异常则不会确认消费
* 发生异常会进入重试模式,服务器按重试配置数推送,默认6次
* 重试还是失败,消息会进入死信队列
*/
@JmsListener(destination = "common.queue", containerFactory = "jmsListenerContainerQueue")
public void commonQueueListen(Book book, ActiveMQMessage message) throws Exception {
System.out.println(book);
// 手动确认消息,当开启事务时,此设置无效
message.acknowledge();
}
/**
* 使用内置序列化的配置的监听容器
*/
@JmsListener(destination = "common.queue", containerFactory = "jmsListenerContainerQueueNoConver")
public void commonQueueListen1(String book, ActiveMQMessage message) throws Exception {
System.out.println(book);
// 手动确认消息,当开启事务时,此设置无效
message.acknowledge();
}
}
当监听容器开启事务后, message.acknowledge()方法并没有作用,事务对应的是自动确认,不受控制。当监听容器关闭事务,应使用确认机制,一般手动确认,如果没有确认,则消息不会被消费。
分组和并发消费
并发消费:如果想在发送消息并且由多个消费者一起并发消费,可以通过设置配置文件中的concurrency属性,或者在@JmsListener中给注解属性concurrency设置数量,如下图。
分组消费:用于队列模式。分组消费需要在生产者发送的消息中设置消息头,使用setStringProperty来设置消息的头属性。在消费者端,在@JmsListener注解上添加消息头过滤 selector =“JMSXGroupID=‘groupB’”,就可以完成分组消费。
生产者:
@RequestMapping("/groupQueue")
public void groupQueue() throws InterruptedException {
for (int i = 0; i < 20; i++) {
Book book = new Book();
book.setName("三体" + i).setAuthor("刘慈欣" + i).setType("科幻" + i);
jmsTemplate.setSessionTransacted(true);
// 可以用session会话模式生成的message来设置消息头,我这里用模版发送,在生成message同时塞入属性
jmsTemplate.send(conmonQueue, session -> {
Message message = jmsTemplate.getMessageConverter().toMessage(book, session);
message.setStringProperty("JMSXGroupID","groupA");
return message;
});
}
}
消费者:
/**
* 分组
* selector,过滤头属性
* concurrency,并发数量,可以生成多个消费者
*/
@JmsListener(destination = "common.queue", containerFactory = "jmsListenerContainerQueue", selector ="JMSXGroupID='groupA'")
public void commonQueueListenGroupA(Book book, ActiveMQMessage message) throws Exception {
System.out.println("groupA: " + book);
// 手动确认消息,当开启事务时,此设置无效
message.acknowledge();
}
/**
* 分组
* selector,过滤头属性
* concurrency,并发数量,可以生成多个消费者
*/
@JmsListener(concurrency = "10", destination = "common.queue", containerFactory = "jmsListenerContainerQueue", selector ="JMSXGroupID='groupB'")
public void commonQueueListenGroupB(Book book, ActiveMQMessage message) throws Exception {
System.out.println("groupB: " + book);
// 手动确认消息,当开启事务时,此设置无效
message.acknowledge();
}
pub/sub模式(图是借鉴来的):
topic模式待完善。。。。
四、解决的问题
此文主要是提供ActiveMQ基本的配置,生产者事务,消费者事务和确认机制,生产者和消费者的序列化问题。更详细的介绍网上有很多资料可以查阅。主要是自己学习的时候,网上的代码都没有很好的解决问题,所以在这里记录一下。
- 序列化和反序列化必需同步:如果使用原生的session来发送消息是没有配置序列化的(应该也是可以设置自定义的序列化配置的),监听类也需要用自带的默认的序列化方式来接受对象。如果使用了jackson序列化方式,在配置类中,jmstemplate和监听容器都要注入此配置,并且发送的时候需要用带有jackson配置的jmstemplate来发送消息,否则消息会序列化失败。
- 生产者的事务:如果用jmstemplate来发送消息,跟读源码会发现,如果设置setSessionTransacted为true,则会代理生成一个事务,并且发送了会commit。在网上用模版批量发送事务消息的例子。于是就用的session会话来控制批量消息事务的控制。如果要用自定义序列化,则可以用“Message message = jmsTemplate.getMessageConverter().toMessage(book, session);”来构造一个含有jackson序列化配置的消息。
- 消费者的事务和确认机制:当事务设置为true的时候,和生产者一样,在配置确认机制就不生效了。因为默认就是自动确认。当发生异常事务回滚,会进行重试阶段,重试后如果失败,则会进入死信队列。当事务设置为false的时候,一般设置的是手动确认,只有确认后的数据才会出队,否则数据会一直存在队列里面(排除定时的消息)。
- 批量事务:用模版方法发送貌似只能一条条的来,批量的话得用session来发送,并commit;
- topic的代码还没有实现,还在学习中,后续补上。。。