连接池是一种众所周知的数据库访问模式,主要目的是减少创建数据库连接和读/写数据库操作的开销。
简单来说,连接池本质上就是数据库连接缓存的一种实现方式,可以通过对其进行配置来满足特定的需求。
本文中,我们会简要介绍一些流行的连接池框架,之后也会讨论如何从零开始实现一个连接池。
关于这个问题,只要我们分析一下典型的数据库连接的生命周期中所涉及的步骤,就会明白为什么:
很显然,数据库连接是非常昂贵的操作,因此在每个可能的应用场景中都要尽量将数据库连接操作降到最低。
这也就是数据库连接池发挥作用的地方。
只需要简单地实现一个数据库连接容器,使我们可以复用一些已存在的数据库连接,我们就可以有效地节省大量昂贵的数据库连接操作消耗的时间成本,从而提高数据库驱动应用程序的整体性能。
从实用角度来看,考虑到目前已有很多企业级连接池框架,从头开始实行连接池是没有意义的。但是从学习角度,也就是本文的角度来看,并不是无意义的。
即便如此,在开始学习如何实现基本的连接池之前,让我们首先了解几个流行的连接池框架。
我们首先看一下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,由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
,一个由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<Connection> connectionPool;
private List<Connection> usedConnections = new ArrayList<>();
private static int INITIAL_POOL_SIZE = 10;
public static BasicConnectionPool create(String url,
String user,
String password)
throws SQLException {
List<Connection> pool = new ArrayList<>(INITIAL_POOL_SIZE);
for (int i = 0; i < INITIAL_POOL_SIZE; 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() < MAX_POOL_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();
}
复制代码
在企业级实现中,连接池需要提供很多额外的特性,比如跟踪当前使用中的连接的能力,对于预编译语句池的支持,等等。
为了保证简洁明了,我们省略了这些额外特性的实现,同时提供的也是非线程安全的实现。
在本文中,我们研究了什么是连接池,并学习了如何实现我们自己的连接池。
当然,我们需要在应用程序中添加连接池时,不必从头开发全新的连接池。这就是为什么我们首先对线程池做了简单的介绍,并展示了一些流行的连接池框架,以便于我们可以清楚地了解它们的使用方式,并选择最适合我们要求的框架。
作者:GuoYaxiang
链接:https://juejin.cn/post/6844904089407488007
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
免责声明: | |
1、 | 资源售价只是赞助,不代表代码或者素材本身价格。收取费用仅维持本站的日常运营所需。 |
2、 | 本站资源来自用户上传,仅供用户学习使用,不得用于商业或者非法用途,违反国家法律一切后果用户自负。用于商业用途,请购买正版授权合法使用。 |
3、 | 本站资源不保证其完整性和安全性,下载后自行检测安全,在使用过程中出现的任何问题均与本站无关,本站不承担任何技术及版权问题,不对任何资源负法律责任。 |
4、 | 如有损害你的权益,请联系275551777@qq.com及时删除。 |