美文网首页
数据库连接池介绍

数据库连接池介绍

作者: engineer_tang | 来源:发表于2020-08-22 16:59 被阅读0次

    1. 概述

    连接池是一种众所周知的数据访问模式,其主要目的是减少执行数据库连接和读/写数据库操作的开销
    简而言之,连接池在最基本的级别上是数据库连接缓存实现,可以对其进行配置以满足特定的需求
    这里我们将对几种流行的连接池框架进行快速汇总,并学习如何从头开始实现自己的连接池。
    为什么是连接池?
    如果我们分析一个典型的数据库连接生命周期中涉及的步骤序列,我们就会明白为什么:
    1 ) 使用数据库驱动程序打开一个到数据库的连接
    2 ) 为了能进行读/写数据打开一个TCP Socket
    3 ) 通过套接字读/写数据
    4 ) 关闭连接
    5 ) 关闭Socket
    很明显,数据库连接是相当昂贵的操作,因此,在每一个可能的用例中都应该减少到最小(在边缘情况下,只是避免)。
    这就是连接池实现发挥作用的地方。
    通过简单地实现一个数据库连接容器(它允许我们重用许多现有的连接),我们可以有效地节省执行大量昂贵的数据库访问的成本,从而提高数据库驱动应用程序的总体性能

    2. 数据库连接池框架

    从实用的角度来看,考虑到可用的“企业级”连接池框架的数量,从头开始实现连接池是毫无意义的。
    从一个说教的角度来看,这是本文的目标,它不是。
    即便如此,在学习如何实现基本连接池之前,让我们先展示几个流行的连接池框架。
    2.1. 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的属性。
    下面是如何使用DBCPDataSource类获取池连接

    Connection con = DBCPDataSource.getConnection();
    

    2.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();
    

    2.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();
    

    3. 实现自己的数据库连接池

    为了更好地理解连接池的底层逻辑,让我们创建一个简单的实现。
    让我们从一个松散耦合的设计开始,它只基于一个接口:

    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类和数据源实现创建JDBC连接。
    由于保持连接数据库的创建不受影响要好得多,我们在create()静态工厂方法中使用了前者。
    在本例中,我们将方法放在BasicConnectionPool中,因为这是接口的唯一实现。
    在一个更复杂的设计中,有多个ConnectionPool实现,最好将它放在接口中,这样可以获得更灵活的设计和更高的内聚性。
    这里要强调的最相关的一点是,一旦创建了池,连接就会从池中获取,因此不需要创建新的连接。
    此外,当一个连接被释放时,它实际上被返回到池中,以便其他客户端可以重用它。
    与底层数据库没有任何进一步的交互,例如显式调用连接的close()方法。

    4. 使用BasicConnectionPool类

    正如预期的那样,使用BasicConnectionPool类非常简单。
    让我们创建一个简单的单元测试,并获得一个内存池中的H2连接:

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

    5. 进一步的改进和重构

    当然,还有很多空间可以调整/扩展连接池实现的当前功能。
    例如,我们可以重构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();
    }
    

    在生产就绪的实现中,连接池应该提供一系列额外的特性,例如跟踪当前正在使用的连接的能力、对准备好的语句池的支持等等。
    为了保持简洁,我们将省略如何实现这些附加特性,并保持实现的非线程安全,以保持清晰。
    参考:https://www.baeldung.com/java-connection-pooling
    参考:https://github.com/alibaba/druid/wiki/Druid%E8%BF%9E%E6%8E%A5%E6%B1%A0%E4%BB%8B%E7%BB%8D

    相关文章

      网友评论

          本文标题:数据库连接池介绍

          本文链接:https://www.haomeiwen.com/subject/lesvjktx.html