spring boot 学习笔记(二)(servlet 3.0 异步请求)

我们可以尝试带着如下两个问题来学习servlet 3.0的异步请求模式。

1、是否真的可以提升服务器性能?

2、真实可应用场景?

一、同步请求模式

在了解servlet 3.0规范的异步请求之前,我们有必要先了解一下同步请求,然后我们再考虑,为什么需要异步请求,以及怎么做异步处理?下图是一个客户端发起的http的请求,往返WEB服务器的过程。

005224_mKmb_1589819.png

同步请求的原理:客户端发起一个http请求的时候,线程就会进入状态,直到接受到一个response对象或者请求超时状态。而http请求在经过dns服务器的域名解析,到nginx反向代理转发到我们的WEB服务器(servlet容器),WEB服务器会启动一个请求处理线程来处理请求,完成资源分配处理之后,线程起调后端的处理线程,同时WEB服务器的线程将会进入阻塞状态,直到后端的线程处理完毕,WEB服务器释放请求处理线程的资源,同时返回response对象,客户端接收到response对象,整个请求完成。

此时我们可以看到,假如在后端处理服务器中进行了大量的IO操作,数据库操作,或者跨网调用等等的问题,导致请求处理线程进入长时间的阻塞。因为WEB服务器的请求处理线程条个数是有限的,如果同时大量的请求阻塞在WEB服务器中,新的请求将会处于等待状态,甚至服务不可用,connection refused。

下边,解释一下tomcat 的两个相关的参数:

<Connector port="8080" 
protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
maxThreads="150" 
acceptCount="30"/>

maxThreads:tomcat启动的最大线程数,即同时处理的线程个数,默认值为150

acceptCount:当tomcat起动的线程数达到最大时,接受排队的请求个数,值被我设置为30个。

它们是如何配合工作的呢?分如下三种情况:

A、接受一个请求,此时tomcat起动的线程数没有到达maxThreads,tomcat会启动一个线程来处理此请求。

B、接受一个请求,此时tomcat起动的线程数已经到达maxThreads,tomcat会把此请求放入等待队列,等待空闲线程。

C、接受一个请求,此时tomcat起动的线程数已经到达maxThreads,等待队列中的请求个数也达到了acceptCount,此时tomcat会直接拒绝此次请求,返回connection refused。

如此来说,我们的WEB服务器的maxThreads是有限值,即便有acceptCount做一个缓冲队列,请求可以进入队列中等待,那也有一个上限,并且在这最大的线程数中,有多少是分配给了请求处理线程?

二、异步请求模式

012057_p05I_1589819.png

请求处理线程调了之后直接返回,而不等待,这样请求处理线程就“自由”了,它可以接着去处理别的请求,当后端处理完成后,会钩起一个回调处理线程来处理调用的结果,这个回调处理线程跟请求处理线程也许都是线程池中的某个线程,相互间可以完全没有关系,由这个回调处理线程向浏览器返回内容。这就是异步的过程。

而这个回调线程,我们也可以启用本地系统线程,并非一定要从WEB服务器线程池中取。

servlet3.0规范增加了对异步的支持,在servlet3.0规范之前,客户端请求到达servlet后,servlet通常会执行一些比较耗时的外部操作,比如数据库操作、I/O操作、跨网络调用等,往往会阻塞当前servlet线程,当前的线程是由servlet容器(tomcat)管理并分配的,容器线程池为请求分配的线程会持有一系列的servlet资源,因为该线程会调用一系列方法(大部分为tomcat内部方法),而方法内部可能会持有很多线程私有的对象,比如在一个有过滤器的web应用中,每个线程将持有私有的filterChain对象。如果阻塞时间过长,那么在这段时间内此线程无法被回收,为资源分配的内存一直被占用,无法被GC回收或者返回Object pool,且该线程也无法分配给其他客户端请求,在一定程度上会造成并发量的降低。

异步的目的:使后台操作异步,提早回收由容器管理的servlet线程。

我会用spring boot的例子来分析异步的整个过程,同时也会将原生servlet的异步实现打包上传上来(代码是山茶果先生写的,在下盗来使用)。

我们先看一下我们任务执行程序:

@Service
public interface TaskService {
	Map<String, Object> execute();
}

/**
* 任务处理
*
* 
*/
@Service
public class TaskServiceImpl implements TaskService {
	private final Logger logger = LoggerFactory.getLogger(this.getClass());

	@Override
	public Map<String, Object> execute() {
		Map<String, Object> map = new HashMap<String, Object>();
		List<String> tasks = new ArrayList<String>();

        // 创建10个任务
		for (int i = 0; i < 10; i++) {
			tasks.add("task:" + i);
		}

        // 循环处理10个任务,并借此打印当前的线程名称(通过线程名称可以看出线程是WEB服务器线程池取得,还是普通的线程)
		try {
			for (String task : tasks) {
				Thread.sleep(1 * 1000L);
				Thread current = Thread.currentThread();
				logger.error(current.getName() + "执行进度:" + task + "/" + tasks.size());
			}
			map.put("resut", "ok");
			return map;
		} catch (InterruptedException e) {
			throw new RuntimeException();
		}
	}
}

OK,那么我们现在就通过创建spring boot程序,以及rest api来处理我们定下的任务。

@SpringBootApplication
@EnableAsync // 支持异步请求
public class TestAsyncApplication {

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

spring提供了Callable<T> 接口来支持异步请求(从WEB服务器的线程取资源)

/** jdk8 的写法*/
@RequestMapping("asyncCallable")
public Callable<Map<String, Object>> asyncCallable() {
	logger.error("async start");
	Callable<Map<String, Object>> callable = taskService::execute;
	logger.error("async end");
	return callable;
}

/** 通用写法*/	
@RequestMapping("asyncCallable")	
public Callable<Map<String, Object>> asyncCallable() {
	logger.error("async start");
	Callable<Map<String, Object>> callable = new Callable<Map<String, Object>> (){

		@Override
		public Map<String, Object> call() throws Exception {
			// TODO Auto-generated method stub
			return taskService.execute();
		}
	};
	logger.error("async end");
	return callable;
}

spring提供了DeferredResult<T> 接口来支持异步异步请求(从操作系统中线程调度中取资源)


@RequestMapping("asyncDeferred")
public DeferredResult<Map<String, Object>> deferredResult() {
	logger.error("async start");
	DeferredResult<Map<String, Object>> deferredResult = new DeferredResult<>();
	CompletableFuture.supplyAsync(taskService::execute)
			.whenCompleteAsync((result, throwable) -> deferredResult.setResult(result));
	logger.error("async end");
	return deferredResult;
}

为了可以可以清楚的感受到线程处理的现象,我们可以分几步来操作。

1、不开启server.tomcat.max-threads这个参数,默认tomcat的最大线程数,默认应该是150个。

2、开启server.tomcat.max-threads=2,通过只有两个处理线程的情况可以清楚看到容器处理的情况。

这两步就不跑了,我们直接打开charles,在最大线程数为2的时候,并发10个请求的效果。

A、同步请求

201942_Fzp7_1589819.png

201734_IPQn_1589819.png

以上结果为我们发起十个并发请求的结果。我们可以看到当我们并发十个请求的时候,由于最大线程只有两个,也就是同时只有两个线程任务在处理,那么其他请求要么阻塞在tomcat的容器队列中,等待处理线程的资源,再多的请求有可能发生请求拒绝的情况了。

B、异步请求

201001_ptWf_1589819.png

 

 

 

 

 

 

以上结果为发起十个并发请求,我们可以看到线程请求线程几乎是同时处理完。也就说,在开启异步请求的模式下,请求到达WEB服务器,即可会分配请求处理线程进行处理,再另起线程去处理任务,而请求处理线程得到释放,就可以立刻接待新进来的请求。在任务处理完成之后,会勾起一个回调处理线程,将reponse的结果返回,终止一次http请求。

第二种异步接口可以自行执行测试,可以看到线程名称的不同。

三、小结:

异步请求的模式,最终结果集就是将线程处理的压力转交给其他线程,而解放了WEB服务器中的请求处理线程的压力,让他们可以更多的接受其他的http请求。

如果服务器的压力是CPU,I/O处理能力的话,那么异步请求的方法似乎并不能带来什么优势,相反会带来更多线程开启/释放的资源损耗。

到这里,对于servlet 3.0规范异步请求模式应该有所了解,那么回到我们最初的那个:

1、是否真的可以提升服务器性能?

2、真实可应用场景?

 

 

转载于:https://my.oschina.net/u/1589819/blog/979048

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值