关于数据库连接池的一些总结

867 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,[点击查看活动详情]

啥是连接池

数据库连接池是在程序初始化时创建一定数量的数据库连接对象并将其保存在一块内存区中,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个;释放空闲时间超过最大空闲时间的数据库连接以避免因为没有释放数据库连接而引起的数据库连接遗漏。

为啥要使用连接池呢

其实在业务量流量不大,并发量也不大的情况下,比如我们自己写实验,连接临时建立完全可以。 但是在企业里面,我们并发量很大,达到百级、千级,其中建立连接、关闭连接的操作会造成性能瓶颈,所以得考虑连接池来优化 操作:

  1. 取出连接(业务服务启动时,初始化若干个连接,放在连接存储中)
  2. 发送请求(当有请求,从连接存储中中取出)
  3. 放回连接(执行完毕,连接放回连接存储中)

这里对连接存储的数据结构,并维护连接,就是连接池。

池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

关于commons-dbutils

在经历过三层架构的学习后,我们知道对于增删改的通用方法 execute(String sql ,Object[] os) 来说,只要传入 sql 参数和置换参数 os ,就能实现相应的增删改功能;但对于查询方法 query(String sql, Object[] os) 而言,为了能够通用,只能封装到结果集 ResultSet ,而不能继续封装成对象或集合等类型。

要解决【无论是增删改还是都可以得到彻底封装】的问题,我们就得使用 commons-dbutils 类库来实现。

commons-dbutils 是 Apache 组织提供的一个 JDBC 工具类库,极大地简化了 JDBC 的代码量,并且不会影响程序的性能。

commons-dbutils 类库主要包含了两个类和一个接口,如下表:

全 名类 或 接 口
org.apache.commons.dbutils.DbUtilsclass
org.apache.commons.dbutils.QueryRunnerclass
org.apache.commons.dbutils.ResultSetHandlerintereface

DbUtils 类

DbUtils 是一个工具类,提供了关闭连接、事务提交/回滚、注册 JDBC 驱动程序等常用方法。DbUtils 类中的方法都是 public static 修饰的(除了构造方法),常用方法如下图所示。

图片描述

QueryRunner 类

QueryRunner 类主要用于执行增删改查等 SQL 语句。特别的,如果执行的是查询 SQL,还需要结合 ResultSetHandler 接口来处理结果集。QueryRunner 类的常用方法如下图所示。

图片描述

ResultSetHandler 接口及其实现类

ResultSetHandler 接口用于处理 ResultSet 结果集,它可以将结果集中的数据封装成单个对象、数组、List、Map 等不同形式。

ResultSetHandler 接口有很多不同的实现类,如下图所示。

图片描述

这里我们介绍一下事务

在学习事务管理之前,我们有必要了解一下 ThreadLocal<T> 类。

ThreadLocal 可以为变量在每个线程中都创建一个副本,每个线程都可以访问自己内部的副本变量。因此,ThreadLocal 被称为线程本地变量(或线程本地存储)。

观察下面的代码,思考哪些地方可能会引发的线程安全问题?

public class ConnectionManager {
    private static Connection conn = null;
    public static Connection getConnection() throws /*…*/    {
        if(conn == null)    {
               conn = DriverManager.getConnection(/*...*/);
           }
           return conn;
    }

    public static void closeConnection() throws /*…*/{
        if(conn!=null)
            conn.close();
    }
}

多线程场景下引发的问题分析:

  1. 因为 conn 是静态全局变量(用于共享),那么就有可能在一个线程使用 conn 操作数据库时,另外一个线程也同时调用 closeConnection() 关闭连接。
  2. 如果多个线程同时进入 getConnection() 方法的 if 语句,那么就会多次创建 conn 对象。

关于上述问题,你可能会想到使用“线程同步”来解决:将 conn 变量、getConnection() 和 closeConnection() 使用 synchronized 进行同步处理。以上面的代码为例,“线程同步”虽然可以解决问题,但会造成极大的性能影响:因为使用了线程同步后,当一个线程正在使用 conn 访问数据库时,其它线程只能等待。

以上面代码为例,引发线程安全问题的实质是因为 conn 变量、getConnection() 和 closeConnection() 都是共享的 static 变量(或方法)而造成的。实际上,一个线程只需要维护自己的 conn 变量,而不需要关心其它线程是否对各自的 conn 进行了修改。因此,不用 static 修饰也是可以的,如下:

public class ConnectionManager {
    // 没有 static 修饰
    private Connection conn = null;
    // 没有 static 修饰
    public Connection getConnection() throws SQLException {
        if (conn == null) {
            conn = DriverManager.getConnection(/*...*/);
        }
        return conn;
    }
    // 没有 static 修饰
    public void closeConnection() throws SQLException {
        if (conn != null)
            conn.close();
    }
}
class Dao{
       public void insert() throws SQLException {
           // 将 connectionManager 和 conn 定义为局部变量
        ConnectionManager connectionManager = new ConnectionManager();
        Connection conn = connectionManager.getConnection();
        // 使用 conn 访问数据库...
        connectionManager.closeConnection();
    }
}

以上代码,将 conn 及相关方法的 static 修饰符去掉,然后在每个使用 conn 的方法中(如 insert())都创建局部变量 conn。这样一来,因为每次都是在方法内部创建的连接,那么线程之间自然不存在线程安全问题。但是,由于在方法中的 conn 变量会在方法结束时自动释放空间,因此频繁的方法调用就会频繁地开启和关闭数据库连接,从而严重影响程序执行性能。

ThreadLocal<T> 可以在每个线程中对该变量创建一个副本,即每个线程内部都会有一个该变量的副本,该副本在线程内部任何地方都可以共享使用,但不同线程的副本之间互不影响。

ThreadLocal<T> 类中有以下几个常用方法,如表所示:

方 法简 介
public T get()获取 ThreadLocal 在当前线程中保存的变量副本。
public void set(T value)设置当前线程中变量的副本。
public void remove()移除当前线程中变量的副本。
protected T initialValue()延迟加载的方法,一般在使用时重写该方法。

连接池和 DbUtils 这个工具类主要有以下重点:

  • 数据库连接池可以分配、管理及释放数据库连接,可以使得应用程序重复地使用一个已有的数据库连接,而不再是重新建立一个。
  • 常用的数据源包括 Tomcat 内置数据源(Apache dbcp)、DBCP 数据源和 C3P0 数据源等,并且很多数据源都有基于配置文件和纯编码两种使用方式。
  • DbUtils 是一个工具类,提供了关闭连接、事务提交/回滚、注册 JDBC 驱动程序等常用方法。
  • QueryRunner 类主要用于执行增删改查等 SQL 语句。特别的,如果执行的是查询 SQL,还需要结合 ResultSetHandler 接口来处理结果集。
  • ResultSetHandler 接口用于处理 ResultSet 结果集,它可以将结果集中的数据封装成单个对象、数组、List 、Map 等不同形式。
  • ArrayHandler 类可以把结果集中的第一行数据封装成 Object[] 。
  • ArrayListHandler 类可以把结果集中的每行数据都封装成一个 Object[] 对象,然后再将所有的 Object[] 组装成一个 List 对象。
  • BeanHandler<T> 类可以把结果集中的第一行数据封装成 JavaBean 。
  • BeanListHandler<T> 类可以把结果集中的每行数据都封装成一个 JavaBean 对象,然后再将所有的 JavaBean 对象组装成一个 List 对象。
  • MapHandler 可以将结果集中的第一条数据封装到 Map 对象中,并且 key 是字段名,value 是字段值。
  • MapListHandler 可以将结果集中的每条数据都封装到 Map 对象中,然后再将所有的 Map 对象组装成一个 List 对象。
  • ColumnListHandler <T> 可以把结果集中某一列的值封装到 List 集合中。
  • ScalarHandler<T> 类用于封装单值结果。

图片描述

  • 使用 JNDI 定义的变量(通过 context.xml 中的 Environment 元素定义),可以在同一个 Tomcat 中的任意一个 Web 项目中使用。
  • 连接池减少连接创建时间。连接池中的连接已准备好的,可重复使用的,获取后可以直接使用访问数据库,因此减少了连接创建的次数和时间。
  • 连接池简化编程模式。当使用连接池时,每一个单独的线程能够创建一个自己的 JDBC 连接一样操作,允许用户直接使用 JDBC 编程技术。
  • 连接池控制资源的使用。如果不使用连接池,每次访问访问数据库都需要创建一个连接,这样系统的稳定性受系统连接需求影响很大,很容易产生资源浪费和高负载异常。连接池能够使用性能最大化,将资源利用控制在一定的水平之下。连接池能控制池中的连接数量,增强了新系统在大量用户应用时的稳定性。
  • ThreadLocal<T> 可以在每个线程中为变量创建一个副本,即每个线程内部都会有一个该变量的副本,该副本在线程内部任何地方都可以共享使用,但不同线程的副本之间互不影响。

写在最后

其实本文对连接池的理解还是比较片面,希望大家在看了以后还是好好深入理解一下。