Java中的连接池简单指南

1. 概述

连接池是一种大家众所周知的数据访问模式。其主要目的是减少执行数据库连接和读/写数据库操作所涉及的资源开销。

从最基本的层面上讲,连接池是一种数据库连接缓存实现 ,可以根据特定要求进行配置。

在本教程中,我们将讨论一些流行的连接池框架。然后我们将学习如何从头开始实现我们自己的连接池。

2. 为什么要使用连接池?

如果我们分析典型数据库连接生命周期所涉及的步骤顺序,我们就会明白为什么:

  • 使用数据库驱动程序打开与数据库的连接
  • 打开TCP 套接字以读取/写入数据
  • 通过套接字读取/写入数据
  • 关闭连接
  • 关闭套接字

因为数据库连接是相当昂贵的操作,所以在每个可能的用例中都应将其减少到最低限度(在边缘情况下,尽量避免)。这就是连接池需要发挥作用的地方。

只需简单地实现一个数据库连接容器,它允许我们重用许多现有连接,我们可以有效地节省执行大量昂贵的数据库访问的成本。这提高了我们数据库驱动应用程序的整体性能。

3. JDBC 连接池框架

从实用的角度来看,考虑到已经有许多"企业就绪"的连接池框架,从头开始实现连接池是没有意义的。

从本文的教学目的来看,事实并非如此。

3.1. Apache Commons DBCP

让我们从Apache Commons DBCP Component开始,这是一个功能齐全的连接池 JDBC 框架:

java 复制代码
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类获取池连接:

java 复制代码
Connection con = DBCPDataSource.getConnection();

3.2. 光HikariCP

现在让我们来看看HikariCP ,一个由Brett Wooldridge创建的闪电般快速的 JDBC 连接池框架(有关如何配置和充分利用 HikariCP 的完整详细信息。这是一个非常轻量级(大约 130Kb)且速度极快的 JDBC 连接池框架,由 Brett Wooldridge于 2012 年左右开发。

有多个基准测试结果可用于比较 HikariCP 与其他连接池框架(例如*c3p0 dbcp2 tomcat*和vibur )的性能。例如,HikariCP 团队发布了以下基准测试(原始结果可在此处获得):

该框架如此之快是因为采用了以下技术:

  • 字节码级工程------ 已经完成了一些极端的字节码级工程(包括汇编级本机编码)
  • **微优化 -**虽然几乎无法衡量,但这些优化结合起来可以提高整体性能
  • 智能使用 Collections 框架 - ArrayList 被自定义类FastList 取代, 该类消除了范围检查,并从头到尾执行删除扫描
java 复制代码
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类获取池连接:

java 复制代码
Connection con = HikariCPDataSource.getConnection();

3.3. C3P0

这篇评论中的最后一篇是C3P0,这是 Steve Waldman 开发的一个强大的 JDBC4 连接和语句池框架:

java 复制代码
public class C3p0DataSource {

    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 C3p0DataSource(){}
}

正如预期的那样,使用C3p0DataSource类获取池连接与前面的示例类似:

java 复制代码
Connection con = C3p0DataSource.getConnection();

4. 简单的实现

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

我们将从基于单一接口的松耦合设计开始:

java 复制代码
public interface ConnectionPool {
    Connection getConnection();
    boolean releaseConnection(Connection connection);
    String getUrl();
    String getUser();
    String getPassword();
}

ConnectionPool接口定义基本连接池的公共API。

现在让我们创建一个提供一些基本功能的实现,包括获取和释放池连接:

java 复制代码
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初始化一个连接池,可以轻松重复使用。

还可以使用DriverManagerDatasource实现创建 JDBC 连接。

由于保持连接数据库的创建与数据库无关更好,因此我们在create() 静态工厂方法中使用了前者。

在这种情况下,我们将方法放在BasicConnectionPool中 ,因为这是接口的唯一实现。

在更复杂的设计中,如果有多个ConnectionPool实现,最好将其放在接口中,从而获得更灵活的设计和更高水平的凝聚力。

这里要强调的最重要的一点是,一旦创建了池,就会从池中获取连接,因此无需创建新的连接

此外,当释放连接时,它实际上会返回到池中,以便其他客户端可以重用它

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

5. 使用BasicConnectionPool

正如预期的那样,使用我们的BasicConnectionPool类非常简单。

让我们创建一个简单的单元测试并获取一个内存池H2连接:

java 复制代码
@Test
public whenCalledgetConnection_thenCorrect() {
    ConnectionPool connectionPool = BasicConnectionPool
            .create("jdbc:h2:mem:test", "user", "password");

    assertTrue(connectionPool.getConnection().isValid(1));
}

6. 进一步改进和重构

当然,我们还有足够的空间来调整/扩展连接池实现的当前功能。

例如,我们可以重构getConnection() 方法并添加对最大池大小的支持。如果所有可用连接都已被占用,并且当前池大小小于配置的最大值,则该方法将创建一个新连接。

我们还可以验证从池中获取的连接是否仍然有效,然后再将其传递给客户端:

java 复制代码
@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,这意味着我们还必须更新接口签名。

或者我们可以添加一种方法来正常关闭我们的连接池实例:

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

在生产就绪的实现中,连接池应该提供一系列额外的功能,例如跟踪当前正在使用的连接的能力、对准备好的语句池的支持等等。

为了使本文简单易懂,我们将省略如何实现这些附加功能,并且为了清晰起见,保持实现非线程安全。

7. 结论

在本文中,我们深入了解了连接池是什么,并学习了如何推出我们自己的连接池实现。

当然,每次我们想要为我们的应用程序添加一个功能齐全的连接池层时,我们不必从头开始。

这就是为什么我们首先探索一些最流行的连接池框架,这样我们就清楚地知道如何使用它们并选择最适合我们要求的框架。

相关推荐
xlsw_3 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹4 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭4 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫4 小时前
泛型(2)
java
超爱吃士力架4 小时前
邀请逻辑
java·linux·后端
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石5 小时前
12/21java基础
java
李小白665 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp5 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea
装不满的克莱因瓶5 小时前
【Redis经典面试题六】Redis的持久化机制是怎样的?
java·数据库·redis·持久化·aof·rdb