jsmubanlogo
  • 首页
  • 网页模板
  • 特效代码
  • 博文源码
  • 插件下载
  •    

Java中的数据库连接池

收藏    

作者第十天    2021-10-24

      

概述

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

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

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

为何使用连接池

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

  1. 使用数据库驱动建立一个到数据库连接;
  2. 建立 TCP socket用于读/写数据;
  3. 通过socket来读写数据;
  4. 关闭连接;
  5. 关闭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<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及时删除。

关于我们 | 积分获取 | 联系我们 | 用户协议 | 标签搜索 | 网站地图.html | 网站地图.xml | 网站地图.txt

Copyright © 2021-2023 All Right Reserved
陕公网安备 61082202000148号      陕ICP备2025078528号-1
js模板网 -陕西千手码农科技有限责任公司