在MybatisPlus启动阶段曾介绍过数据源的实例化,那时还没有真正连接数据库,MP系列中对于数据库的使用也是一笔带过,本篇将以druid连接池为例,介绍数据库连接池相关内容。
与线程池概念相似,数据库连接池是对数据库连接的管理,我们知道Web应用与数据库的连接非常频繁,倘若每次都连接再关闭,这是对资源和性能的一种浪费,数据库连接池的出现,使得与数据库连接时可以直接从池中获取,用完再还给连接池,连接池还能对资源进行管理,如资源的创建、回收、关闭等等。
连接
// 数据源dataSource是DruidDataSourceWrapper类,它是DruidDataSource的子类
// getConnection()是获取数据库连接的入口方法
DruidDataSource->getConnection():
return getConnection(maxWait);
DruidDataSource->getConnection():
// 实例化DruidDataSourceWrapper的时候,已经调用过一次了,这里直接返回
init();
// 使用责任链模式,最终调用getConnectionDirect()方法
......
DruidDataSource->getConnectionDirect():
poolableConnection = getConnectionInternal(maxWaitMillis);
DruidDataSource->getConnectionInternal():
// maxWait默认是-1,走下面的else方法
if (maxWait > 0) {
holder = pollLast(nanos);
} else {
holder = takeLast();
}
DruidDataSource->takeLast():
// poolingCount是连接池的数量
while (poolingCount == 0) {
emptySignal(); // --1
......
notEmpty.await(); // --2
复制代码
第一次获取数据库连接,一定会走到–1处的方法,看这个方法的名称,我们就知道与多线程有关。
DruidDataSource->emptySignal():
if (createScheduler == null) {
empty.signal();
return;
}
复制代码
启动阶段实例化数据源的时候,创建连接的线程调用了empty.await()
,这里调用empty.signal()
将其唤醒。主线程返回,在–2处阻塞。
DruidDataSource->CreateConnectionThread->run():
for (;;) {
......
// 防止创建超过maxActive数量的连接
// 这里的activeCount是资源分配出去但是还没有回收的连接数量
if (activeCount + poolingCount >= maxActive) {
empty.await(); // --3
continue;
}
// 建立连接,返回的是PhysicalConnectionInfo
// 连接过程与JDBC大同小异,不做解释
connection = createPhysicalConnection();
......
// 连接成功的情况下调用此方法
boolean result = put(connection);
}
DruidDataSource->put():
// holder将物理连接又做了一次封装
return put(holder);
DruidDataSource->put():
if (poolingCount >= maxActive) {
return false;
}
connections[poolingCount] = holder;
// 连接池数量poolingCount加1
incrementPoolingCount();
......
notEmpty.signal(); // --4
复制代码
调用notEmpty.signal()
唤醒–2处的阻塞,此时poolingCount已经不为0了,跳出循环。
DruidDataSource->takeLast():
// 连接池数量poolingCount减1
decrementPoolingCount();
// 从connections数组中获取一个连接并返回
DruidConnectionHolder last = connections[poolingCount];
connections[poolingCount] = null;
return last;
复制代码
从代码中可以知道,从连接池获取连接实际上是从connections数组中取值,然后将其下标置null,获取连接有非常多种情况,这里分析的只是最简单的一种。
回收
用完连接后,调用connection.close()
关闭连接,这里的connection是DruidPooledConnection对象,它的关闭方法最终会调用dataSource.recycle()
。
DruidDataSource->recycle():
......
result = putLast(holder, lastActiveTimeMillis);
DruidDataSource->putLast():
// 这里的e即DruidConnectionHolder对象,是一个物理连接
connections[poolingCount] = e;
incrementPoolingCount();
......
notEmpty.signal(); // --5
复制代码
对于线程池来说是回收连接,过程比较简单,就是把物理连接赋值给connections的下标,然后某种情况下,–2处有线程阻塞的话将其唤醒。
属性配置
yml配置:
datasource:
# 数据源属性
......
druid:
min-idle: 1
max-active: 2
max-wait: 10
validation-query: SELECT 1
time-between-eviction-runs-millis: 5
min-evictable-idle-time-millis: 5
max-evictable-idle-time-millis: 50
复制代码
第一次连接并回收后,再次获取连接,此时poolingCount已经不为0了,直接从connections数组中取值即可。
最大连接数
在同一线程中获取连接是同步的,如果在不同线程同时获取,线程池会创建多个物理连接以供使用,但这一数目并不是无限大的,如果没有设置max-active
,默认是8个。
如果最大连接数已经满了,创建连接的线程在–3处阻塞,主线程在–2处阻塞。直到有线程池回收其他线程的连接后,在–5处调用notEmpty.signal()
唤醒–2处的阻塞,这里如果有多个线程的话,唤醒最先阻塞的那个。再一次循环,poolingCount已经不为0了,从connections数组中取值返回。
最大等待时间
如果设置了max-wait
,调用的是holder = pollLast(nanos)
方法。
DruidDataSource->pollLast():
estimate = notEmpty.awaitNanos(estimate);
复制代码
本来调用notEmpty.await()
无限等待,这里调用了notEmpty.awaitNanos(estimate)
设置一定的等待时间,如果estimate返回小于0,说明已经超时了,后续会对estimate进行判断,小于0将会抛出相对应超时的异常。
关闭连接、检测时间与空闲时间
CreateConnectionThread负责创建数据库连接,相应的,DestroyConnectionThread负责关闭数据库连接。
DruidDataSource->DestroyConnectionThread->run():
for (;;) {
// 这里的timeBetweenEvictionRunsMillis即检测时间,默认是60s
if (timeBetweenEvictionRunsMillis > 0) {
Thread.sleep(timeBetweenEvictionRunsMillis);
} else {
Thread.sleep(1000);
}
// 同步执行关闭数据库连接的线程方法
destroyTask.run();
}
DruidDataSource->DestroyTask->run():
shrink(true, keepAlive);
DruidDataSource->shrink():
// 连接池数量与最小连接数的差值
final int checkCount = poolingCount - minIdle;
// 当前时间毫秒数
final long currentTimeMillis = System.currentTimeMillis();
for (int i = 0; i < poolingCount; ++i) {
// 从connections下标为0开始遍历
DruidConnectionHolder connection = connections[i];
......
// 空闲时间=当前时间减去上次活跃时间
long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;
// 这里的minEvictableIdleTimeMillis即最小空闲时间
if (idleMillis < minEvictableIdleTimeMillis) {
// 如果空闲时间小于上次活跃时间,直接跳出循环了,这是因为随着connections下标的增大,活跃时间也是增大的
break;
}
// 确保关闭后数量不会小于最小连接数,否则就不关闭了
if (checkTime && i < checkCount) {
evictConnections[evictCount++] = connection;
// 如果空闲时间比设定的最大空闲时间还大的话,说什么也要关闭连接了
} else if (idleMillis > maxEvictableIdleTimeMillis) {
evictConnections[evictCount++] = connection;
} else if (keepAlive) {
keepAliveConnections[keepAliveCount++] = connection;
}
// 计算出要关闭连接的资源数量
int removeCount = evictCount + keepAliveCount;
if (removeCount > 0) {
// connections、poolingCount重新设置值
System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
poolingCount -= removeCount;
}
// 要关闭连接的资源都在evictConnections数组中
if (evictCount > 0) {
for (int i = 0; i < evictCount; ++i) {
// 遍历数组,一一关闭,过程与JDBC大同小异,不做解释
DruidConnectionHolder item = evictConnections[i];
Connection connection = item.getConnection();
JdbcUtils.close(connection);
destroyCount.incrementAndGet();
}
Arrays.fill(evictConnections, null);
}
......
复制代码
总结
- 从代码中可以得出结论,无论是获取连接,还是回收连接,还是关闭连接,核心都是对
DruidDataSource
类下成员变量connections
的操作,这是数据库连接池的精华所在 - druid创建数据库连接使用的是异步线程的方式,仅利用多线程的Lock锁、ConditionObject就将调用线程与数据库连接线程、调用线程与调用线程间的关系展现得淋漓尽致,非常值得学习