最近的项目中有个需求是这样的,用户发起一次查询请求,后台需要在数据量很大的数据库表中查询相关数据然后生成一份PDF文档。由于这个过程十分漫长,而且还涉及到与另一个系统的交互,所以用户想设计成异步的形式。也就是说用户点击查询,后端接收到后会立刻返回给前端,这个过程占用的web服务器的线程很快可以释放,但是后端处理服务器会有一个【任务线程】处理刚刚用户的点击查询逻辑。
本来一开始我想采用SpringMVC提供的异步线程(其实是Servlet3.0提供的),整个设计如下图所示
但是后来仔细想了一下,采用异步线程的方式会增大系统的复杂程度还可能会出现一些不可控的因素,而普通的Java提供的多线程也能满足我的需求,于是决定采用多线程方式完成生产者消费者模式的业务。
采用普通的多线程后的设计流程如下:
用户从web容器中获得线程后调继续调用后端服务器的处理线程,而后端服务器会将任务丢给【任务线程】执行真正的业务逻辑。在【任务线程】执行期间,我们就已经将响应(Respose)返回给前端了,web容器中的线程也得到了释放,但此时后端处理服务器其实还在处理着刚刚用户的操作。
接下来我们就以SpringBoot作为基础框架,实现一次这样的业务流程。
首先我们需要创建一个队列,该队列用于存放我们需要处理的任务。可以想象为就是一个任务列表,用户发起请求,这个请求会放在这个队列中
/**
* 监听队列
*/
@Component
@Slf4j
public class ListenQueue extends Thread {
public static BlockingQueue queue = new ArrayBlockingQueue<>(16);@Autowiredprivate UserService userService;@SneakyThrows@Overridepublic void run() {while (true) {
UserEntity userEntity = queue.take();// 模拟超长的业务处理
Thread.sleep(5000);
userService.save(userEntity);
}
}
}
代码中的BlockingQueue就是任务列表,里面存放的UserEntity是我们需要处理的业务。代码中的queue.take();
为阻塞式,也就是说当queue中存在元素时就会取出来,不存在则此线程一直等待在这里。
这个队列会被添加进Spring的IOC容器中,并且会在IOC容器将所用的Bean加载完毕后执行。如下代码
/**
* IOC加载全部的Bean后执行此事件
*/
public class ApplicationStartup implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 获得IOC
ApplicationContext ioc = event.getApplicationContext();
// 获得任务类
ListenQueue listenQueue = ioc.getBean(ListenQueue.class);
// 开启任务线程
listenQueue.start();
}
}
在SpringBoot的启动类中还需要加上这个监听器
/**
* 项目启动类
*/
@SpringBootApplication
@MapperScan("com.ethan.mapper")
public class UserApplication {
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(UserApplication.class);
springApplication.addListeners(new ApplicationStartup());
springApplication.run(args);
}
}
至此,【任务线程】就会随着项目启动而启动,并且启动后一直监听者队列中的消息,如果有任务则会拿来消费。
我们模拟一次Http调用
/**
* 测试
*/
@RestController
public class AsyncController {
@PostMapping("/test/testThread")
public ResponseEntity testThread(@RequestBody UserEntity userEntity) {
// 把任务丢进队列中后立刻返回给前端
ListenQueue.queue.offer(userEntity);
return ResponseEntity.ok(userEntity);
}
}
可以看到前端返回的数据没问题
再看看后端控制台的打印。输出的MyBatis Log也显示已经存入数据库中
总结:通过采用普通多线程的方式能够实现异步编程,在一些处理业务耗时长的场景中,我们可以采用这种生产消费的模式避免长时间占用web容器中的线程,提高系统的健壮性。