安装
前置条件
- activemq的运行依赖于jdk,需要
提前安装jdk
- 如果已经安装了jdk,需要根据jdk的版本来选择对应的版本进行安装activemq
- 版本对应在官网上,使用
java -version
看jdk的版本- 注意:jdk和mq的版本不一致会报错,电脑的命名
不能用中文
启动
- 有的版本bin目录会有2个目录win32和win64,有的只有win64
- 打开bin目录
- 点击activemq.bat,访问网址 http://127.0.0.1:8161/admin/
使用服务进行启动
- 上述方式使用bat文件启动,只要cmd窗口一关,就会关闭
- 自带安装服务和卸载服务的bat文件(使用
管理员
权限运行)
关闭或开启验证
- 更改完配置之后记得
重启
一下服务
消息中间件的应用场景
- 异步处理
- 应用解耦
- 流量削峰
场景
用户注册需要三个功能:写数据库、发送邮件、注册短信
异步处理
串行
并行
消息中间件
对比
应用解耦
流量削峰
中间件对比
- activemq:java、万级吞吐量、毫秒级响应速度、可用性高(主从架构),很多公司在用
- rabbitmq:erlang,其他同上,管理界面丰富
- rocketmq:java,10万级别,可用性很高(分布式)
- kafka:消息查询和追溯没有提供,大数据应用广泛
jms消息模型
- 两种模型
- P2P(point to point):点对点模型(queue模型)
- P/S(publish/subscribe):发布订阅模型(topic模型)
点对点
生产者
和消费者
之间的消息往来
特点
- 每个消息只能被
一个
消费者消费,消费之后就直接消失了- 生产者和消费者没有直接
依赖
关系,不管消费者有没有运行,都不妨碍生产者将消息发给队列- 消费者成功接受消息之后需要向队列回应
应答
成功
发布订阅模型
三个
角色
- 发布者
- 订阅者
- 主题
- 发布者将消息发布到
主题
,只有订阅了主题的订阅者才能接收到消息topic
来实现消息的发布- 当一个消息被发布,
n
个订阅者都可以得到消息的拷贝
特点
- 一个主题可以被
多个
订阅者订阅- 存在时间的
先后顺序
,消费者需要先订阅
,生产者才能发布消息- 订阅者必须保持
持续运行
状态,才能接收到生产者的消息
JMS API
访问地址
- http协议:http://127.0.0.1:8161/(网页
监控
)- tcp协议:tcp://127.0.0.1:61616(java
后端
访问)
JMS原始方式
点对点queue
引入依赖
log4j
的依赖也需要引入一下
<dependencies>
<!-- 引入原生的依赖 5.18版本不受activemq版本的影响 -->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>6.1.0</version>
</dependency>
<!-- 引入log4j的依赖,不引入会报错 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.17.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.1</version>
</dependency>
</dependencies>
生产者
- 后端使用的端口是
tcp://localhost:61616
import jakarta.jms.*;
import org.apache.activemq.ActiveMQConnectionFactory;
// 队列生产者 点对点
public class Producer {
public static void main(String[] args) throws JMSException {
// 1. 创建连接工厂 默认使用tcp协议
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
// 2. 创建连接
Connection connection = activeMQConnectionFactory.createConnection();
// 3. 启动连接
connection.start();
// 4. 创建会话
// 第一个参数:是否开启事务
// 第二个参数:消息确认机制
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 5. 创建队列
Queue queue = session.createQueue("queue01");
// 6. 创建消息生产者
MessageProducer producer = session.createProducer(queue);
// 7. 创建消息 createTextMessage创建文本消息
TextMessage textMessage = session.createTextMessage("Hello, ActiveMQ!");
// 8. 发送消息
producer.send(textMessage);
// 9. 关闭资源 从后往前关闭
session.close();
connection.close();
}
}
下图表示发送消息成功
消费者
一般方式(不常用)
- 步骤和生产者如出一辙,就是最后变成了
receive
方法- 使用
while true
,使得消费者一直处在等待
中
// 点对点消费者
public class Consumer {
public static void main(String[] args) throws JMSException {
//1. 创建连接工厂
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
//2. 创建连接
Connection connection = connectionFactory.createConnection();
//3. 启动连接
connection.start();
//4. 创建会话
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//5. 指定队列
Queue queue = session.createQueue("queue01");
//6. 创建消息消费者
MessageConsumer consumer = session.createConsumer(queue);
//7. 接受并且消费消息
//receive()是阻塞方法,如果没有消息会一直等待
// 一般不会关闭消费者,因为一旦关闭就不能接收消息了
while (true) {
Message textMessage = consumer.receive();
if (textMessage == null) {
break;
}
//判断消息类型
if (textMessage instanceof TextMessage) {
TextMessage message = (TextMessage) textMessage;
System.out.println("接收到消息:" + message.getText());
}
}
}
}
监听器(推荐使用)
使用监听器,更加优雅
public class Listener {
public static void main(String[] args) throws JMSException {
//1. 创建连接工厂
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
//2. 创建连接
Connection connection = connectionFactory.createConnection();
//3. 启动连接
connection.start();
//4. 创建会话
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//5. 指定队列
Queue queue = session.createQueue("queue01");
//6. 创建消息消费者
MessageConsumer consumer = session.createConsumer(queue);
//7. 设置监听器 匿名内部类
consumer.setMessageListener(new MessageListener() {
// 重写onMessage方法 接收消息
@Override
public void onMessage(Message message) {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("接收到消息:" + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
});
}
}
发布订阅模式
生产者
- 和点对点十分相似,唯一不同使用的是
createTopic
// 主题生产者
public class Producer {
public static void main(String[] args) throws JMSException {
// 1. 创建连接工厂 默认使用tcp协议
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
// 2. 创建连接
Connection connection = activeMQConnectionFactory.createConnection();
// 3. 启动连接
connection.start();
// 4. 创建会话
// 第一个参数:是否开启事务
// 第二个参数:消息确认机制
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 5. 创建主题
Topic topic = session.createTopic("topic01");
// 6. 创建消息生产者
MessageProducer producer = session.createProducer(topic);
// 7. 创建消息 createTextMessage创建文本消息
session.createTextMessage("Hello, ActiveMQ!");
// 8. 发送消息
producer.send(topic, session.createTextMessage("Hello, ActiveMQ!"));
// 9. 关闭资源 从后往前关闭
session.close();
connection.close();
}
}
消费者
这里是引用
// 主题消费者
public class Consumer {
public static void main(String[] args) throws JMSException {
// 1. 创建连接工厂 默认使用tcp协议
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
// 2. 创建连接
Connection connection = activeMQConnectionFactory.createConnection();
// 3. 启动连接
connection.start();
// 4. 创建会话
// 第一个参数:是否开启事务
// 第二个参数:消息确认机制
// 一般使用自动确认
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 5. 创建主题
Topic topic = session.createTopic("topic01");
// 6. 创建消息消费者
MessageConsumer consumer = session.createConsumer(topic);
// 7. 设置监听器 匿名内部类
consumer.setMessageListener(new MessageListener() {
// 重写onMessage方法 接收消息
@Override
public void onMessage(Message message) {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("接收到消息:" + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
});
}
}
整合springboot项目
- 新建项目,导入依赖
<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>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
配置application.yml
# 服务端口
server:
port: 9001
spring:
application:
name: spring-active-mq-producer
activemq:
broker-url: tcp://localhost:61616
user: admin
password: admin
jms:
# 是否是发布订阅模式
pub-sub-domain: false
aaa:
bbb: ccc23
生产者
发送消息
测试
@SpringBootTest(classes = ActiveMqBootApplication.class)
class ActiveMqBootApplicationTests {
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
@Value("${aaa.bbb}")
private String name;
@Test
void contextLoads() {
System.out.println("Test");
System.out.println(name);
}
@Test
void testSend() {
//参数一:目的地名称(队列或者主题名称)
//参数二:消息
jmsMessagingTemplate.convertAndSend(name, "hello, activemq");
}
}
消费者
监听类位置
- 把监听类放到启动类的同一目录下,或者开启包扫描模式
监听类
这个类被扫描到之后,会自动启动监听
// 加入到spring容器中
@Component
public class MyListener implements InitializingBean {
@Value("#'{aaa.bbb}'")
private String name;
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("name: " + name);
}
// 监听消息 监听的队列或者是主题
@JmsListener(destination = "${aaa.bbb:ccc}")
public void receiveMessage(Message message){
if(message instanceof TextMessage){
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("接收到消息:"+textMessage.getText());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
JMS消息组成
JMS协议组成如下
JMS Message 由三个部分组成
- 消息头
- 消息体
- 消息属性
消息头
红色是
重要
的消息头
上述的属性:大部分是不可以修改的,只有
少部分可以修改
:
- JMSCorrelationID
- JMSReplyTo
- JMSType
红色的是修改成功的,绿色的是修改失败的
发送方更改属性
@Autowired
private JmsTemplate jmsTemplate;
@Test
void testSend2() {
//参数一:目的地名称(队列或者主题名称)
//参数二:消息
jmsTemplate.send(name, session -> {
Message message = session.createTextMessage("hello activemq");
// 这些值都没法设置
message.setJMSMessageID("CustomMessageID");
message.setJMSExpiration(10000);
message.setJMSPriority(9);
// 设置消息属性
message.setJMSCorrelationID("CustomCorrelationID");
message.setJMSType("CustomType");
message.setJMSReplyTo(session.createQueue("CustomReplyTo"));
return message;
});
}
接收方获取已经更改完毕的属性
// 监听消息 监听的队列或者是主题
@JmsListener(destination = "${aaa.bbb:ccc}")
public void receiveMessage(Message message) {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
//获取更改过的消息头信息
System.out.println("消息ID:" + textMessage.getJMSMessageID());
System.out.println("消息过期时间:" + textMessage.getJMSExpiration());
System.out.println("消息优先级:" + textMessage.getJMSPriority());
//获取消息属性
System.out.println("消息属性:" + textMessage.getJMSCorrelationID());
System.out.println("消息属性:" + textMessage.getJMSType());
System.out.println("消息属性:" + textMessage.getJMSReplyTo());
System.out.println("接收到消息:" + textMessage.getText());
} catch (Exception e) {
e.printStackTrace();
}
}
}
有些属性已经更改完毕,有些没有
消息体
消息体有五中类型,其中三种比较常用
TextMessage类型
文本是最常见的消息类型了
// 发送TextMessage
@Test
void testSendTextMessage() {
jmsTemplate.send(name, session -> {
Message message = session.createTextMessage("发送的是TextMessage");
return message;
});
}
接收方
@JmsListener(destination = "${aaa.bbb:ccc}")
public void receiveMessage(Message message) {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
System.out.println("接收到消息:" + textMessage.getText());
}
}
map类型(不常用)
发送方
// 发送mapMessage
@Test
void testSendMapMessage() {
jmsTemplate.send(name, session -> {
MapMessage message = session.createMapMessage();
try {
message.setString("name", "张三");
message.setInt("age", 20);
} catch (JMSException e) {
e.printStackTrace();
}
return message;
});
}
接收方
@JmsListener(destination = "${aaa.bbb:ccc}")
public void receiveMapMessage(Message message) {
// 如果是MapMessage
if (message instanceof MapMessage) {
MapMessage mapMessage = (MapMessage) message;
try {
System.out.println("第一个参数:" + mapMessage.getString("name"));
System.out.println("第二个参数:" + mapMessage.getInt("age"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
ObjectMessage类型
object类型是不被信任的,发送之前先增加如下配置,否则会报错
spring:
application:
name: spring-active-mq-producer
activemq:
broker-url: tcp://localhost:61616
user: admin
password: admin
packages:
trust-all: true # 是否信任所有包
实体类(必须可序列化)
@Data
public class User implements Serializable {
private String name;
private Integer age;
}
发送方
// 发送ObjectMessage
@Test
void testSendObjectMessage() {
jmsTemplate.send(name, session -> {
User user = new User();
user.setName("张三");
user.setAge(206);
return session.createObjectMessage(user);
});
}
接收方
如果是ObjectMessage 需要强转 才能使用getObject
方法
// 监听消息 objectMessage
@JmsListener(destination = "${aaa.bbb:ccc}")
public void receiveObjectMessage(Object message) {
if (message instanceof ObjectMessage) {
// 如果是ObjectMessage 需要强转 才能使用getObject方法
ObjectMessage objectMessage = (ObjectMessage) message;
try {
User user = (User) objectMessage.getObject();
System.out.println("接收到消息:" + user);
} catch (Exception e) {
e.printStackTrace();
}
}
}
BytesMessage类型
- 读取文件
- 创建输入流
- 使用输入流将文件写入到字节数组中
- 将字节数组写入到 bytesMessage
// 发送bytesMessage
@Test
void testSendBytesMessage() {
jmsTemplate.send(name, session -> {
BytesMessage bytesMessage = session.createBytesMessage();
//1. 读取文件 获取当前项目根目录的text1.txt文件
File file = new File("text1.txt");
FileInputStream fileInputStream = null;
//2 创建输入流
try {
fileInputStream = new FileInputStream(file);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
//3. 读取文件内容
byte[] bytes = new byte[(int) file.length()];
try {
fileInputStream.read(bytes);
} catch (Exception e) {
throw new RuntimeException(e);
}
//4. 将字节流数组写入到bytesMessage
try {
bytesMessage.writeBytes(bytes);
} catch (JMSException e) {
throw new RuntimeException(e);
}
return bytesMessage;
});
}
接受方的操作和发送方法反着来就行
getBodyLength
获取长度创建字节数组- readBytes 将消息读取到
字节数组
中
// 监听消息 ByteMessage
@JmsListener(destination = "${aaa.bbb:ccc}")
public void receiveByteMessage(Message message) {
if (message instanceof BytesMessage) {
BytesMessage bytesMessage = (BytesMessage) message;
try {
// 1. 获取消息的长度 字节数组
byte[] bytes = new byte[(int) bytesMessage.getBodyLength()];
// 2. 读取消息
bytesMessage.readBytes(bytes);
// 3. 文件输出流
FileOutputStream fileOutputStream = new FileOutputStream("text2.txt");
// 4. 写入文件
fileOutputStream.write(bytes);
} catch (Exception e) {
e.printStackTrace();
}
}
}
StreamMessage类型(不常用)
和
TextMessage
不同,能发送任何类型
的消息
发送方
// 发送StreamMessage
@Test
void testSendStreamMessage() {
jmsTemplate.send(name, session -> {
StreamMessage streamMessage = session.createStreamMessage();
try {
streamMessage.writeBoolean(true);
streamMessage.writeLong(4L);
} catch (JMSException e) {
throw new RuntimeException(e);
}
return streamMessage;
});
}
接收方
// 监听消息 streamMessage
@JmsListener(destination = "${aaa.bbb:ccc}")
public void receiveStreamMessage(Message message) {
if (message instanceof StreamMessage) {
StreamMessage streamMessage = (StreamMessage) message;
try {
System.out.println("第一个消息:" + streamMessage.readBoolean());
System.out.println("第二个消息:" + streamMessage.readLong());
} catch (Exception e) {
e.printStackTrace();
}
}
}
消息属性
给消息增加一些标记
发送方
- 方法
setStringProperty
// 消息属性
@Test
void testSendProperty() {
jmsTemplate.send(name, session -> {
TextMessage message = session.createTextMessage("发送的是TextMessage");
// 设置消息属性
message.setStringProperty("orderId", "1102");
return message;
});
}
接收方
-方法 getStringProperty
// 消息属性
@JmsListener(destination = "${aaa.bbb:ccc}")
public void receiveMessage(Message message) throws JMSException {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
String orderId = message.getStringProperty("orderId");
System.out.println("接收到消息:" + textMessage.getText());
System.out.println("orderId: " + orderId);
}
}
持久化方案
作用:保证消息
不丢失
类型:有三种
- 存储在
内存
中(效率高,重启服务器数据就没了)- 存储在
日志
文件中(khaDB是默认的存储方式)- 存储到
数据库
中(基于jdbc的存储方式)
生产者持久化的流程图
消费者持久化流程图
存储在日志文件中(默认存储方式)
delivery-mode有两个值
persistent
:默认存储在日志
文件中non-persistent
:存储在内存
中
spring:
jms:
# 是否是发布订阅模式
pub-sub-domain: false
template:
delivery-mode: persistent # 持久化模式 非持久化(将消息保存在内存中) 持久化(将消息保存在磁盘中)
日志文件
存储在数据库中
开启持久化
spring:
jms:
# 是否是发布订阅模式
pub-sub-domain: false
template:
delivery-mode: persistent # 持久化模式 非持久化(将消息保存在内存中) 持久化(将消息保存在磁盘中)
修改activemq.xml的配置
- 新增bean
<bean id="mysql-mq" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<!--如果是mysql5.7版本就写这个jdbc路径的url路径-->
<!-- <property name="url" value="jdbc:mysql://127.0.0.1:3306/activemq?characterEncoding=utf-8&serverTimezone=Asia/Shanghai" /> -->
<!--如果是mysql8.0的版本就写这个jdbc的url路径-->
<property name="url" value="jdbc:mysql://127.0.0.1:3306/activemq?useSSL=false&serverTimezone=UTC" />
<property name="username" value="root"/>
<property name="password" value="root"/>
<property name="initialSize" value="5"/>
<property name="maxTotal" value="100"/>
<property name="maxIdle" value="30"/>
<property name="maxWaitMillis" value="10000"/>
<property name="minIdle" value="1"/>
</bean>
修改配置
<persistenceAdapter>
<!-- <kahaDB directory="${activemq.data}/kahadb"/> -->
<jdbcPersistenceAdapter dataSource="#mysql-mq" createTablesOnStartup="true" />
</persistenceAdapter>
引入mysql的jar包
新建activemq,数据库,重启activemq,就会多三个表
事务
- 事务特性就是保持
原子性
,要么全部成功,要么全部失败- 这里的事务指的是
生产者
,要么信息全部发送到服务器,要不全部都不发送到服务器
新增配置类(生产者)
此配置类和
启动类
在一个目录下
// 配置类
@Configuration
public class ActiveMQConfig {
// JMS事务管理器
@Bean
public JmsTransactionManager jmsTransactionManager(ConnectionFactory connectionFactory) {
return new JmsTransactionManager(connectionFactory);
}
}
注解使用
- 在对应方法上使用注解
@Transactional
即可- 原生的写法是使用
session.commit();
和session.rollback();
// 测试事务
// 不加事务的情况下,前面的消息发送成功,后面的消息发送失败
// 加了事务的情况下,前后的消息都不会发送成功,保持了原子性
// 如果是原生的JMS,需要手动提交事务 session.commit(); session.rollback();
// 如果是springboot的JMS,不需要手动提交事务,使用@Transactional注解即可
@Test
@Transactional
void testTransaction() {
for (int i = 0; i < 10; i++) {
if (i == 5) {
throw new RuntimeException("出错了");
}
jmsTemplate.send(name, session -> {
TextMessage message = session.createTextMessage("发送的是TextMessage");
return message;
});
}
}
在消费方的使用
- 消费方只能使用
session.commit();
和session.rollback();
- 回滚之后会
再次从服务器发送给消费方
,总共5次,5次之后,此消息加入死信队列
消息确认机制
事务
和确认机制
只能二选一
- 如果开启了
事务
,那么消息是自动确认
的- 如果没有开启事务,有三个值可以选择
配置类(接收方)
先关闭事务,开启手动确认
// 配置类
@Configuration
public class ActiveMQConfig {
// 关闭事务
@Bean("jmsListenerContainerFactory")
public DefaultJmsListenerContainerFactory defaultJmsListenerContainerFactory(ConnectionFactory connectionFactory) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
// 关闭事务
factory.setSessionTransacted(false);
// 修改消息确认机制
// factory.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE); // CLIENT_ACKNOWLED在原生的JMS中是有用的,但是在springboot中是没有用的,被修改过了
factory.setSessionAcknowledgeMode(4); // 在springboot中,4代表的是手动确认 不要在使用Session.CLIENT_ACKNOWLED
return factory;
}
}
在监听器中指定工厂类
// 确认机制 指向我们配置的配置类
@JmsListener(destination = "${aaa.bbb:ccc}",containerFactory = "jmsListenerContainerFactory")
public void receiveMessage(Message message, Session session) throws JMSException {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
System.out.println("接收到消息:" + textMessage.getText());
// 手动确认
textMessage.acknowledge();
}
}
不进行手动确认的话,
等待的消息会一直变多
,不会在第二框中出列,会随着下一次发送消息再次发送
消息投递的方式
除了同步投递之外三种方式:
异步投递、延时投递、定时投递
同步发送和异步发送
两个值的设定
在配置类中配置(发送方)
两者不要同时配置,只允许有一个JmsTemplate 的bean注入
// 配置类
@Configuration
public class ActiveMQConfig {
// 配置用於異步發送的非持久化的jmstempalte(事务中)
@Bean
public JmsTemplate asynJmsTemplate(ConnectionFactory connectionFactory) {
JmsTemplate jmsTemplate = new JmsTemplate();
jmsTemplate.setConnectionFactory(connectionFactory);
// 设置为非持久化
jmsTemplate.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
// 设置为异步发送
jmsTemplate.setExplicitQosEnabled(true);
return jmsTemplate;
}
// 配置用於同步發送的持久化的jmstempalte(默认的)
@Bean
public JmsTemplate synJmsTemplate(ConnectionFactory connectionFactory) {
JmsTemplate jmsTemplate = new JmsTemplate();
jmsTemplate.setConnectionFactory(connectionFactory);
return jmsTemplate;
}
}
延时投递
修改配置文件
activemq.xml
,新增 schedulerSupport=“true”
<broker 。。。。。。。dataDirectory="${activemq.data}" schedulerSupport="true">
延迟投递
// 延时投递
@Test
void testDelay() {
jmsTemplate.send(name, session -> {
TextMessage message = session.createTextMessage("发送的是TextMessage");
// 设置延时投递 20秒之后消息才会到达服务器
message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, 20 * 1000);
return message;
});
}
定时投递
- 这个地方直接使用
定时任务
做吧
死信队列
死信队列,又名DLQ,全称:Dead Letter Queue,保存两种类型的信息:
处理失败
的信息过期
的信息
有三种情况会出现死信队列:
- 事务性的回话调用了
rollbck
方法- 事务性的回话在
commit之前
,被关闭
了- 回话被自定义成
CLIENT_ACKNOWLEDGE
,调用了recover
函数
缺省情况下:
持久化的消息才会被发送到DLQ
,非持久化的消息不会发送到DLQ- 除非自定义,没有特别指定的话,所有的消息都会发送到 官方预定的死信队列:
ACTIVEMQ.DLQ
- 消息被重发的次数是
6
次
在接受方 出错,服务器就会重发6次消息,间隔时间1s
演示死信队列
一定要
开启事务
,死信队列 就是为了重启activemq之后,能够对这部分数据进行处理
在接受方的配置类中进行事务管理类的配置
@Configuration
public class ActiveMQConfig {
// // JMS事务管理器
@Bean
public JmsTransactionManager jmsTransactionManager(ConnectionFactory connectionFactory) {
return new JmsTransactionManager(connectionFactory);
}
}
// 测试死信队列
@JmsListener(destination = "${aaa.bbb:ccc}")
public void receiveMessage(Message message, Session session) throws JMSException {
try {
int i = 1 / 0;
System.out.println("被监听到了");
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
System.out.println("接收到消息:" + textMessage.getText());
}
} catch (Exception e) {
session.rollback();
throw e; // 重新抛出异常以便上层处理
}
}
因为调用了rollback方法,所以多了一个死信队列,里面存储了
服务器发送给接收方失败
的消息
为每一个队列单独配置死信队列
- 所有队列共用一个死信队列,不容易找到对应队列发送失败的消息
- 可以通过配置
activemq.xm
l配置文件,为每一个队列都配置一个死信队列
新增如下配置:
<policyEntry queue=">" >
<deadLetterStrategy>
<individualDeadLetterStrategy queuePrefix="DLQ." useQueueForQueueMessages="true"/>
</deadLetterStrategy>
</policyEntry>
重启activemq之后,重新触发死信队列就会看到一个基于 原队列的的新的死信队列
重发的策略也可以在配置类中记性配置
- 这是
接受方
的配置类- 一定要
开启事务
- 配置RedeliveryPolicy 配置
失效
的话,将ActiveMQConnectionFactory 和JmsTemplate 也配置一下,确保真的使用的是我们定义的redeliveryPolicy
一般设置:重发
间隔
(默认1s),时间是否递增
(默认不递增),次数
(默认次数6)
// 配置类
@Configuration
public class ActiveMQConfig {
// // JMS事务管理器
@Bean
public JmsTransactionManager jmsTransactionManager(ConnectionFactory connectionFactory) {
return new JmsTransactionManager(connectionFactory);
}
@Bean
public RedeliveryPolicy redeliveryPolicy() {
RedeliveryPolicy redeliveryPolicy = new RedeliveryPolicy();
// 设置是否在每次尝试重新发送失败后,增长重发延迟
redeliveryPolicy.setUseExponentialBackOff(true);
// 设置重发次数 10次
redeliveryPolicy.setMaximumRedeliveries(10);
// 设置重发间隔时间 1s
redeliveryPolicy.setInitialRedeliveryDelay(1000);
// 设置重发时间间隔递增 2倍
redeliveryPolicy.setBackOffMultiplier(2);
// 是否避免消息碰撞
redeliveryPolicy.setUseCollisionAvoidance(false);
//设置最大拖延时间是-1,表示没有最大拖延时间
return redeliveryPolicy;
}
@Bean
public ActiveMQConnectionFactory connectionFactory(RedeliveryPolicy redeliveryPolicy) {
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory();
connectionFactory.setRedeliveryPolicy(redeliveryPolicy);
return connectionFactory;
}
@Bean
public JmsTemplate jmsTemplate(ActiveMQConnectionFactory connectionFactory) {
return new JmsTemplate(connectionFactory);
}
}
面试题
activemq服务器宕机问题如何解决
- 使用activemq 主从集群方案:zookeeper集群 + replicated leveldb + activemq集群
如何防止消费方的消息被重复消费
因为网路延迟等原因,MQ无法及时接受消费方的
确认消息
,导致MQ重试,重试过程总就会导致消息被重复消费
解决思路
- 如果是基于
数据库
的操作,将消息的id作为表的唯一主键
进行存储,重复的的时候会触发冲突,避免脏数据的产生。- 如果
不是基于数据库
的的操作,可以借助第三方来判别消息是否已经被消费:- 使用redis,每次消息被
消费完毕
的时候,将当前消息的id作为key
存储到redis
中- 消费前,使
用消息的id查询redis中是否存在记录
,记录存在,则说明已经被消费过了
如何防止消息丢失
- 在消息的生产方和消费方都使用
事务
- 在消费方设置为
手动确认
(ACK)- 使用
事务
的基础上,对消息进行持久化
:JDBC或者默认的日志文件
死信队列
- 回头看
- 死信队列的定义
- 配置
- 重发配置