异步消息与异步调用
本章主要介绍ActiveMQ的安装与使用、Spring Boot集成ActiveMQ、利用ActiveMQ实现异步发表微信说说以及Spring Boot异步调用@Async等内容
1.JMS消息概述
JMS(Java Message Service,即Java消息服务)是一组Java应用程序接口,它提供消息的创建、发送、读取等一系列服务。JMS提供了一组公共应用程序接口和响应的语法,类似于Java数据库的统一访问接口JDBC,它是一种与厂商无关的API,使得Java程序能够与不同厂商的消息组件很好地进行通信。
JMS支持两种消息发送和接收模型。一种称为P2P(Ponit to Point)模型,即采用点对点的方式发送消息。P2P模型是基于队列的,消息生产者(Producer)发送消息到队列,消息消费者(Consumer)从队列中接收消息,队列的存在使消息的异步传输成为可能。P2P模式图如图所示。
P2P的特点是每个消息只有一个消费者(一旦被消费,消息就不在消息队列中),发送者和接收者之间在时间上没有依赖性,也就是说当发送者发送了消息之后,不管接收者有没有正在运行,它不会影响消息被发送到队列中,接收者在成功接收消息之后需向队列应答成功。
另一种称为Pub/Sub(Publish/Subscribe,即发布-订阅)模型,发布-订阅模型定义了如何向一个内容节点发布和订阅消息,这个内容节点称为Topic(主题)。主题可以认为是消息传递的中介,消息发布者将消息发布到某个主题,而消息订阅者则从主题订阅消息。主题使得消息的订阅者与消息的发布者互相保持独立,不需要进行接触即可保证消息的传递,发布-订阅模型在消息的一对多广播时采用。Pub/Sub模式图如图所示。
Pub/Sub的特点是每个消息可以有多个消费者,发布者和订阅者之间有时间上的依赖性。针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息,而且为了消费消息,订阅者必须保持运行的状态。为了缓和这样严格的时间相关性,JMS允许订阅者创建一个可持久化的订阅。这样,即使订阅者没有被激活(运行),它也能接收到发布者的消息。如果你希望发送的消息不被做任何处理,或者被一个消息者处理,或者可以被多个消费者处理的话,那么可以采用Pub/Sub模型。
2.Spring Boot集成ActiveMQ
2.1ActiveMQ概述
MQ全称为MessageQueue(消息队列),是一个消息的接收和转发的容器,可用于消息推送。ActiveMQ是Apache提供的一个开源的消息系统,完全采用Java来实现,因此,它能很好地支持J2EE提出的JMS(Java Message Service,即Java消息服务)规范。
2.2ActiveMQ的安装
安装ActiveMQ之前,需要到官方网站下载,本书使用apache-activemq-5.15.0这个版本进行讲解。ActiveMQ的具体安装步骤如下:
- 将官方网站下载的安装包apache-activemq-5.15.0-bin.zip解压。
- 打开解压的文件夹,进入bin目录,根据电脑操作系统是32位还是64位,选择进入【win32】文件夹或者【win64】文件夹。
- 双击【activemq.bat】,即可启动ActiveMQ。当看到如图11-4所示的启动信息时,代表ActiveMQ安装成功。从图中可以看出,ActiveMQ默认启动到8161端口。
![](https://img-blog.csdnimg.cn/20210316194709147.png)
- 安装成功之后,在浏览器中输入http://localhost:8161/admin链接访问,第一次访问需要输入用户名admin和密码admin进行登录,登录成功之后,就可以看到ActiveMQ的首页。
![](https://img-blog.csdnimg.cn/20210316194726601.png)
2.3引入依赖
在Spring Boot中集成ActiveMQ,首先需要在pom.xml文件中引入所需的依赖,具体代码如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</actifactId>
</dependency>
2.4添加ActiveMQ配置
依赖添加完成之后,需要在application.properties配置文件中添加ActiveMQ配置,具体代码如下:
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:ObjectMessage的使用机制是不安全的,ActiveMQ自5.12.2和5.13.0之后,强制Consumer端声明一份可信任的包列表,只有当ObjectMessage中的Object在可信任包内,才能被提取出来。该配置表示信任所有的包。
3.使用ActiveMQ
3.1创建生产者
ActiveMQ依赖和配置开发完成之后,首先在数据库中建立说说表ay_mood。具体建表语句如下:
DROP TABLE IF EXISTS `ay_mood`;
CREATE TABLE `ay_mood`(
`id` varchar(32) NOT NULL,
`content` varchar(256) DEFAULT NULL,
`user_id` varchar(32) DEFAULT NULL,
`praise-num` int(11) DEFAULT NULL,
`publish_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
KEY `mood_user_id_index` (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
数据库表建好之后,在src\main\java\com\example\demo\model\目录下开发对应的Java Bean对象,具体代码如下:
@Entity
@Table(name ="ay_mood")
public class AyMood implements Serializable{
//主键
@Id
private String id;
//说说内容
private String content;
//用户id
private String userId;
/点赞数量
private Integer praiseNum;
//发表时间
private Date publishTime;
//省略set、get方法
}
AyMood实体对象开发完成之后,开发对应的AyMoodRepository接口,具体代码如下:
public interface AyMoodRepository extends JpaRepository<AyMood,String> {
}
Repository接口开发完成之后,开发对应的说说服务层接口AyMoodService和相应的实现类AyMoodServiceImpl。AyMoodService的具体代码如下:
public interface AyMoodService {
AyMood save(AyMood ayMood);
}
AyMoodService代码很简单,只有一个保存说说的方法save(),AyMoodService开发完成之后,实现该接口,具体代码如下:
@Service
public class AyMoodServiceImpl implements AyMoodService {
@Resource
private AyMoodRepository ayMoodRepository;
@Override
public AyMood save(AyMood ayMood) {
return ayMoodRepository.save(ayMood);
}
}
在实现类AyMoodServiceImpl中注入AyMoodRepository接口,并调用其提供的sava()方法,保存说说到数据库。代码开发完成之后,在测试类DemoApplicationTests下添加测试方法:
@Resource
private AyMoodService ayMoodService;
@Test
public void testAyMood(){
AyMood ayMood =new AyMood ();
ayMood.setId("1");
//用户阿毅id为1
ayMood.setUserId("1");
ayMood.setPraiseNum(0);
//说说内容
ayMood.setContent("这是我的第一条微信说说!!!");
ayMood.setPublishTime(new Date());
//往数据库保存一条数据,代码用户阿毅发表了一条说说
AyMood mood = ayMoodService.save(ayMood);
}
测试用例testAyMood开发完成之后,我们允许它。运行成功之后,可以在数据库表ay_mood中看到一条记录。
3.2创建生产者
用户发表说说的类虽然都开发完成了,但是有一个问题,我们知道微信的用户量极大,每天都有几亿的用户发表不同的说说,如果按照上面的做法,用户每发一条说说,后端都会单独开一个线程,将该说说的内容实时地保存到数据库中。由于后端服务系统的线程数和数据库线程池中的线程数量是有限而且宝贵的,将用户发表的说说实时保存到数据库中,必然造成后端服务和数据库极大的压力,所以需要使用ActiveMQ作异步消费,来减轻用户大并发表说说而产生的压力,提高系统的整体性能。
下面来开发生产者和消费者。生产者AyMoodProducer的代码如下:
@Service
public class AyMoodProducer{
@Resource
private JmsMessageingTemplate jmsMessagingTemplate;
public void sendMessage(Destination destination, final String message) {
jmsMessagingTemplate.convertAndSend(destination, message);
}
}
- JmsMessagingTemplate:发消息的工具类,也可以注入JmsTemplate,JmsMessagingTemplate是对JmsTemplate的封装。参数destination是发送到的队列,message是待发送的消息。
3.3创建消费者
生产者AyMoodProducer开发完成之后,我们来开发消费者AyMoodConsumer,具体代码如下:
@Component
public class AyMoodConsumer{
@JmsListener(destination = "ay.queue")
public void receiveQueue(String text){
System.out.println("用户发表" + text + "成功");
}
}
- @JmsListener:使用JmsListener配置消费者监听的队列ay.queue,其中text是接收到的消息。
3.4测试
生产者和消费者开发完成之后,我们在测试类DemoApplicationTests下开发测试方法testActiveMQ (),具体代码如下:
@Resource
private AyMoodProducer ayMoodProducer;
@Test
public void testActiveMQ(){
Destination destincation = new ActiveMQQueue("ay.queue");
ayMoodProducer.sendMessage(destination, "hello,mq!!!");
}
测试方法开发完成之后,运行测试方法,可以在控制台看到打印的信息,如图所示。
同时可以在浏览器中访问http://localhost:8161/admin/查看队列ay.queue的消费情况,具体如图所示。
3.5异步消费模式
生产者和消费者开发完成之后,现在我们把用户发说说改成异步消费模式。首先在AyMoodService类下添加异步保存接口asynSave(),具体代码如下:
public interface AyMoodService{
AyMood save(AyMood ayMood);
String asynSave(AyMood ayMood);
}
然后在类AyMoodServiceImpl下实现asynSave方法,asynSave方法并不保存说说记录,而是调用AyMoodProducer类的sendMessage推送消息,完整代码如下
@Service
public class AyMoodServiceImpl implements AyMoodService {
@Resource
private AyMoodRepository ayMoodRepository;
@Override
public AyMood save(AyMood ayMood) {
return ayMoodRepository.save(ayMood);
}
//队列
private static Destination destination = new ActiveMQQueue("ay.queue.asyn.save");
@Resource
private AyMoodProducer ayMoodProducer;
@Override
public String asynSave(AyMood ayMood){
//往队列ay.queue.asyn.save推送消息,消息内容为说说实体
ayMoodProducer.sendMessage(desination,ayMood);
return "success";
}
}
其次,在AyMoodProducer生产者类下添加sendMessage(Destination destination,final AyMood ayMood)方法,消息内容是ayMood实体对象。AyMoodProducer生产者的完整代码如下:
@Service
public class AyMoodProducer{
@Resource
private JmsMessagingTemplate jmsMessagingTemplate;
public void sendMessage(Destination, final String message) {
jmsMessagingTemplate.convertAndSend(destination, message);
}
public void sendMessage(Destination destination, final AyMood ayMood) {
jmsMessagingTemplate.convertAndSend(destination, ayMood);
}
}
最后,修改AyMoodConsumer消费者,在receiveQueue方法中保持说说记录,完整代码如下:
@Component
public class AyMoodConsumer{
@JmsListener(destination = "ay.queue")
public void receiveQueue(String text){
System.out.println("用户发表" + text + "成功");
}
@Resource
private AyMoodService ayMoodService;
@JmsListener(destination = "ay.queue.asyn.save")
public void receiveQueue(AyMood ayMood) {
ayMoodService.save(ayMood);
}
}
用户发表说说,异步保存所有代码开发完成之后,我们在测试类DemoApplicationTests下添加testActiveMQAsynSave测试方法,具体代码如下:
@Test
public void testActiveMQAsynSave(){
AyMood ayMood =new AyMood ();
ayMood.setId("2");
ayMood.setUserId("2");
ayMood.setPraiseNum(0);
ayMood.setContent("这是我的第一条微信说说!!!");
ayMood.setPublishTime(new Date());
String msg = ayMoodService.asynSave(ayMood);
System.out.println("异步发表说说:" + msg);
}
运行测试方法testActiveMQAsynSave成功之后,可以在数据库表ay_mood查询到用户id为2发表的记录。
4.Spring Boot异步调用
4.1异步调用概述
异步调用是相对于同步调用而言的,同步调用是指程序按预定顺序一步步执行,每一步必须等到上一步执行完成之后才能执行,而异步调用则无须等待上一步程序执行完成即可执行。在日常开发的项目中,当访问的接口较慢或者做耗时任务时,不想程序一直卡在耗时任务上,想让程序能够并行执行,除了可以使用多线程来并行地处理任务,也可以使用Spring Boot提供的异步处理方式@Async来处理。在Spring Boot框架中,只要添加**@Async注解**就能将普通的同步任务改为异步调用任务。
4.2@Async使用
使用@Async注解之前,需要在入口类添加注解@EnableAsync开启异步调用,具体代码如下:
@SpringBootApplication
@ServletComponentScan
@ImportResource(location={"classpath:spring-mvc.xml"})
@EnableAsync
public class MySpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApplication.class, args);
}
}
然后,修改AyUserServiceImpl类的findAll方法,使它能够记录方法执行花费的时间,具体代码如下:
@Override
public List<AyUser> findAll(){
try{
System.out.println("开始做任务");
long start = System.currentTimeMillis();
List<AyUser> ayUserList = ayUserRepository.findAll();
long end = System.currentTimeMillis();
return ayUserList;
}catch (Exception e) {
logger.error("method [findAll] error", e);
return Collections.EMPTY_LIST;
}
}
4.3测试
AyUserServiceImpl类的方法findAll()开发完成之后,在DemoApplicationTests测试类下开发测试方法testAsync(),该方法调用3次findAll(),并记录总共消耗的时间,由于现在是同步调用,所以代码按照顺序一步一步执行。testAsync方法的具体代码如下:
第一次查询所有用户!
开始做任务
完成任务,耗时:371毫秒
第二次查询所有用户!
开始做任务
完成任务,耗时:34毫秒
第三次查询所有用户!
开始做任务
完成任务,耗时:32毫秒
总共消耗:438毫秒
从打印结果可以看出,调用3次findAll(),总共消耗438毫秒。现在在AyUserService接口中添加异步查询方法findAsynAll(),并在AyUserServiceImpl类实现该方法,具体代码如下:
public interface AyUserService {
//省略大量代码
List<AyUser> findAll();
//异步查询
Future<List<AyUser>> findAsynAll();
}
在AyUserServiceImpl类中实现findAsynAll()方法,并在方法上添加异步调用注解@Async,具体代码如下:
本处书中附图错误,无代码,自行猜测
@Override
@Async
public Future<List<AyUser>> findAsynAll(){
try{
System.out.println("开始做任务");
long start = System.currentTimeMillis();
Future<List<AyUser>> ayUserList = ayUserRepository.findAsynAll();
long end = System.currentTimeMillis();
return ayUserList;
}catch (Exception e) {
logger.error("method [findAll] error", e);
return Collections.EMPTY_LIST;
}
}
findAsynAll()方法开发完成之后,在DemoApplicationTests测试类下开发测试方法testAsync2(),具体代码如下:
@Test
public void testAsync2 () throws Exception{
long startTime = System.currentTimeMillis();
System.out.println("第一次查询所有用户!");
Future<List<AyUser>> ayUserList = ayUserService. findAsynAll();
System.out.println("第二次查询所有用户!")
Future<List<AyUser>> ayUserList = ayUserService. findAsynAll();
System.out.println("第三次查询所有用户!")
Future<List<AyUser>> ayUserList = ayUserService. findAsynAll();
while (true){
if(ayUserList.isDone()&& ayUserList.isDone()&& ayUserList3.isDone()){
break;
}else{
Thread.sleep(10);
}
}
long endTime =System.currentTimeMillis();
System.out.println("总共消耗:"+(endTime - startTime)+"毫秒");
}
测试方法testAsync2()开发完成之后,运行它,运行成功之后,可以在控制台看到如下的打印信息。
第一次查询所有用户!
第二次查询所有用户!
第三次查询所有用户!
开始做任务
开始做任务
开始做任务
完成任务,耗时:316毫秒
完成任务,耗时:316毫秒
完成任务,耗时:315毫秒
总共消耗:334毫秒
从上面的打印结果可以看出,testAsync2方法执行速度比testAsync方法快104毫秒(即438-334)。由此说明异步调用的速度比同步调用快。