(十一)Spring Boot 集成异步消息和异步调用 —— 《一步一步学 Spring Boot 2》读书笔记

本文介绍了JMS的两种消息模型,详细阐述了ActiveMQ的下载、运行与访问。重点讲解了Spring Boot整合ActiveMQ的步骤,包括引入依赖、添加配置、创建生产者和消费者等。还说明了使用ActiveMQ做异步消费提高系统性能的方法,以及Spring Boot中使用@Async注解实现异步调用,可减少程序耗时。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文纯个人读书笔记,书籍《一步一步学 Spring Boot 2》
如果喜欢,可直接购买书籍。如有侵权,请联系删除

一、JMS 介绍

JMS(Java Message Service,即 Java 消息服务)是一组 Java 应用程序接口,提供消息的创建、发送、读取等一系列服务,类似于 Java 数据库的统一访问接口 JDBC,不同厂商的消息组件很好地进行通信。

JMS 支持两种消息发送和接收模型。一种称为 P2P(Ponit to Point) 模型,采用点对点的方式发送消息。P2P 模型是基于队列的,消息生产者(Producer)发送消息到队列,消息消费者(Consumer)从队列中接收消息,队列的存在使得消息的异步传输成为可能。
在这里插入图片描述

P2P 的特点是一个消息只有一个消费者,发送者和接收者之间在时间上没有依赖性,也就是说当发送者发送了消息之后,不管接收者有没有正在运行。

另一种称为 Pub / Sub(Publish / Subscribe,即发布-订阅)模型,发布-订阅模型没有队列,有一个类似的概念叫 Topic(主题)。主题可以认为是消息传递的中介,消息发布者将消息发布到某个主题,而消息订阅者则从主题订阅消息。发布-订阅模型在消息的一对多广播时采用。
在这里插入图片描述

Pub/Sub 可以有多个消费者,必须创建一个订阅者之后,才能消费发布者的消息,为了消费消息,订阅者必须保持运行的状态。
为了缓和这样严格的时间相关性,JMS 允许订阅者创建一个可持久化的订阅。这样,即使订阅者没有被激活(运行),它也能接收到发布者的消息。

二、ActiveMQ

MQ 即 MessageQueue,消息队列,是一个消息的接受和转发的容器。ActiveMQ 是 Apache 提供的一个开源的消息系统,完全采用 Java 来实现,能很好地支持 J2EE 提出的 JMS(Java Message Service,即 Java 消息服务)规范。

1.下载

我们需要到 官网链接 去进行下载。

点击 ActiveMQ 5 。
在这里插入图片描述

这边版本是 5.15.9,点击 apache-activemq-5.15.9-bin.zip 进行下载。
在这里插入图片描述

2.解压运行

直接解压即可,进入到 bin 文件,根据系统 32 位或者 64 位进行选择,双击 activemq.bat 运行 ActiveMQ。
在这里插入图片描述
当出现下面内容时候表示运行成功,ActiveMQ 默认端口是 8161。
在这里插入图片描述
这边碰见一个错误。
在这里插入图片描述
桶盖关闭计算机管理器下的服务 Internet Connection Sharing (ICS)服务,改成手动启动或禁用) 后,可运行。
在这里插入图片描述

3.访问

在浏览器输入 http://localhost:8161/admin/ 进行访问,第一次需要输入用户名 admin 和密码 admin 进行登录。登录成功后,显示首页。
在这里插入图片描述

三、整合 ActiveMQ

1.引入依赖

在 pom.xml 中添加对应的依赖。

pom.xml:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>

2.添加配置

在 application.properties 中添加配置。

application.properties:

spring.activemq.broker-url=tcp://localhost:61616
spring.activemq.in-memory=true
spring.activemq.pool.enabled=false
spring.activemq.packages.trust-all=true
spring.activemq.packages.trust-all

3.生产者

创建生产者 com.xiaoyue.demo.activemq.MessageProducer。

MessageProducer:

@Service
public class MessageProducer {

    @Resource
    private JmsMessagingTemplate jmsMessagingTemplate;

    public void sendMessage(String destinationName, String message) {
        jmsMessagingTemplate.convertAndSend(destinationName, message);
    }
}

JmsMessagingTemplate: 发消息的工具类,也可以注入 JmsTemplate,JmsMessagingTemplate 是对 JmsTemplate 进行了封装。参数 destinationName 是发送到的队列的名字,message 是待发送的消息。

4.消费者

创建消费者 com.xiaoyue.demo.activemq.MessageConsumer。

MessageConsumer:

public class MessageConsumer {

    @Resource
    private MessageService messageService;

    @JmsListener(destination = "xiaoyue.message.queue")
    public void receiveQueue(String message) {
        System.out.println("用户发表消息【" + message + "】成功");
    }
}

@JmsListener: 配置消费者监听的队列 xiaoyue.message.queue,其中 message 是接收到的消息。

5.测试

在测试类 DemoApplicationTests 中添加测试方法进行测试。

DemoApplicationTests:

    @Resource
    private MessageProducer messageProducer;

    @Test
    public void testActiveMQ() {

        Destination destination = new ActiveMQQueue("xiaoyue.message.queue");
        messageProducer.sendMessage("xiaoyue.message.queue", "hello,mq!!!");
    }

结果:
控制台输出日志。
在这里插入图片描述
浏览器访问 http://localhost:8161/admin/,查看 xiaoyue.message.queue 队列。
在这里插入图片描述

四、使用 ActiveMQ

我们先创建一个标准的消息存储 Message 模块。

1.数据库表

运行数据库建表语句。

DROP TABLE IF EXISTS `message`;
CREATE TABLE `message` (
  `id` VARCHAR(32) NOT NULL,
  `content` VARCHAR(256) DEFAULT NULL,
  `user_id` VARCHAR(32) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `message_user_id_index` (`user_id`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8;

2.实体类

创建对应的实体类 com.xiaoyue.demo.model.Message。

Message:

@Entity
@Table(name = "message")
public class Message implements Serializable {

    @Id
    private String id;
    private String content;
    private String userId;
    ...
}

3.Repository

创建数据库操作模块 com.xiaoyue.demo.repository.MessageRepository

MessageRepository:

public interface MessageRepository extends JpaRepository<Message, String> {

}

4.service 层

标准的 service 模块,创建接口 com.xiaoyue.demo.service.MessageService。

MessageService:

public interface MessageService {

    Message save(Message message);
}

创建接口实现类 com.xiaoyue.demo.service.impl.MessageServiceImpl。

MessageServiceImpl:

@Service
@Transactional
public class MessageServiceImpl implements MessageService {

    @Resource
    private MessageRepository messageRepository;

    @Override
    public Message save(Message message) {
        return messageRepository.save(message);
    }
}

5.测试

在测试类 DemoApplicationTests 中添加测试方法进行测试。

DemoApplicationTests:

    @Resource
    private MessageService messageService;

    @Test
    public void testSaveMessage(){
        Message message = new Message();
        message.setId("1");
        message.setUserId("1");
        message.setContent("消息内容!");

        message = messageService.save(message);
    }

结果:
在这里插入图片描述

6.使用 ActiveMQ

假设消息发送的频率十分频繁,同一时间可能有多个消息需要保存。如果按照上面的做法,每保存一条消息,后端都会单独开一个线程。
后端服务系统的线程数和数据库线程池中的线程数量都是固定的,这些资源非常宝贵,当消息实时保存到数据库中,必然造成后端服务和数据库极大的压力。所以我们使用 ActiveMQ 做异步消费,提高系统整体的性能。

service 新增异步保存接口 asynSave。

MessageService:

public interface MessageService {

    Message save(Message message);

    String asynSave(Message message);
}

MessageServiceImpl:

@Service
@Transactional
public class MessageServiceImpl implements MessageService {

    @Resource
    private MessageRepository messageRepository;

    private static Destination destination = new ActiveMQQueue("xiaoyue.message.queue");

    @Resource
    private MessageProducer messageProducer;

    @Override
    public Message save(Message message) {
        return messageRepository.save(message);
    }

    @Override
    public String asynSave(Message message) {
		//不直接保存消息,把消息推到队列中
        messageProducer.sendMessage("xiaoyue.message.asynsave.queue", message);
        return "success";
    }
}

在生产者中发送消息到队列,在消费者中进行数据库的保存,从而达到异步存储。

生产者添加对应方法。

MessageProducer:

@Service
public class MessageProducer {

    @Resource
    private JmsMessagingTemplate jmsMessagingTemplate;

    public void sendMessage(String destinationName, String message) {
        jmsMessagingTemplate.convertAndSend(destinationName, message);
    }

    public void sendMessage(String destinationName, Message message) {
        jmsMessagingTemplate.convertAndSend(destinationName, message);
    }
}

消费者添加对应处理方法。

MessageConsumer:

@Component
public class MessageConsumer {

    @Resource
    private MessageService messageService;

    @JmsListener(destination = "xiaoyue.message.queue")
    public void receiveQueue(String message) {
        System.out.println("用户发表消息【" + message + "】成功");
    }

    @JmsListener(destination = "xiaoyue.message.asynsave.queue")
    public void receiveQueue(Message message) {
        messageService.save(message);
    }
}

7.测试

在测试类 DemoApplicationTests 中添加测试方法进行测试。

DemoApplicationTests:

    @Resource
    private MessageService messageService;

    @Test
    public void testMQAsynSaveMessage(){
        Message message = new Message();
        message.setId("5");
        message.setUserId("1");
        message.setContent("消息内容!");

        String result = messageService.asynSave(message);
        System.out.println("异步发表说说 :" + result);
    }

结果:
在这里插入图片描述
这边运行的在控制台会有一个错误,需要自定义 MessageConverter。但是仍然可以保存到数据库,这边就不进行具体讲解了,需要的自行百度。附一 传送门

五、异步调用

在应用开发过程中,需要进行耗时操作的时候,不希望程序卡在耗时任务上,想让程序能够并行执行,除了可以使用多线程来并行的处理任务,也可以使用 Spring Boot 提供的异步处理方式 @Async 来处理。在 Spring Boot 框架中,只要提过 @Async 注解就能将普通的同步任务改为异步调用任务。

1.记录时间

执行多个数据库调用(耗时操作) 并记录耗时时间。

修改 com.xiaoyue.demo.service.impl.UserServiceImpl 中的 findAll 方法,让他们各自记录耗时时间。

UserServiceImpl:

    @Override
    public List<User> findAll() {

        System.out.println("开始做任务");
        long start = System.currentTimeMillis();
        List<User> userList = userRepository.findAll();
        long end = System.currentTimeMillis();
        System.out.println("findAll() 完成任务,耗时:" + (end - start) + "毫秒");
        return userList;
    }

    @Override
    public Page<User> findAll(Pageable pageable) {
        System.out.println("开始做任务");
        long start = System.currentTimeMillis();
        
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        Page<User> userPage = userRepository.findAll(pageable);
        long end = System.currentTimeMillis();
        System.out.println("findAll(Pageable pageable)  完成任务,耗时:" + (end - start) + "毫秒");
        return userPage;
    }

在测试类 DemoApplicationTests 中添加测试方法进行测试并记录时间。

DemoApplicationTests:

    @Test
    public void testAsync(){
        long startTime = System.currentTimeMillis();
        System.out.println("findAll() 查询所有用户!");
        List<User> userList = userService.findAll();
        System.out.println("findAll(Pageable pageable) 查询所有用户!");
        PageRequest pageRequest = new PageRequest(0,10);
        Page<User> userPage = userService.findAll(pageRequest);
        long endTime = System.currentTimeMillis();
        System.out.println("总共消耗:" + (endTime - startTime) + "毫秒");
    }

先调用 findAll(),再调用 findAll(Pageable pageable) ,都是对 User 表进行查询,会有缓存,所以在 findAll(Pageable pageable) 中添加 sleep 等待,这样对比比较明显。

结果:
在这里插入图片描述

2.使用异步

修改 service 层,使用 @Async 创建上面用的 findAll 方法对应的异步方法。

UserService:

    List<User> findAll();
    List<User> findAsynAll();

    Page<User> findAll(Pageable pageable);
    Page<User> findAsynAll(Pageable pageable);

UserServiceImpl:

    @Override
    @Async
    public List<User> findAsynAll() {

        System.out.println("开始做任务");
        long start = System.currentTimeMillis();
        List<User> userList = userRepository.findAll();
        long end = System.currentTimeMillis();
        System.out.println("findAll() 完成任务,耗时:" + (end - start) + "毫秒");
        return userList;
    }

    @Override
    @Async
    public Page<User> findAsynAll(Pageable pageable) {
        System.out.println("开始做任务");
        long start = System.currentTimeMillis();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Page<User> userPage = userRepository.findAll(pageable);
        long end = System.currentTimeMillis();
        System.out.println("findAll(Pageable pageable)  完成任务,耗时:" + (end - start) + "毫秒");
        return userPage;
    }

需要在入口类添加注解 @EnableAsync 开启异步调用。

DemoApplication:

@SpringBootApplication
@ServletComponentScan
@EnableAsync
@ImportResource(locations={"classpath:spring-mvc.xml"})
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

在测试类 DemoApplicationTests 中添加测试方法进行测试并记录时间。

DemoApplicationTests:

    @Test
    public void testAsync(){
        long startTime = System.currentTimeMillis();
        System.out.println("findAll() 查询所有用户!");
        List<User> userList = userService.findAsynAll();
        System.out.println("findAll(Pageable pageable) 查询所有用户!");
        PageRequest pageRequest = new PageRequest(0,10);
        Page<User> userPage = userService.findAsynAll(pageRequest);
        long endTime = System.currentTimeMillis();
        System.out.println("总共消耗:" + (endTime - startTime) + "毫秒");
    }

结果:

在这里插入图片描述

可以发现,异步调用确实可以减少时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值