对于WEB项目开发,涉及网络调用时,连接池是很常用的技术。为什么要使用连接池技术,连接池带来的优势具体体现在哪里,连接池大小是不是越大越好等这些问题是开发过程中技术选型需要思考的问题,本文通过一个简单的案例揭开关于连接池的原理。
以数据库连接为例,考虑一个不用连接池的并发应用,当一个线程执行任务要连接mysql
数据库时,接下来的动作大概如下: 应用调用内核开启一个socket
,通过tcp
三次握手连接到数据库,发送mysql
报文,mysql
数据库接收到报文解析执行并返回,应用获取返回报文解析,四次挥手关闭连接。这些操作涉及TCP
网络连接频繁创建释放等,会导致调用者等待,而耗费不必要的时间。
当没有并发或比较小时,每次TCP
网络连接释放等操作不会对服务性能有太大影响,现代OS的多核系统可以很高效的处理。然而在高并发下时,就会有大量的任务线程执行任务时都需要经过以上流程才能获取连接,CPU
需要来回调度线程、存在大量的上下文切换、用户态内核态转换开销,线程运行等待时间太长导致服务整体性能变差。
那么有没有什么方法可以减小耗时、提升性能呢?这就需要用到数据库连接池技术。
连接池就是在应用程序中维护一定数量的网络连接,对外暴露获取连接和释放连接的方法,应用需要用到连接时就从连接池中获取、用完后就回收。这样可以重复利用网络连接,避免频繁的创建、关闭连接。
TCP
网络涉及到3个过程:三次握手创建连接、传输应用报文、四次挥手关闭连接。当经过3次握手创建完连接后,TCP
网络双方处于ESTABLISHED
状态,此时双方可以传输报文;如果没有报文传输时,此连接状态默认可以keepalive
(保活)2h时,在此期间只要应用端口正常,双方都认为是ESTABLISHED
;当不需要连接时,一方主机可以发起FIN
经过四次挥手后可以关闭释放连接。
所以应用连接池技术创建连接放入连接池中维护,当需要的时候从连接池中获取ESTABLISHED
状态的连接,当使用完毕后,放入连接池中待下次取用。并发时每个线程获取一个连接使用,理想情况下是每个CPU
调度一个线程执行,充分发挥多核CPU
并行执行的优势。
/**
* mysql数据库连接池demo
*/
@Slf4j
public class MysqlConnectionPool {
private String url;
private String userName;
private String password;
/**
* 连接池大小,默认为8
*/
private int poolSize = 8;
private long waitTime = -1L;
/**
* 连接池维护队列
*/
private BlockingQueue<Connection> idleQueue = new LinkedBlockingQueue<>();
public MysqlConnectionPool(String url, String userName, String password) {
this.url = url;
this.userName = userName;
this.password = password;
}
/**
* 初始化连接池
* @throws Exception
*/
public void init() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
try {
for (int i = 0; i < poolSize; i++) {
Connection connection = DriverManager.getConnection(url, userName, password);
DatabaseMetaData metaData = connection.getMetaData();
//log.info("====> schema {}", metaData);
idleQueue.add(connection);
}
} catch (Exception e) {
throw new RuntimeException("init connect pool error");
}
}
public Connection getConnection() throws Exception {
if (-1L == this.waitTime) {
return idleQueue.take();
}
return idleQueue.poll(waitTime, TimeUnit.MILLISECONDS);
}
public Connection getConnection(long waitTime) throws Exception {
return idleQueue.poll(waitTime, TimeUnit.MILLISECONDS);
}
/**
* 返回连接
* @param connection
* @throws Exception
*/
public void returnConnection(Connection connection) throws Exception {
idleQueue.put(connection);
}
/**
* 设置连接池大小
* @param size
*/
public void setPoolSize(int size) {
if (size < 0) {
throw new IllegalArgumentException("size must large than 0");
}
this.poolSize = size;
}
/**
* 设置获取连接等待时间
* @param time 等待时间 millseconds
*/
public void setWaitTime(long time) {
if (time < 0) {
throw new IllegalArgumentException("time must large than 0");
}
this.waitTime = time;
}
}
3000
线程并发下,测试多组poolSize
参数下执行时间。
执行sql:
SELECT * FROM `user`;
取连接池大小2 ~ 700
参数,通过多线程模拟并发下的执行性能。测试代码如下:
@Test
public void testExecuteConnection() throws Exception {
testExecuteTest(3000, 2);
testExecuteTest(3000, 10);
testExecuteTest(3000, 20);
testExecuteTest(3000, 50);
testExecuteTest(3000, 100);
testExecuteTest(3000, 200);
testExecuteTest(3000, 300);
testExecuteTest(3000, 400);
testExecuteTest(3000, 500);
testExecuteTest(3000, 600);
testExecuteTest(3000, 700);
}
public ExecuteInfo testExecuteTest(int threadSize, int poolSize) throws Exception {
MysqlConnectionPool connectionPool = new MysqlConnectionPool(url, userName, password);
connectionPool.setPoolSize(poolSize);
connectionPool.init();
ExecuteInfo executeInfo = new ExecuteInfo(0, Integer.MAX_VALUE, 0);
Long maxTime = 0L;
Long minTime = (long)Integer.MAX_VALUE;
CountDownLatch downLatch = new CountDownLatch(threadSize);
for(int i = 0; i < threadSize; i++) {
new Thread(new Runnable() {
@Override
public void run() {
long s1 = System.currentTimeMillis();
// 执行语句
Connection connection = null;
try {
connection = connectionPool.getConnection();
boolean execute = connection.prepareStatement(sql).execute();
//log.info("====> sql excute res is {}", execute);
} catch (Exception e) {
log.info("===> {}", e);
} finally {
if (null != connection) {
try {
connectionPool.returnConnection(connection);
} catch (Exception e) {
e.printStackTrace();
}
}
}
long s2 = System.currentTimeMillis();
long thisTime = s2 - s1;
if (thisTime > executeInfo.getMaxTime()) {
executeInfo.setMaxTime(thisTime);
}
if (thisTime < executeInfo.getMinTime()) {
executeInfo.setMinTime(thisTime);
}
executeInfo.setMeanTime(executeInfo.getMeanTime() + thisTime);
downLatch.countDown();
}
}).start();
}
downLatch.await();
executeInfo.setMeanTime(executeInfo.getMeanTime() / threadSize);
log.info("====> poolSize {} ; executeInfo {}", poolSize, executeInfo.toString());
return executeInfo;
}
@Data
class ExecuteInfo {
private long maxTime;
private long minTime;
private long meanTime;
public ExecuteInfo(long maxTime, long minTime, long meanTime) {
this.maxTime = maxTime;
this.minTime = minTime;
this.meanTime = meanTime;
}
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
}
}
poolSize 2 ; executeInfo {"maxTime":36970,"minTime":22,"meanTime":18720}
poolSize 10 ; executeInfo {"maxTime":7111,"minTime":21,"meanTime":3439}
poolSize 20 ; executeInfo {"maxTime":4957,"minTime":24,"meanTime":2348}
poolSize 50 ; executeInfo {"maxTime":4773,"minTime":41,"meanTime":2346}
poolSize 100 ; executeInfo {"maxTime":5084,"minTime":21,"meanTime":2315}
poolSize 200 ; executeInfo {"maxTime":5698,"minTime":26,"meanTime":2806}
poolSize 300 ; executeInfo {"maxTime":7338,"minTime":22,"meanTime":2684}
poolSize 400 ; executeInfo {"maxTime":8085,"minTime":22,"meanTime":2698}
poolSize 500 ; executeInfo {"maxTime":8503,"minTime":22,"meanTime":2895}
poolSize 600 ; executeInfo {"maxTime":8549,"minTime":29,"meanTime":2805}
poolSize 700 ; executeInfo {"maxTime":7954,"minTime":23,"meanTime":2918}
从以上结果我们可以得到这样的结论:
mysql
连接池数量为20 ~ 100
平均执行时间meanTime
最少,彼此相差不大。如上测试结果可知,在连接池大小为20~100
时平均执行时间比较小,彼此相差不大,此时优先选择较小的连接池大小,因为连接池越大,初始化创建TCP
连接需要的时间也越大,而且需要的内存也越大。所以在实际使用时需要综合考虑各方面的因素选择。
连接池中的连接是被线程调用的,所以池大小选择可以参考线程池大小的原则:
size = N_cpu + 1
;size = N_cpu * U * (1 + W / C)
.N_cpu
为服务器核心数;U
为CPU利用率;W / C
为执行等待时间与计算时间的比值(IO密集型应用等待时间会较长,这个参数一般需要更详细的测试)。我们知道线程是调度单位,连接池数量越大时,相同数量的并发线程都获取到这些连接时,由于CPU
基于时间片调度,会在大量的线程间来回上下文切换,疲于应对,产生较高的开销(如保存读取线程context
信息),所以导致的后果是有时候可能不如线程串行执行效率高。
文章介绍了连接池的原理,实现了一个简单的数据库连接池Demo,并进行高并发测试,发现连接池大小并不是越大越好,从而引出池大小选择原则。当然,实际项目使用时并不是像本文简单,需要根据实际情况,比如并发量、IO延时,找出对应的瓶颈,然后找出最优化的方案,如数据库集群分担查询流量、服务集群提高吞量等等。