java SpringMVC异步编程:提升系统吞吐能力

使用spring boot搭建web项目时,spring boot会内置一个tomcat作为web服务器,每一次请求落到tomcat上时,tomcat都会把请求封装成一个servlet,在传统的开发过程中servlet的生命周期会从tomcat创建它开始一直到业务逻辑处理完成并返回处理结果,换句话说在一次请求过程中servlet是独占的。

tomcat的默认并发量maxThreads=200,意味着tomcat同时只能支持200个并发量,当并发量大于200时,就会出现等待、延迟等情况。现在有很多种方法可以解决并发量问题,如集群、负载等,今天我介绍一个在代码层面优化的方法来解决tomcat吞吐量的方法。

如果在处理请求时,tomcat主线程把业务逻辑交给其他线程执行,自身则立刻释放,这样tomcat的线程就会大大减少tomcat线程的被占用事件,增加tomcat的吞吐能力,从而提高并发量。有两种方式可以实现这个思路:

通过Callable创建线程替代tomcat主线程

DeferredResult方式返回数据

通过这两种方式可以立即释放tomcat主线程,如果我们的代码里用到了ThreadLocal线程内部存储类,ThreadLocal里面的数据会在线程切换的时候不被传递,Spring提供了一个子类InheritableThreadLocal以支持父子线程间的值传递。 实例源码:https://gitee.com/topduang/test-service.git

test-servic/web/src/main/java/com/yc/web/controller/AsyncController.java

1. 首先创建2个方法,手动延时2s,代表2次复杂的业务处理

@Override
public UserVo getByIdAsync(int id) throws Exception {
Long startTime = System.currentTimeMillis();
System.out.println("start getByIdAsync");
UserVo userVo = new UserVo();
userVo.setId(id);
userVo.setName("YC");
Thread.sleep(2000);
System.out.println("end getByIdAsync:" + (System.currentTimeMillis() - startTime));
return userVo;
}
@Override
public void getByIdDeff(int id, DeferredResult deferredResult) throws Exception {
Long startTime = System.currentTimeMillis();
System.out.println("start getByIdDeff");
UserVo userVo = new UserVo();
userVo.setId(id);
userVo.setName("YC");
Thread.sleep(2000);
deferredResult.setResult(ResultInfo.Success(userVo));
System.out.println("end getByIdDeff:" + (System.currentTimeMillis() - startTime));
}

2. 分别通过Callable和DeferredResult创建两个API

在controller层创建方法调用上述业务方法:

@ApiOperation(response = ResultInfo.class, value = "获取分页列表", notes = "获取分页列表")
@GetMapping(value = "/getByIdAsync")
public Callable getByIdAsync(@ApiParam(value = "id") @RequestParam int id) throws Exception {
System.out.println("start main thread:" + Thread.currentThread());
Callable result = () -> ResultInfo.Success(asyncService.getByIdAsync(id));
System.out.println("end main thread:" + Thread.currentThread());
return result;
}
@ApiOperation(response = ResultInfo.class, value = "获取分页列表", notes = "获取分页列表")
@GetMapping(value = "/getByIdDeff")
public DeferredResult getByIdDeff(@ApiParam(value = "id") @RequestParam int id) throws Exception {
System.out.println("start main thread:" + Thread.currentThread());
DeferredResult deferredResult = new DeferredResult(3000L, "fail");
new Thread(() -> {
try {
asyncService.getByIdDeff(id, deferredResult);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
System.out.println("end main thread:" + Thread.currentThread());
return deferredResult;
}

3. 测试

分别对两种异步方法进行测试:

调用getByIdAsync,看到控制台打印日志,可以看到main thread被返回之后才处理了业务逻辑:

start main thread:Thread[http-nio-10000-exec-3,5,main]
end main thread:Thread[http-nio-10000-exec-3,5,main]
start getByIdAsync
end getByIdAsync:2000

调用getByIdDeff,同理:

start main thread:Thread[http-nio-10000-exec-7,5,main]
end main thread:Thread[http-nio-10000-exec-7,5,main]
start getByIdDeff
end getByIdDeff:2000

后记

在DeferredResult方式中还可以把动态创建线程的函数用线程池代替,进一步优化因线程创建释放浪费的资源。