连接池原理

​ 连接池的基本思想就是为连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立连接时,只需要从缓冲池中取出一个了,使用完毕后再放回去。

apache commons-pool是apache基金会的一个开源对象池组件,我们常用的数据库连接池dpcpredis的java客户端jedis都使用commons-pool来管理连接。

主要提供这个几种类型的对象池:

类图
这篇文章主要介绍GenericObjectPool的实现。

工作原理:

连接池的核心思想是连接的复用,通过建立一个数据库连接池以及一套连接使用、分配和管理策略,使得该连接池中的连接可以得到高效,安全的复用,避免了数据库连接频繁建立和关闭的开销。

连接池的工作原理主要由三部分组成,分别为连接池的建立,连接池中连接的使用管理,连接池的关闭。

  1. 连接池的建立。一般在系统初始化时,连接池会根据系统配置建立,并在池中建立几个连接对象,以便使用时能从连接池中获取,连接池中的连接不能随意创建和关闭,这样避免了连接随意建立和关闭造成的系统开销。

  2. 连接池的管理。连接池管理策略是连接池机制的核心,连接池内连接的分配和释放对系统的性能有很大的影响。其策略是:

    当客户请求数据库连接时,首先查看连接池中是否有空闲连接,如果存在空闲连接,则将连接分配给客户使用;如果没有控线连接,则查看当前所开的连接数是否已经达到最大连接数,例如如果没有达到就重新创建一个请求的客户;如果达到,就按设定的最大等待时间进行等待,如果超出最大等待时间,则抛出异常给客户。

  3. 连接池的关闭。当应用程序退出时,关闭连接池中所有的链接,释放连接池相关资源,该过程正好与创建相反。

源码分析

commons-pool定义了一个接口PooledObject来封装池中的对象,添加了空闲时间,使用时间等信息和对象状态流转的行为。

对象池定义

对象池(连接池)是我们保存对象的地方,对象的获取,归还和定时检查都通过对象池来实现。以GenericObjectPool为例,使用了线程安全的集合类来保存对象,LinkedBlockingDeque用于保存空闲的对象,ConcurrentHashMap保存全部对象.

1
2
private final Map<T, PooledObject<T>> allObjects = new ConcurrentHashMap<T, PooledObject<T>>();
private final LinkedBlockingDeque<PooledObject<T>> idleObjects = new LinkedBlockingDeque<PooledObject<T>>();
1
2
3
4
5
6
7
8
9
10
11
// GenericObjectPool.java
public GenericObjectPool(PooledObjectFactory<T> factory,
GenericObjectPoolConfig config) {
...other code...
setConfig(config);
// **启动自动回收空闲连接**
startEvictor(getTimeBetweenEvictionRunsMillis());
}

获取连接:borrowObject

连接池借出对象时,经过Abandonedvalidate两种检查,在连接池满时根据配置执行对应的等待策略,当没有可用对象时会抛出异常。

  1. Abandoned检查目标是连接池所有被借出的对象,主要防止对象借出之后长时间被占用,不能退还(或者使用者忘记return)到连接池导致连接被耗尽。
  2. validate检查目标是当前即将被借出的对象,目的是保证提供的对象是可用的,检查方式由对象工厂的validateObject方法定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
public T borrowObject(long borrowMaxWaitMillis) throws Exception {
// getNumIdle() 表示当前空闲对象数量,getNumActive() 表示当前非空闲的对象数量,getMaxTotal()表示连接池容量
// 那么假设最大容量10个,非空闲8个 > 7 ,空闲对象只要少于2个,就需要开始Abandoned检查
AbandonedConfig ac = this.abandonedConfig;
if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
(getNumIdle() < 2) &&
(getNumActive() > getMaxTotal() - 3) ) {
removeAbandoned(ac);
}
PooledObject<T> p = null;
//blockWhenExhausted对象池耗尽时是否等待(默认为true)
boolean blockWhenExhausted = getBlockWhenExhausted();
boolean create;
long waitTime = 0;
while (p == null) {
create = false;
// 设置了对象池耗尽(是否达到最大活跃数)时等待
if (blockWhenExhausted) {
//从空闲队列取对象
p = idleObjects.pollFirst();
if (p == null) {
//没有空闲对象 新建一个
create = true;
p = create();
}
if (p == null) {
if (borrowMaxWaitMillis < 0) {
//注意:没配置等待时间,会一直阻塞
p = idleObjects.takeFirst();
} else {
//按照配置的时间等待
.....
p = idleObjects.pollFirst(borrowMaxWaitMillis,
TimeUnit.MILLISECONDS);
.....
}
}
//等待之后还是没有空闲对象
if (p == null) {
throw new NoSuchElementException(
"Timeout waiting for idle object");
}
//等待之后获得对象 尝试分配对象
//这个方法由pooledobject实现
if (!p.allocate()) {
p = null;
}
} else {
//没有配置blockWhenExhausted 不等待
p = idleObjects.pollFirst();
if (p == null) {
create = true;
p = create();
}
if (p == null) {
throw new NoSuchElementException("Pool exhausted");
}
if (!p.allocate()) {
p = null;
}
}
//对象分配成功
if (p != null) {
try {
//激活对象
factory.activateObject(p);
} catch (Exception e) {
try {
destroy(p);
} catch (Exception e1) {
}
p = null;
.....
}
//如果配置了对象检查
if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) {
boolean validate = false;
Throwable validationThrowable = null;
try {
validate = factory.validateObject(p);
} catch (Throwable t) {
PoolUtils.checkRethrow(t);
validationThrowable = t;
}
if (!validate) {
//验证失败 销毁对象
try {
destroy(p);
destroyedByBorrowValidationCount.incrementAndGet();
} catch (Exception e) {
}
p = null;
.....
}
}
}
}
//更新借出时间等信息
updateStatsBorrow(p, waitTime);
return p.getObject();
}

1) 当客户请求数据库连接时,首先查看连接池中是否有空闲连接;

2) 如果存在空闲连接,则将连接分配给客户使用;如果没有空闲连接,则查看当前所开的连接数是否已经达到最大连接数;

3) 如果没有达到就重新创建一个连接;如果达到最大连接数,就按设定的最大等待时间进行等待,如果超出最大等待时间,则抛出异常给客户。

释放连接:returnObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// GenericObjectPool.java
@Override
public void returnObject(T obj) {
// 从allObjects中获取要归还的连接
PooledObject<T> p = allObjects.get(obj);
...other code...
int maxIdleSave = getMaxIdle();
// 如果idleObjects空闲队列中连接数已经>=允许的最大空闲连接数或者连接池已经关闭就直接销毁这个连接
if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
try {
// 销毁连接
destroy(p);
} catch (Exception e) {
swallowException(e);
}
} else {
// 将连接放入到idleObjects队列中, 一旦将连接放入到idleObjects中如果连接长时间不被使用就会被自动回收
if (getLifo()) {
// 默认是使用last in first out机制(后进先出)
idleObjects.addFirst(p);
} else {
idleObjects.addLast(p);
}
}
updateStatsReturn(activeTime);
}

1) 从allObjects中获取要归还的连接

2) 如果idleObjects空闲队列中连接数已经>=允许的最大空闲连接数或者连接池已经关闭就直接销毁这个连接

3) 否则,将连接放入到idleObjects队列中, 一旦将连接放入到idleObjects中如果连接长时间不被使用就会被自动回收(由后台驱逐线程回收空闲连接)

销毁连接

1
2
3
4
5
6
7
8
9
10
11
private void destroy(PooledObject<T> toDestory) throws Exception {
toDestory.invalidate();
idleObjects.remove(toDestory);
allObjects.remove(toDestory.getObject());
try {
factory.destroyObject(toDestory);
} finally {
destroyedCount.incrementAndGet();
createCount.decrementAndGet();
}
}

回收空闲连接:evict

启动驱逐者线程
1
2
3
4
5
6
7
8
//GenericObjectPool.java
//构造方法中启动驱逐者线程
public GenericObjectPool(PooledObjectFactory<T> factory,
GenericObjectPoolConfig config) {
...
startEvictor(getTimeBetweenEvictionRunsMillis());
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//父类BaseGenericObjectPool.java
final void startEvictor(long delay) {
//evictionLock对象锁
synchronized (evictionLock) {
if (null != evictor) {
//已存在evictor,取消当前驱逐者线程;重新启动另一个驱逐者线程
EvictionTimer.cancel(evictor);
evictor = null;
evictionIterator = null;
}
if (delay > 0) {
evictor = new Evictor();
EvictionTimer.schedule(evictor, delay, delay);
}
}
}

驱逐者Evictor,在BaseGenericObjectPool中定义,本质是由java.util.TimerTask定义的定时任务.

驱逐者线程执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//BaseGenericObjectPool内部类Evictor
//作用:驱逐空闲对象
class Evictor extends TimerTask {
@Override
public void run() {
ClassLoader savedClassLoader =
Thread.currentThread().getContextClassLoader();
try {
// 切换到当前连接池的classLoader
Thread.currentThread().setContextClassLoader(
factoryClassLoader);
try {
//执行evict()方法
evict();
} catch(Exception e) {
swallowException(e);
} catch(OutOfMemoryError oome) {
oome.printStackTrace(System.err);
}
// 驱逐之后还要保证空闲连接数量不能小于配置
try {
ensureMinIdle();
} catch (Exception e) {
swallowException(e);
}
} finally {
// 切换回之前的classLoader
Thread.currentThread().setContextClassLoader(savedClassLoader);
}
}
}

驱逐者线程Evictor被多个连接池共享,但是这些连接池可能属于不同的classloader,所以Evictor必须要保证它的所有行为在当前这个连接池的classloader下执行

参考

如何设计一个连接池:commons-pool2源码分析

Jedis 源码阅读之连接池

数据库连接池的工作原理

数据源连接池的原理及Tomcat中的应用

热评文章