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. 结论

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

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

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

相关推荐
BillKu41 分钟前
Java + Spring Boot + Mybatis 插入数据后,获取自增 id 的方法
java·tomcat·mybatis
全栈凯哥42 分钟前
Java详解LeetCode 热题 100(26):LeetCode 142. 环形链表 II(Linked List Cycle II)详解
java·算法·leetcode·链表
chxii43 分钟前
12.7Swing控件6 JList
java
全栈凯哥44 分钟前
Java详解LeetCode 热题 100(27):LeetCode 21. 合并两个有序链表(Merge Two Sorted Lists)详解
java·算法·leetcode·链表
YuTaoShao1 小时前
Java八股文——集合「List篇」
java·开发语言·list
PypYCCcccCc1 小时前
支付系统架构图
java·网络·金融·系统架构
华科云商xiao徐1 小时前
Java HttpClient实现简单网络爬虫
java·爬虫
扎瓦1 小时前
ThreadLocal 线程变量
java·后端
BillKu2 小时前
Java后端检查空条件查询
java·开发语言
jackson凌2 小时前
【Java学习笔记】String类(重点)
java·笔记·学习