本文纯个人读书笔记,书籍《一步一步学 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) + "毫秒");
}
结果:
可以发现,异步调用确实可以减少时间。