开发过程中会用到很多的连接池,像是数据库连接池、HTTP 连接池、Redis 连接池等等。而连接池的管理是连接池设计的核心
1.连接池
(1)连接池管理的关键点
数据库连接池有两个最重要的配置:最小连接数和最大连接数,它们控制着从连接池中获取连接的流程:
- 如果当前连接数小于最小连接数,则创建新的连接处理数据库请求;
- 如果连接池中有空闲连接则复用空闲连接;
- 如果空闲池中没有连接并且当前连接数小于最大连接数,则创建新的连接处理请求;
- 如果当前连接数已经大于等于最大连接数,则按照配置中设定的时间(C3P0 的连接池配 置是 checkoutTimeout)等待旧的连接可用;
- 如果等待超过了这个设定时间则向用户抛出错误。
- 在这里,需要注意池子中连接的维护问题,有的连接虽然还存在,但有的时候会有故障:
- 数据库的域名对应的 IP 发生了变更,池子的连接还是使用旧的 IP,当旧的 IP 下的数据 库服务关闭后,再使用这个连接查询就会发生错误;
- MySQL 有个参数是“wait_timeout”,控制着当数据库连接闲置多长时间后,数据库会 主动的关闭这条连接。这个机制对于数据库使用方是无感知的,所以当使用这个被关闭 的连接时就会发生错误。 有以下解决方案
-
- 启动一个线程来定期检测连接池中的连接是否可用,比如使用连接发送“select 1”的命 令给数据库看是否会抛出异常,如果抛出异常则将这个连接从连接池中移除,并且尝试关闭。目前 C3P0 连接池可以采用这种方式来检测连接是否可用。
- 在获取到连接之后,先校验连接是否可用,如果可用才会执行 SQL 语句。比如 DBCP 连 接池的 testOnBorrow 配置项,就是控制是否开启这个验证。这种方式在获取连接时会引 入多余的开销,在线上系统中还是尽量不要开启,在测试服务上可以使用。
(2)httpclient连接池设置
HttpConnectionManager httpConnectionManager = new MultiThreadedHttpConnectionManager();
HttpConnectionManagerParams params = httpConnectionManager.getParams();
params.setConnectionTimeout(5000);
params.setSoTimeout(20000);
params.setDefaultMaxConnectionsPerHost(32);//每个host路由的默认最大连接
params.setMaxTotalConnections(256);//qps*建立连接时间*预留时间(一般是1.7)
(3)为什么不用IO多路复用
DB 访问一般采用连接池这种现象是生态造成的。历史上的 BIO + 连接池的做法经过多年的发展,已经解决了主要的问题。在 Java 的大环境下,这个方案是非常靠谱的,成熟的。而基于 IO 多路复用的方式尽管在性能上可能有优势,但是其对整个程序的代码结构要求过多,过于复杂。当然,如果有特定的需要,希望使用 IO 多路复用管理 DB 连接,是完全可行的。
(4)数据库连接为什么费资源
MySQL 的通信协议是基于 TCP 传输协议的,所以需要经过三次握手和四次挥手
2.用线程池预先创建线程
(1)线程池简介
JDK 1.5 中引入的 ThreadPoolExecutor 就是一种线程池的实现,它有两个重要 的参数:coreThreadCount 和 maxThreadCount
- 如果线程池中的线程数少于 coreThreadCount 时,处理新的任务时会创建新的线程;如果线程数大于 coreThreadCount 则把任务丢到一个队列里面,由当前空闲的线程执 行;
- 当队列中的任务堆积满了的时候,则继续创建线程,直到达到 maxThreadCount;当线程数达到 maxTheadCount 时还有新的任务提交,那么我们就不得不将它们丢弃 了。
(2)线程池设置
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
3.什么时候考虑使用池化
当遇到下面的场景,就可以考虑使用池化来增加系统性能:
- 对象的创建或者销毁,需要耗费较多的系统资源;
- 对象的创建或者销毁,耗时长,需要繁杂的操作和较长时间的等待;
- 对象创建后,通过一些状态重置,可被反复使用。
将对象池化之后,只是开启了第一步优化。要想达到最优性能,就不得不调整池的一些关键参数,合理的池大小加上合理的超时时间,就可以让池发挥更大的价值。和缓存的命中率类似,对池的监控也是非常重要的。