Spring Boot 整合 ActiveMQ 实现手动确认和重发消息
消息队列中间件是分布式系统中重要的组件,已经逐渐成为企业系统内部通信的核心手段。主要功能包括松耦合、异步消息、流量削锋、可靠投递、广播、流量控制、最终一致性等。实现高性能,高可用,可伸缩和最终一致性架构。消息形式支持点对点和订阅-发布。
消息队列中间件常见的应用场景包括应用解耦、异步处理、流量错峰与流控、日志处理等等。目前常见的消息队列中间件有ActiveMQ、RabbitMQ、ZeroMQ、Kafka、MetaMQ和RocketMQ等。本文在Spring Boot整合ActiveMQ的基础上,以点对点模式为例介绍两种消息处理机制——手动确认和重发消息。
之所以介绍消息重发机制,是因为ActiveMQ被消费失败的消息将会驻留在队列中,因为没有进行消息确认,所以,下次还会监听到这条消息。
软件环境
- Apache ActiveMQ 5.15.9
- Java version 11
- IntelliJ IDEA 2020.2 (Ultimate Edition)
- Spring Boot 2.3.1.RELEASE
1.引入ActiveMQ pom 坐标和基本配置
引入pom坐标:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
2. 修改配置文件
spring.activemq.broker-url=tcp://localhost:61616
spring.activemq.in-memory=true
spring.activemq.pool.enabled=false
#默认值false,表示点到点模式,true时代表发布订阅模式
spring.jms.pub-sub-domain=false
spring.activemq.user=wiener
spring.activemq.password=wiener123
3. SpringBoot 的启动类配置
在 SpringBoot 的启动类上加上一个 @EnableJms 注解。我的项目去掉此注解也可以正常启动,故没有添加,你的项目如果启动失败,请添加。
4. ActiveMQ 连接配置
当我们发送消息的时候,会出现发送失败的情况,此时我们需要用到ActiveMQ提供的消息重发机制,重新发送消息。那么问题来了,我们怎么知道消息是否发送成功呢?ActiveMQ还有消息确认机制,消费者在接收到消息的时候可以进行确认。本节将学习ActiveMQ消息的确认机制和重发机制。
消息确认机制有五种,在session对象中定义了如下四种:
- SESSION_TRANSACTED= 0:事务提交并确认
- AUTO_ACKNOWLEDGE= 1 :自动确认
- CLIENT_ACKNOWLEDGE= 2:客户端手动确认
- UPS_OK_ACKNOWLEDGE= 3: 自动批量确认
在ActiveMQSession类中补充了一个自定义的ACK机制:
INDIVIDUAL_ACKNOWLEDGE= 4:单条消息确认。
消息确认机制的使用场景分两种:
1、带事务的session
如果session带有事务,并且事务成功提交,则消息被自动签收。如果事务回滚,则消息会被再次传送。
2、不带事务的session
不带事务的session的签收方式,取决于session的配置。
新建一个配置类ActiveMQConfig用来配置 ActiveMQ 连接属性,在工厂中设置开启单条消息确认模式INDIVIDUAL_ACKNOWLEDGE。注意:ActiveMQ 默认是开启事务的,且消息确认机制与事务机制是冲突的,只能二选一,所以演示消息确认前,请先关闭事务。
配置代码如下:
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.config.SimpleJmsListenerContainerFactory; /**
* 配置 Active MQ
* @author Wiener
* @date 2020/9/15
*/
@Configuration
public class ActiveMQConfig {
@Value("${spring.activemq.user}")
private String userName; @Value("${spring.activemq.password}")
private String password; @Value("${spring.activemq.broker-url}")
private String brokerUrl;
/**
* 配置名字为givenConnectionFactory的连接工厂
* @return
*/
@Bean("givenConnectionFactory")
public ActiveMQConnectionFactory connectionFactory() {
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(userName, password, brokerUrl);
// 设置重发机制
RedeliveryPolicy policy = new RedeliveryPolicy();
policy.setUseExponentialBackOff(Boolean.TRUE);
// 消息处理失败重新处理次数,默认为6次
policy.setMaximumRedeliveries(2);
// 重发时间间隔,默认为1秒
policy.setInitialRedeliveryDelay(1000L);
policy.setBackOffMultiplier(2);
policy.setMaximumRedeliveryDelay(1000L);
activeMQConnectionFactory.setRedeliveryPolicy(policy);
return activeMQConnectionFactory;
}
/**
* 在Queue模式中,对消息的监听需要对containerFactory进行配置
*
* @param givenConnectionFactory
* @return
*/
@Bean("queueListener")
public JmsListenerContainerFactory<?> queueJmsListenerContainerFactory(ActiveMQConnectionFactory givenConnectionFactory) {
SimpleJmsListenerContainerFactory factory = new SimpleJmsListenerContainerFactory();
// 关闭事务
factory.setSessionTransacted(false);
// 手动确认消息
factory.setSessionAcknowledgeMode(ActiveMQSession.INDIVIDUAL_ACKNOWLEDGE);
factory.setPubSubDomain(false);
factory.setConnectionFactory(givenConnectionFactory);
return factory;
} @Bean("topicListener")
public JmsListenerContainerFactory<?> jmsListenerContainerTopic(ActiveMQConnectionFactory givenConnectionFactory){
//设置为发布订阅模式, 默认情况下使用生产消费者方式
DefaultJmsListenerContainerFactory bean = new DefaultJmsListenerContainerFactory();
bean.setPubSubDomain(true);
bean.setConnectionFactory(givenConnectionFactory);
return bean;
} }
这里设置消息发送失败时重发次数为2次,其系统默认值为6次,源码如下:

5.手动确认和重发消息
JmsTemplate是消息处理核心类(线程安全),用于发送和接收消息,在发送或者是消费消息的同时也会对所需资源进行创建和释放。消息消费采用receive方式(同步、阻塞的),这里不做具体描述。关于消息的发送,常用的方式为send()以及convertAndSend()两种,其中send()发送需要指定消息格式,使用convertAndSend可以根据定义好的MessageConvert自动转换消息格式。
消息生产者代码比较简单,主要是调用 jmsMessagingTemplate 的 convertAndSend() 方法。发送的消息如果是对象类型的,可以将其转成 json 字符串。
@Service("producer")
public class Producer {
/**
* 也可以注入JmsTemplate,JmsMessagingTemplate对JmsTemplate进行了封装
*/
@Autowired
private JmsMessagingTemplate jmsTemplate;
/**
* 发送消息
*
* @param destination 指定消息目的地
* @param message 待发送的消息
*/
public void sendMessage(Destination destination, final String message) {
jmsTemplate.convertAndSend(destination, message);
}
}
其中,Student 类的定义如下:
/**
* @author Wiener
*/
@Getter
@Setter
@ToString
@Component
public class Student implements Serializable { private static final long serialVersionUID = 4481359068243928443L;
private Long id;
/** 姓名 */
private String name;
private String birthDay; public Student() {
} public Student(Long id, String name, String birthDay) {
this.id = id;
this.name = name;
this.birthDay = birthDay;
}
}
在消费者监听类中,我们使用@JmsListener监听队列消息。大家请注意,如果我们不在@JmsListener中指定containerFactory,那么将使用默认配置,默认配置中Session是开启了事务的,即使我们设置了手动Ack也是无效的。
定义两个名为 "east7-queue" 的消费者,在消费消息时,二者默认会轮询消费。在消费者消费完一条消息之后,调用 message.acknowledge() 进行消息的手动确认。如果在消费者中未进行手动确认,由于 ActiveMQ 进行了持久化消息,那么在下次启动项目的时候还会再次发送该消息。
import com.eg.wiener.dto.Student;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.activemq.command.ActiveMQMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component; import javax.jms.JMSException;
import javax.jms.Session;
import javax.jms.TextMessage; @Component
public class ConsumerListener {
private static Logger logger = LoggerFactory.getLogger(ConsumerListener.class);
// 记录重发总次数
private static int num = 0;
/**
* east7-queue普通队列:消费者1
*/
@JmsListener(destination = "east7-queue", containerFactory = "queueListener")
public void receiveQueueTest1(ActiveMQMessage message, Session session) throws
JsonProcessingException, JMSException {
logger.info("receiveQueueTest:1-mode {}", session.getAcknowledgeMode()); String text = null;
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
ObjectMapper objectMapper = new ObjectMapper();
Student student = objectMapper.readValue(textMessage.getText(), Student.class);
logger.info("queue1接收到student:{}", student);
// 手动确认
try {
message.acknowledge();
} catch (JMSException e) {
// 此不可省略 重发信息使用
session.recover();
}
}
} /**
* east7-queue普通队列:消费者2
*/
@JmsListener(destination = "east7-queue", containerFactory = "queueListener")
public void receiveQueueTest2(ActiveMQMessage message, Session session) throws JMSException,
JsonProcessingException {
logger.info("receiveQueueTest:2-transacted mode {}", session.getTransacted());
String msg = null;
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
msg = textMessage.getText();
ObjectMapper objectMapper = new ObjectMapper();
Student student = objectMapper.readValue(msg, Student.class);
logger.info("queue2接收到student:{}", student);
try {
if (msg.contains("典韦5") || msg.contains("典韦6") || msg.contains("典韦7")) {
throw new JMSException("故意抛出的异常");
}
message.acknowledge();
} catch (JMSException e) {
++ num;
System.out.println(String.format("触发重发机制,num = %s, msg = %s", num, msg));
session.recover();
}
}
} }
这里为了验证消息重发机制,故意在消费消息的时候抛出了一个异常。我们的Junit测试用例如下:
@Test
public void ackMsgTest() throws JsonProcessingException {
Destination destination = new ActiveMQQueue("east7-queue"); Student student = new Student(1L, "张三", DateUtils.parseDateSM(new Date()));
ObjectMapper objectMapper = new ObjectMapper();
for (int i = 0; i < 10; i++) {
student.setId(i * 10L);
student.setName("典韦" + i);
producer.sendMessage(destination, objectMapper.writeValueAsString(student));
} sleep(5000L); //给消费者足够的时间消费消息
} private void sleep(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
执行测试函数后,可以发现两个消费者交替消费消息。而且可以发现,每个错误消息重发次数为3次,与我们的预期吻合。
触发重发机制,num = 1, msg = {"id":500,"name":"典韦5","birthDay":"2020-09-20 16:31:39"}
触发重发机制,num = 2, msg = {"id":500,"name":"典韦5","birthDay":"2020-09-20 16:31:39"}
触发重发机制,num = 3, msg = {"id":500,"name":"典韦5","birthDay":"2020-09-20 16:31:39"}
触发重发机制,num = 4, msg = {"id":700,"name":"典韦7","birthDay":"2020-09-20 16:31:39"}
触发重发机制,num = 5, msg = {"id":700,"name":"典韦7","birthDay":"2020-09-20 16:31:39"}
触发重发机制,num = 6, msg = {"id":700,"name":"典韦7","birthDay":"2020-09-20 16:31:39"}
Reference
https://www.cnblogs.com/liuyuan1227/p/10776189.html
https://blog.csdn.net/chinese_cai/article/details/108342611
https://blog.csdn.net/p_programmer/article/details/88384138
Spring Boot 整合 ActiveMQ 实现手动确认和重发消息的更多相关文章
- spring boot整合activemq消息中间件
spring boot整合activemq消息中间件 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi ...
- activeMQ入门+spring boot整合activeMQ
最近想要学习MOM(消息中间件:Message Oriented Middleware),就从比较基础的activeMQ学起,rabbitMQ.zeroMQ.rocketMQ.Kafka等后续再去学习 ...
- spring boot 整合activemq
1 Spring Boot与ActiveMQ整合 1.1使用内嵌服务 (1)在pom.xml中引入ActiveMQ起步依赖 <properties> <spring.version& ...
- Spring Boot 整合 ActiveMQ
依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spri ...
- Spring Boot入门 and Spring Boot与ActiveMQ整合
1.Spring Boot入门 1.1什么是Spring Boot Spring 诞生时是 Java 企业版(Java Enterprise Edition,JEE,也称 J2EE)的轻量级代替品.无 ...
- Spring Boot与ActiveMQ整合
Spring Boot与ActiveMQ整合 1使用内嵌服务 (1)在pom.xml中引入ActiveMQ起步依赖 <dependency> <groupId>org.spri ...
- Spring Boot 整合 Kafka
Kafka 环境搭建 kafka 安装.配置.启动.测试说明: 1. 安装:直接官网下载安装包,解压到指定位置即可(kafka 依赖的 Zookeeper 在文件中已包含) 下载地址:https:// ...
- Spring Boot整合Mybatis并完成CRUD操作
MyBatis 是一款优秀的持久层框架,被各大互联网公司使用,本文使用Spring Boot整合Mybatis,并完成CRUD操作. 为什么要使用Mybatis?我们需要掌握Mybatis吗? 说的官 ...
- Spring Boot整合Elasticsearch
Spring Boot整合Elasticsearch Elasticsearch是一个全文搜索引擎,专门用于处理大型数据集.根据描述,自然而然使用它来存储和搜索应用程序日志.与Logstash和K ...
- Spring Boot2 系列教程(九)Spring Boot 整合 Thymeleaf
虽然现在慢慢在流行前后端分离开发,但是据松哥所了解到的,还是有一些公司在做前后端不分的开发,而在前后端不分的开发中,我们就会需要后端页面模板(实际上,即使前后端分离,也会在一些场景下需要使用页面模板, ...
随机推荐
- 阻尼、模态应变能法与FRP的关系
阻尼的概念 系统损耗振动能或声能的能力称为阻尼 阻尼越大,输人系统的能量便能在较短时间内损耗完毕.系统从受激振动到重新静止所经历的时间就越短; 阻尼也可理解为系统受激后迅速恢复到受激前状态的一种能力 ...
- Selenium WebDriver上创建 WebDriver测试脚本
本文实现一个WebDriver测试脚本,介绍WebDrive的常用命令.UI元素定位的策略以及在脚本中的使用,还有Get命令. 你将学到: 脚本创建 代码走查 测试执行 定位Web元素 ...
- Ubuntu下如何管理多个ssh密钥
Ubuntu下如何管理多个ssh密钥 前言 我一直在逃避这个问题,误以为我能够单纯地用一个 ssh 走天下. 好吧,现实是我不得不管理多个 ssh 做,那就写个博客总结一下吧. 查阅后发现前人已经 ...
- [源码系列:手写spring] IOC第三节:Bean实例化策略InstantiationStrategy
主要内容 在第二节中AbstractAutowireCapableBeanFactory类中使用class.newInstance()的方式创建实例,仅适用于无参构造器. 大家可以测试一下,将第二节 ...
- 一个检查左右括号是否配对的语法检查器(c语言)
目录 一.题目如下 二.解题思路 三.代码实现 四.测试结果 一.题目如下 通过键盘输入一个包括 '(' 和 ')' 的字符串string ,判断字符串是否有效.要求设计算法实现检查字符串是否有效,有 ...
- DeepSeek 会话补全 API
DeepSeek 会话补全 API 是一个超强大的 AI 对话接口 ,可以让你: 打造自己的 智能聊天机器人 让 AI 帮你 写文章.改代码.编故事 甚至模拟 各种角色(比如猫娘.霸道总裁.科幻作家- ...
- 读取excel单元格填过得的坑
通过TdxSpreadSheet读取excel单元格值. 有一个cxDBTreeList来来表现科室单元,可是从科室单元excel文件中读取单元内容后,各种报错.一度怀疑cxdbtree的bug. 实 ...
- SpringAI用嵌入模型操作向量数据库!
嵌入模型(Embedding Model)和向量数据库(Vector Database/Vector Store)是一对亲密无间的合作伙伴,也是 AI 技术栈中紧密关联的两大核心组件,两者的协同作用构 ...
- Metasploit(MSF)渗透测试之永恒之蓝实验
实验环境 前提:对方的445端口必须开放,首先要保证是能够访问到目标机器的,那么我们先ping一下目标机器,看网络是否连通 如果无法ping的话,对方机器必须要关闭防火墙,或许有其他方法在对方开启防火 ...
- ESP32+Arduino入门(四):OLED屏随机显示古诗
前言 我觉得去做一些简单又好玩的案例是入门很好的选择. 在实践的过程中会碰到很多需求很多问题在解决这些需求这些问题的过程就是在学习的过程. 今天我来分享一个随机显示古诗的案例,如果对此感兴趣可以跟我一 ...