在系统开发过程中,我们经常会用到池化技术来减少系统消耗,提升系统性能。对象池通过复用对象来减少创建对象、垃圾回收的开销。连接池(数据库连接池、Redis连接池、HTTP连接池)通过复用TCP连接来减少创建和释放连接的时间。线程池通过复用线程提升性能。简单来说,池化技术就是通过复用来提升性能。
一、数据库连接池
数据库连接池基本的思想是在系统初始化的时候,将数据库连接作为对象存储在内存中,当用户需要访问数据库时,并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象。使用完毕后,用户也并非将连接关闭,而是将连接放回连接池中,以供下一个请求访问使用。而连接的建立、断开都由连接池自身来管理。同时,还可以通过设置连接池的参数来控制连接池中的初始连接数、连接的上下限数以及每个连接的最大使用次数、最大空闲时间等等,也可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。
DBCP是一个依赖 commons-pool对象池机制的数据库连接。DBCP可以直接的在应用程序中使用,Tomcat的数据源使用的就是DBCP。
DBCP关键配置:
最小连接数: minIdle
数据库一直保持的数据库连接数,如果应用程序对数据库连接的使用量不大,将有大量的数据库资源被浪费。
最大空闲连接数:maxIdle
数据库允许的最大空闲连接数,超过的空闲连接将自动被释放。
初始化连接数:initialSize
连接池启动时创建的初始化数据库连接数量。
最大连接数:maxTotal
连接池能申请的最大连接数,如果数据库连接请求超过此数,后面的数据库连接请求被加入到等待队列中。
最大等待时间:maxWaitMillis
当没有可用连接时,连接池等待连接被归还的最大时间,超过时间则抛出异常,可设置参数为0或者负数使得无限等待(根据不同连接池配置)。但设置超时时间太长容易造成大量的TIMED_WAIT和线程阻塞,造成滚雪球的效果,一旦出现问题很难恢复。
二、HttpClient连接池
HttpClient 我们经常用来进行HTTP服务访问。我们的项目中有一个获取任务执行状态的功能使用HttpClient,一秒钟请求一次,经常出现Connection reset异常。经过分析发现,由于我们的httpClient每次请求都新建一个连接,创建连接的频率比连接关闭的频率大,导致了系统中产生了大量处于TIME_CLOSED状态的连接。这个时候使用连接池复用连接就解决了这个问题。
HttpClient连接池实现源码
response = requestExecutor.execute(request, managedConn, context);
if(reuseStrategy.keepAlive(response, context)){
final long duration=keepAliveStrategy.getKeepAliveDuration(response, context);
connHolder.setValidFor(duration, TimeUnit,.MILLISENCONDS);
connHolder.markReuseable();
}else{
connHolder.markNonReuseable();
}
从上述源码可以看出,HttpClient请求的步骤为:
1、发生请求并接收响应
2、判断响应是否长连接(长连接才可复用),非长连接标记为不可复用
3、获取长连接超时周期(如果没有设置,则永不过期)
4、设置过期周期,并标记为可复用
HttpClient通过PoolingHttpClientConnectionManager类来管理连接池,可以同时为很多线程提供http连接请求。当请求一个新的连接时,如果连接池有可用的持久连接,连接管理器就会使用其中的一个,而不是再创建一个新的连接。
PoolingHttpClientConnectionManager维护的连接数在每个路由基础和总数上都有限制。默认,每个路由基础上的连接不超过2个,总连接数不能超过20。在实际应用中,这个限制可能会太小了,尤其是当服务器也使用Http协议时
下面的例子演示了如果调整连接池的参数:
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// 将连接池最大连接数设置到200
cm.setMaxTotal(200);
// 将每个路由最大连接数设置为20
cm.setDefaultMaxPerRoute(20);
HttpHost localhost = new HttpHost("www.xxx.com", 80);
//将目标主机的最大连接数增加到50
cm.setMaxPerRoute(new HttpRoute(localhost), 50);
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(cm)
.build();
三、Tomcat线程池
tomcat线程池有如下参数:
acceptCount: 等待队列大小 ,当Tomcat没有可用空闲线程处理连接请求时,新来的连接请求将放入等待队列中。当队列超过acceptCount时,后续的连接请求就会被拒绝。
maxThreads:线程池最大线程数,tomcat能创建来处理请求的最大线程数,当线程池空闲一段时间后会释放到只保留minSpareThreads个线程。
maxSpareThreads:最大空闲线程数,在最大空闲时间内活跃过,但现在处于空闲,若空闲时间大于最大空闲时间,则回收,小于则继续存活,等待被调度。
minSpareTHreads:最小空闲线程数,无论如何都会存活的最小线程数。
maxIdleTime: 最大空闲时间,超过这个空闲时间,且线程数大于最小空闲数的,都会被回收。
Tomcat线程池在工作的时候,实际情况是:一开始就创建最小空闲数的线程在池里,20个,当同一时间请求数量大于最小空闲数20,比如来了50个并发请求,那么线程池还需要创建30个线程来处理请求。这时候当请求都处理完了,持续来的请求低于50个的时候,那么当时间过了60秒,并发数还是没有达到50,那么从第50个线程开始,线程池将按照,空闲时间达到60s的,开始逐个回收,49个,48个,47个,如此回收。如果并发请求小于20个,那么线程池会回收至20个的时候,停止回收,这就是最小空闲数的作用,即使一个请求都没有,那么线程池也得保证随时都有20个。所谓空闲回收是指:一个线程在60s的时间内,一直处于等待。那么就可以判定该线程是空闲。如果这个空闲线程是在最小空闲数以上,则会被回收。当请求并发高于500最大空闲数的时候,线程池是会继续创建线程的,来满足特大突发性并发。当并发请求数降下之后,线程池中有空闲,那么,无论线程空闲时间是否达到60s,线程池都会进行回收至500。500以内的线程也会根据空闲时间是否大于60s来判断是否需要进行回收。
四、总结
通过数据库连接池、HttpClient连接池、Tomcat线程池这三个最常用的案例不难看出,
这几个池化技术共同点实现的原理都是类似的,通过对连接或线程的复用,并对复用的数量、时间等进行控制,从而使系统的性能和资源消耗达到最优的状态。实际项目中,我们可以考虑使用池化技术来解决程序性能的瓶颈。