概述

连接池是一种众所周知的数据库访问模式,主要目的是减少创建数据库连接和读/写数据库操作的开销。

简单来说,连接池本质上就是数据库连接缓存的一种实现方式,可以通过对其进行配置来满足特定的需求。

本文中,我们会简要介绍一些流行的连接池框架,之后也会讨论如何从零开始实现一个连接池。

为何使用连接池

关于这个问题,只要我们分析一下典型的数据库连接的生命周期中所涉及的步骤,就会明白为什么:

使用数据库驱动建立一个到数据库连接;

建立 TCP socket用于读/写数据;

通过socket来读写数据;

关闭连接;

关闭socket;

很显然,数据库连接是非常昂贵的操作,因此在每个可能的应用场景中都要尽量将数据库连接操作降到最低。

这也就是数据库连接池发挥作用的地方。

只需要简单地实现一个数据库连接容器,使我们可以复用一些已存在的数据库连接,我们就可以有效地节省大量昂贵的数据库连接操作消耗的时间成本,从而提高数据库驱动应用程序的整体性能。

JDBC连接池框架

从实用角度来看,考虑到目前已有很多企业级连接池框架,从头开始实行连接池是没有意义的。但是从学习角度,也就是本文的角度来看,并不是无意义的。

即便如此,在开始学习如何实现基本的连接池之前,让我们首先了解几个流行的连接池框架。

Apache Commons DBCP

我们首先看一下Apache Commons DBCP组件,这是一个功能齐全的连接池JDBC框架:

public class DBCPDataSource{
private static BasicDataSource ds = new BasicDataSource();
static {
ds.setUrl("jdbc:h2:mem:test");
ds.setUsername("user");
ds.setPassword("password");
ds.setMinIdle(5);
ds.setMaxIdle(10);
ds.setMaxOpenPreparedStatements(100);
}
public static Connection getConnection() throws SQLException{
return ds.getConnection();
}
private DBCPDataSource(){ }
}

复制代码

在这个例子中,我们使用带有静态块的包装器类可以很容易地配置DBCP的属性。使用DBCPDateSource类获取池化连接的方式如下:

Connection con = DBCPDataSource.getConnection();

复制代码

C3P0

接着介绍的是C3P0,由Steve Waldman开发,是一个强大的JDBC4连接和语句池框架。

public class C3poDataSource{
private static ComboPooledDataSource cpds = new ComboPooledDataSource();
static {
try {
cpds.setDriverClass("org.h2.Driver");
cpds.setJdbcUrl("jdbc:h2:mem:test");
cpds.setUser("user");
cpds.setPassword("password");
} catch (PropertyVetoException e) {
// handle the exception
}
}
public static Connection getConnection() throws SQLException{
return cpds.getConnection();
}
private C3poDataSource(){}
}

复制代码

通过C3PoDataSource类获取池化连接的方式与前面类似:

Connection con = C3poDataSource.getConnection();

复制代码

HikariCP

最后来看一下HikariCP,一个由Breet Wooldridge开发的快速JDBC连接池框架。我们会在后续的文章中详细介绍HikariCP的配置和使用方式。

public class HikariCPDataSource{
private static HikariConfig config = new HikariConfig();
private static HikariDataSource ds;
static {
config.setJdbcUrl("jdbc:h2:mem:test");
config.setUsername("user");
config.setPassword("password");
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
ds = new HikariDataSource(config);
}
public static Connection getConnection() throws SQLException{
return ds.getConnection();
}
private HikariCPDataSource(){}
}

复制代码

通过HikariCPDataSource类获取池化连接的方式同样很简单:

Connection con = HikariCPDataSource.getConnection();

复制代码

连接池简单实现

为了更好地理解连接池的底层逻辑,我们来实现一个简单的连接池。

首先,我们基于单个接口做一个松耦合设计:

public interface ConnectionPool{
Connection getConnection();
boolean releaseConnection(Connection connection);
String getUrl();
String getUser();
String getPassword();
}

复制代码

ConnectionPool接口定义了一个基本连接池所需的公共API。

现在,我们通过实现该接口来提供一些基础功能,包括获取和释放池化连接:

public class BasicConnectionPool implements ConnectionPool{
private String url;
private String user;
private String password;
private List connectionPool;
private List usedConnections = new ArrayList<>();
private static int INITIAL_POOL_SIZE = 10;
public static BasicConnectionPool create(String url,
String user,
String password)
throws SQLException{
List pool = new ArrayList<>(INITIAL_POOL_SIZE);
for (int i = 0; i 
pool.add(createConnection(url, user, password));
}
return new BasicConnectionPool(url, user, password, pool);
}
// standard constructors
@Override
public Connection getConnection(){
Connection connection = connectionPool.remove(connectionPool.size() - 1);
usedConnections.add(connection);
return connection;
}
@Override
public boolean releaseConnection(Connection connection){
connectionPool.add(connection);
return usedConnections.remove(connection);
}
private static Connection createConnection(String url,
String user,
String password)
throws SQLException{
return DriverManager.getConnection(url, user, password);
}
public int getSize(){
return connectionPool.size() + usedConnections.size();
}
// standard getters
}

复制代码

虽然很简单,但BasicConnectionPool类确实提供了我们期望从典型的连接池得到的基础功能。简单来说,该类基于一个可存储10个数据库连接的ArrayList来初始化连接池,从而使得这些连接可以被复用。

我们可以使用DriverManager类或Datasource实现来创建JDBC连接。从设计的角度来看,屏蔽数据库连接的创建过程更好,因此我们在create()静态工厂方法中选择了前者。

在本例中,我们把创建连接的方法放在了BasicConnectionPool类中,因为这个类是连接池接口的唯一实现类。但是在更复杂的设计中,可能存在多个ConnectionPool实现类,此时最好将该方法放在接口中,从而获得更灵活的设计和更强的内聚性。

需要强调的一点是,一旦创建了连接池,所有的连接都会从池中获取,因此不需要创建新的连接。此外,当一个连接被释放时,它实际上是被归还到池中,以便其他客户端可以重用它。

这里与底层数据库没有任何进一步的交互,例如对连接的close()方法的显式调用。

对于BasicConnectionPool的使用非常简单,我们可以写一个简单的单元测试,获取一个内存数据库H2的池化连接:

@Test
public whenCalledgetConnection_thenCorrect(){
ConnectionPool connectionPool = BasicConnectionPool
.create("jdbc:h2:mem:test", "user", "password");
assertTrue(connectionPool.getConnection().isValid(1));
}

复制代码

改进与重构

当然,我们还有很大的空间去改进或者扩展现在的线程池实现。

比如说,我们可以重构getConnection()方法,增加对最大连接池规模参数的支持。如果所有可用的连接都已经被使用,而且当前的连接数小于配置的最大值,该方法会创建新的连接:

@Override
public Connection getConnection() throws SQLException{
if (connectionPool.isEmpty()) {
if (usedConnections.size() 
connectionPool.add(createConnection(url, user, password));
} else {
throw new RuntimeException(
"Maximum pool size reached, no available connections!");
}
}
Connection connection = connectionPool.remove(connectionPool.size() - 1);
usedConnections.add(connection);
return connection;
}

复制代码

需要注意,该方法在这里抛出了SQLException,这意味着我们也需要修改接口中的方法签名。

此外,我们也可以增加方法来优雅地关闭连接池实例:

public void shutdown() throws SQLException{
usedConnections.forEach(this::releaseConnection);
for (Connection c : connectionPool) {
c.close();
}
connectionPool.clear();
}

复制代码

在企业级实现中,连接池需要提供很多额外的特性,比如跟踪当前使用中的连接的能力,对于预编译语句池的支持,等等。

为了保证简洁明了,我们省略了这些额外特性的实现,同时提供的也是非线程安全的实现。

在本文中,我们研究了什么是连接池,并学习了如何实现我们自己的连接池。

当然,我们需要在应用程序中添加连接池时,不必从头开发全新的连接池。这就是为什么我们首先对线程池做了简单的介绍,并展示了一些流行的连接池框架,以便于我们可以清楚地了解它们的使用方式,并选择最适合我们要求的框架。