1.概述
连接池是一种众所周知的数据访问模式,其主要目的是减少执行数据库连接和读/写数据库操作所涉及的开销。
简而言之,连接池是最基本的数据库连接缓存实现,可以将其配置为适合特定要求。
在本教程中,我们将快速概述一些流行的连接池框架,并且将学习如何从头开始实现我们自己的连接池。
2.为什么要建立连接池?
当然,这个问题是修辞。
如果我们分析典型的数据库连接生命周期中涉及的步骤顺序,我们将理解原因:
- 使用数据库驱动程序打开与数据库的连接
- 打开TCP套接字以读取/写入数据
- 通过套接字读取/写入数据
- 断开连接
- 关闭套接字
显而易见的是,数据库连接是相当昂贵的操作,因此,在每种可能的使用情况下(应避免使用边缘情况),应将其减少到最少。
这是连接池实现起作用的地方。
通过简单地实现数据库连接容器(使我们能够重用许多现有连接),我们可以有效地节省执行大量昂贵的数据库行程的成本,从而提高数据库驱动的应用程序的整体性能。
3. JDBC连接池框架
从务实的角度来看,考虑到可用的“企业就绪”连接池框架的数量,从头开始实施连接池是没有意义的。
从说教的角度出发,这不是本文的目标。
即便如此,在我们学习如何实现基本连接池之前,让我们首先展示一些流行的连接池框架。
3.1。Apache Commons DBCP
让我们从Apache Commons DBCP Component(一个功能齐全的连接池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的属性。
以下是使用DBCPDataSource类获得池化连接的方法:
| Connection con = DBCPDataSource.getConnection(); |
3.2。HikariCP
继续,让我们看一下HikariCP,这是由Brett 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(); |
3.3。C3PO
本文的最后内容是C3PO,这是由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(); |
4.一个简单的实现
为了更好地理解连接池的基本逻辑,让我们创建一个简单的实现。
让我们从一个仅基于单个接口的松耦合设计开始:
| public interface ConnectionPool { |
| Connection getConnection(); |
| boolean releaseConnection(Connection connection); |
| String getUrl(); |
| String getUser(); |
| String getPassword(); |
| } |
该连接池接口定义了一个基本的连接池的公共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类和数据源实现创建JDBC连接。
由于保持连接数据库的创建无关紧要好得多,因此我们在create()静态工厂方法中使用了前者。
在这种情况下,我们将方法放置在BasicConnectionPool中,因为这是接口的唯一实现。
在具有多个ConnectionPool实现的更复杂的设计中,最好将其放置在界面中,从而获得更灵活的设计和更大的凝聚力。
这里最需要强调的一点是,一旦创建了池,便会从池中获取连接,因此无需创建新的连接。
此外,释放连接后,它实际上会返回到池中,以便其他客户端可以重用它。
与基础数据库没有任何进一步的交互,例如对Connection的close()方法的显式调用。
5.使用BasicConnectionPool类
不出所料,使用我们的BasicConnectionPool类非常简单。
让我们创建一个简单的单元测试,并获得一个内存中的H2连接池:
| @Test |
| public whenCalledgetConnection_thenCorrect() { |
| ConnectionPool connectionPool = BasicConnectionPool |
| .create("jdbc:h2:mem:test", "user", "password"); |
| |
| assertTrue(connectionPool.getConnection().isValid(1)); |
| } |
6.进一步的改进和重构
当然,还有很多空间可以调整/扩展我们连接池实现的当前功能。
例如,我们可以重构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); |
| |
| if(!connection.isValid(MAX_TIMEOUT)){ |
| connection = createConnection(url, user, password); |
| } |
| |
| usedConnections.add(connection); |
| return connection; |
| } |
注意,该方法现在抛出SQLException,这意味着我们也必须更新接口签名。
或者,我们可以添加一个方法来正常关闭连接池实例:
| public void shutdown() throws SQLException { |
| usedConnections.forEach(this::releaseConnection); |
| for (Connection c : connectionPool) { |
| c.close(); |
| } |
| connectionPool.clear(); |
| } |
在可用于生产环境的实现中,连接池应提供许多额外的功能,例如跟踪当前正在使用的连接的能力,对准备好的语句池的支持等等。
为了使事情变得简单,为了清楚起见,我们将省略如何实现这些附加功能,并使实现保持非线程安全。
7.结论
在本文中,我们深入研究了什么是连接池,并学习了如何滚动我们自己的连接池实现。
当然,我们不必每次都要向应用程序添加功能齐全的连接池层时都从头开始。
这就是为什么我们首先进行一个简单的综述,展示一些最受欢迎的连接池框架,因此我们可以对如何使用它们有一个清晰的了解,并挑选出最适合我们要求的框架。
像往常一样,本文中显示的所有代码示例都可以在GitHub上获得。