在开发应用程序过程中,涉及到对系统资源进行有效管理时往往会用到池化操作。资源池模式的应用场景很多,可以管理那些想要通过重用来分摊昂贵初始化代价的对象,而管理数据库连接就是很好的一种应用场景。数据库连接池作为一种典型的池化技术手段,能够显著提升数据库访问效率,在Mybatis等主流ORM框架中都有对应的实现方案。本文将对资源池技术进行全面介绍。
在日常开发过程中,相信你对线程池、数据库连接池等技术并不陌生。这里池(Pool)是一种对资源的抽象方法,代表一组可以随时使用的资源,但这些资源的创建和释放过程则基于一定的管理策略。
资源池的应用非常广泛,存在线程池、数据库连接池等多种具体的池化组件。这些技术组件虽然表现形式多样,但基本原理都是一致的。在介绍具体的池化技术之前,让我们先来分析资源池的基本原理,并尝试自己动手实现一个资源池。
资源池的基本原理与实现
一个典型的资源池的结构如下图所示。
可以看到,客户端可以向池请求资源, 用它来完成一些任务并当任务完成时归还该资源,而被归还的资源可以继续用来满足请求,从而达到资源复用的效果。资源池的特点主要在于节省了创建资源实例的开销和时间,但存储空间会随着对象的增多而增大。
基于资源池的基本结构,我们可以进一步分析它的应用场景。作为一种通用的技术组件,资源池的应用场景很多:
- 管理那些想要通过重用来分摊昂贵初始化代价的对象;
- 或者面向请求资源的频率很高且使用资源总数较低的业务处理过程;
- 当系统面临性能问题时,也可以通过资源池模式进行时间延迟方面的处理。
资源池的概念本身比较简单,我们对它的基本操作进行抽象,可以自己实现一个资源池,如下所示。
public abstract class ResourcePool<T> {
private HashSet<T> available = new HashSet<>();
private HashSet<T> inUse = new HashSet<>();
protected abstract T create();
public synchronized T out() {
if (available.size() <= 0) {
available.add(create());
}
T instance = available.iterator().next();
available.remove(instance);
inUse.add(instance);
return instance;
}
public synchronized void in(T instance) {
inUse.remove(instance);
available.add(instance);
}
@Override
public String toString() {
return String.format("池中可用资源=%d 在用资源=%d", available.size(), inUse.size());
}
}
可以看到,在ResourcePool类中,使用HashSet保持了池中的可用资源以及在用资源列表。然后,我们提供了out方法和in方法分别用于从资源池中获取资源以及将资源返回给资源池。我们在这两个方法上也通过使用synchronized关键词确保线程安全。
注意到,ResourcePool是一个抽象类,提供了create()抽象方法供具体的资源类使用。例如,我们可以构建如下所示的UserPool来实现基于User对象的资源池。
public class UserPool extends ResourcePool<User> {
@Override
protected User create() {
return new User();
}
}
接下来,为了模拟昂贵的对象初始化过程,我们可以构建如下所示的User类,注意到在构造函数中,我们通过Thread.sleep(1000)来模拟这种高成本的创建过程。
public class User {
private static int counter = 1;
private final int id;
public User() {
id = counter++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public int getId() {
return id;
}
@Override
public String toString() {
return String.format("User id=%d", id);
}
}
我们运行一下如下所示的代码来模拟对ResourcePool的使用过程。
public static void main(String[] args) {
UserPool pool = new UserPool();
System.out.println(pool);
User user1 = pool.out();
System.out.println("使用 " + user1);
System.out.println(pool);
User user2 = pool.out();
System.out.println("使用 " + user2);
User user3 = pool.out();
System.out.println("使用 " + user3);
System.out.println(pool);
System.out.println("归还 " + user1);
pool.in(user1);
System.out.println("归还" + user2);
pool.in(user2);
System.out.println(pool);
User user4 = pool.out();
System.out.println("使用 " + user4);
System.out.println(pool);
}
运行结果如下所示,体现了系统的运行时快照。
池中可用资源=0 在用资源=0
使用 User id=1
池中可用资源=0 在用资源=1
使用 User id=2
使用 User id=3
池中可用资源=0 在用资源=3
归还 User id=1
归还User id=2
池中可用资源=2 在用资源=1
使用 User id=2
池中可用资源=1 在用资源=2
作为示例,这个ResourcePool的对象体系是比较简单的,但已经完整阐述了基本的资源池模式。现实中资源池中存放的资源一般不大会是类似User类这样的业务对象,而更多关注于诸如数据库连接和套接字连接等需要网络通信的远程资源,以及线程和内存等系统资源。在接下来的内容中,我们将基于Mybatis中的数据库连接池的实现过程来进一步加深对资源池的理解。
资源池的应用场景:数据库连接池
在介绍Mybatis中数据库连接池之前,我们有必要对连接池的实现机制有个总体的把握。
数据库连接池工作流程
在连接池中,对连接的管理策略是重点,也在很大程度上决定了不同连接池之间的实现方式。常见的实现策略是:当客户请求连接时,首先查看连接池中是否有空闲连接,如果存在空闲连接,则将连接分配给客户使用;如果没有空闲连接,则查看当前所开的连接数是否已经达到最大连接数,如果没达到就重新创建一个连接给请求的客户;如果达到就按设定的最大等待时间进行等待,如果超出最大等待时间,则抛出异常给客户。整个流程如下图所示。
围绕前面介绍的连接池的管理方式,我们可以抽象出一些控制维度和参数,常见的参数包括最小空闲连接数(MinIdle)、最大空闲连接数(MaxIdle)、连接池最大活跃连接数(MaxActive)、最大超时时间(MaxTimeout)等。
对于连接池而言,性能是我们选择不同实现工具的首要考虑因素。基于已知内容,我们可以进一步分析连接池内连接的分配和释放对系统的性能的影响:
- 如果将总连接数的上限设置得过大,可能因连接数过多而导致数据库僵死,系统整体性能下降;
- 如果总连接数上限过小,则无法完全发挥数据库的性能,浪费数据库资源。
另一方面:
- 如果将空闲连接的上限设置得过大,则会浪费系统资源来维护这些空闲连接;
- 如果空闲连接上限过小,当出现瞬间的峰值请求时,系统的快速响应能力就比较弱。
所以在设置数据库连接池的这些值时,需要进行测试和权衡,不同的实现方案会有不同的考虑。
Mybatis中的数据库连接池实现过程
我们知道DataSource是JDBC中定义的接口,该接口只包含两个重载的getConnection()方法。Mybatis实现了该接口,并提供了三种实现方案,其中最主要的就是池化类PooledDataSource。我们直接来看PooledDataSource的getConnection()方法,发现它进一步调用了如下所示的popConnection()方法。这个方法非常长,为了更好的把握代码结构,我们对该方法的代码进行裁剪,只关注于主要的分支和流程。
private PooledConnection popConnection(String username, String password) throws SQLException {
while (conn == null) {
synchronized (state) {
if (!state.idleConnections.isEmpty()) {
// 如果idle列表不为空,表示有可用连接,直接选取第一个元素
conn = state.idleConnections.remove(0);
} else {
// 连接池没有可用连接的场景
if (state.activeConnections.size() < poolMaximumActiveConnections) {
// 如果active列表没有满,直接创建新连接
conn = new PooledConnection(dataSource.getConnection(), this);
} else {// active已经满了
// 获得试用最久的连接,判断是否已经超时
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > poolMaximumCheckoutTime) {
// 已经超时,将原连接废弃并建立新连接
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
} else {
// 如果没有超时,则进行等待,并计算时间累加
state.wait(poolTimeToWait);
}
}
}
if (conn != null) {
// 如果获取到了连接,则验证该连接是否有效
if (conn.isValid()) {
// 如果连接有效,更新该链接相关参数和状态
} else {
// 如果连接无效,且无效连接数量超过上限则抛异常
}
}
}
}
if (conn == null) {
// 如果最终无法获取有效连接,则同样抛异常
}
return conn;
}
在上述代码中,我们添加了很多注释来解释用于获取Connection连接对象的整体流程。在整体流程上,当Mybatis执行查询时会首先从idleConnections列表中申请一个空闲的连接,只有当idleConnections列表为空时才会常见新连接。当然PooledDataSource并不允许无限建立新连接,当连接池中连接数目达到一定数量时,即使idleConnections列表为空,也不会建立新连接。而是从activeConnections列表中找出使用最久的一个连接,判断其是否超时。如果超时,则将该连接废弃并建立新连接,否则线程等待直到有连接池中有新的可用连接。
可以看到popConnection()方法结构非常清晰。如果我们对前面介绍都的连接池管理方法有一定了解的话,理解这段代码难度并不大。
全文小结
本文系统分析了在日常开发过程中非常常用的一种技术组件,即资源池。我们通过分析资源池的结构了解了它的基本实现原理,并尝试自己动手实现一个简单的资源池。更为重要的,我们详细分析了资源池的应用场景,并基于Mybatis这一主流ORM框架,给出了数据库连接池这一具体资源池的工作流程和实现过程。