数据库连接池

在实际开发项目的过程中,总是会需要连接数据库,而连接数据库的主要目的就是通过获取的连接来执行 SQL 语句,但是在有关数据库的开发中,与数据库的连接是十分重要的资源,如果每次都是在需要连接时创建,使用完之后就立即销毁的话,这样就会耗费大量的资源,比较好的做法就是使用的一个容器来管理与数据库的连接,应用初始化的时候便新建一定数量的连接放到容器中,当有程序需要连接的时候,便直接从这个容器中获取,程序使用完之后也不是立即销毁该连接,而是将连接放回到容器当中,以供其它程序获取使用,当然,如果在某一时刻需要获取连接的数量增加,甚至大于容器中初始化时创建的连接数量,这时候管理数据库连接的容器也会一次性增加一定量的数据库连接,不够的时候就会以此方法递增,上面所说的管理数据库连接的容器就是数据库连接池了。

1.JDBC的事务管理

JDBC 中事务的管理是交给 Connection (连接)对象来完成的,其中和事务管理相关的主要是以下几个方法:

void setAutoCommit(boolean autoCommit)

如果传入的参数是false,则代表关闭自动提交功能,也就是开启事务,手动提交事务

void commit()

这个便是提交事务的方法

void rollback()

这个方法用来执行回滚事务,当一个完整的事务在执行过程中出现异常时,我们就需要使用这个方法进行事务的回滚了。

2.JDBC事务管理案例

下面写一个转账案例来说明一个事务完整的重要性,在数据库中可以看到相关账户的数据是否变化。

首先定义一个账户类(Account):

public class Account {
    private int id;
    private String name;
    private double money;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }

    public Account() {
        super();
    }

    public Account(int id, String name, double money) {
        super();
        this.id = id;
        this.name = name;
        this.money = money;
    }

    @Override
    public String toString() {
        return "Account [id=" + id + ", name=" + name + ", money=" + money
                + "]";
    }
}

接下来的程序将会演示完成一个转账操作,主要分两步,一步便是将一个账户中的数量减少,另一步便是将另一个账户中的数量增加,如果在这两步操作的过程中发生错误,就很可能导致钱数不一致,但使用事务管理的话,就可以使两步操作要么一起成功,要么一起失败。

public class TransactionException {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstm = null;

        try {
            conn = JDBCUtils.getConnection();
            conn.setAutoCommit(false);
            String sql = "update account set money = money+? where name =?";
            pstm = conn.prepareStatement(sql);
            pstm.setDouble(1, -1000);
            pstm.setString(2, "aaa");
            pstm.executeUpdate();

            int i = 1 / 0;

            pstm.setDouble(1, 1000);
            pstm.setString(2, "bbb");
            pstm.executeUpdate();
            conn.commit();
        } catch (SQLException e) {
            try {
                conn.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            JDBCUtils.release(pstm, conn);
        }
    }
}

正常的逻辑应该是两者都顺利提交,不过依照上面的逻辑,即使发生错误,也会执行回滚操作,以便让它们两者都不提交,这样就能保证不发生错误,程序最后也会释放掉所有的数据库资源。JDBCUtils 这个类是抽象出来操作数据库的工具类。

public class JDBCUtils {
    private static final String driverClassName;
    private static final String url;
    private static final String user;
    private static final String password;

    static {
        Properties prop = new Properties();
        try {
            prop.load(new FileInputStream(new File( "src/db.properties")));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        driverClassName = prop.getProperty("driverClassName");
        url = prop.getProperty("url");
        user = prop.getProperty("user");
        password = prop.getProperty("password");
    }

    /**
     * 加载数据库驱动
     */
    public static void loadDriver() {
        try {
            Class.forName(driverClassName);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取数据库连接
     * @return
     */
    public static Connection getConnection() {
        Connection conn = null;
        try {
            loadDriver();
            conn = DriverManager.getConnection(url, user, password);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return conn;
    }

    /**
     * 释放资源
     * @param statement  语句执行对象
     * @param conn 数据库连接对象
     */
    public static void release(Statement statement, Connection conn) {
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            statement = null;
        }

        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            conn = null;
        }
    }

    /**
     * 释放连接
     * @param rs 结果集对象
     * @param statement 语句执行对象
     * @param conn 数据库连接对象
     */
    public static void release(ResultSet rs, Statement statement,
            Connection conn) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            rs = null;
        }

        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            statement = null;
        }

        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            conn = null;
        }
    }
}

使用上面这个工具类的时候,会首先加载数据库配置文件中的信息。

driverClassName = com.mysql.jdbc.Driver
url = jdbc:mysql://localhost:3306/test4
user = root
password = 123456

3.数据库连接池

这里主要是介绍两种数据库连接池的使用,就是 DruidC3P0 连接池的使用,重点的话就是各个数据库连接池的配置文件应该怎么写,以及应该放到什么地方,其实关于配置文件的书写,它们各自的 API 文档都给予了详细的说明。

3.1 Druid连接池的使用

要想使用 Druid 数据库连接池,第一步必须做的就是导入相应的 jar 包了,只有导入了相应的 jar 包才能使用其中相应的功能,关于 jar 包的获取当然还是去官网下载是最好的,其次的话就是关于数据库的操作都应该导入相应数据库的驱动包,MySQL 数据库就应该导入 MySQL 的驱动包,Oracle 数据库就应该导入 Oracle 的驱动包,因此使用连接池时要注意需要导入的驱动包是两个,一个是连接池的 jar 包,另一个则是数据库的驱动包。

首先看下面的代码,就是使用 Druid 连接池获取驱动并执行相关数据库操作了。

public class DruidDemo {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstm = null;
        ResultSet rs = null;

        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/test");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        try {
            conn = dataSource.getConnection();
            String sql = "select * from account";
            pstm = conn.prepareStatement(sql);
            rs = pstm.executeQuery();
            while (rs.next()) {
                System.out.println(rs.getInt("id") + "\t"
                        + rs.getString("name") + "\t" + rs.getDouble("money"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.release(rs, pstm, conn);
        }
    }
}

从上面的程序中很明显的可以看出,现在是直接将与数据库进行连接的几个属性写入了代码之中,包括驱动类名称、数据库连接地址、用户名以及密码,这样在平时自己编程时是没有问题的,不过在实际开发中一般不使用这种方式,因为如果有需要要修改这些属性的时候,那我们就必须修改源代码了,更好的做法是将这些属性信息放到一个配置文件当中,然后在程序启动时再去加载这些配置信息。

druid.property

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/test4
username=root
password=123456
#<!-- 初始化连接 -->
initialSize=10

#最大连接数量
maxActive=50

#<!-- 最大空闲连接 -->
maxIdle=20

#<!-- 最小空闲连接 -->
minIdle=5

#<!-- 超时等待时间以毫秒为单位 6000毫秒/1000等于60秒 -->
maxWait=60000

其中具体的属性设置当然还是要根据自己的实际要求来设置。

下面是加载配置文件信息再进行相关数据库操作的案例。

public class DruidDemo2 {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstm = null;
        ResultSet rs = null;

        Properties properties = new Properties();
        try {
            properties.load(new FileInputStream(new File("src/druid.properties")));
            DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
            conn = dataSource.getConnection();
            String sql = "select * from account";
            pstm = conn.prepareStatement(sql);
            rs = pstm.executeQuery();
            while(rs.next()) {
                System.out.println(rs.getInt("id")+"\t"+rs.getString("name")+"\t"+rs.getDouble("money"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.release(rs, pstm, conn);
        }
    }
}

3.2 C3P0连接池的使用

想要使用 c3p0 数据库连接池,首先也是要导入相应的 jar 包,同时和上面一样的就是凡是要使用数据库的操作,也都必须要导入数据库相关 jar 包。下面就是一个使用 c3p0 数据库连接池的案例程序:

public class C3p0Demo {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstm = null;
        ResultSet rs = null;
        ComboPooledDataSource cpds = new ComboPooledDataSource();
        try {
            cpds.setDriverClass("com.mysql.jdbc.Driver");
            cpds.setJdbcUrl("jdbc:mysql://localhost:3306/test");
            cpds.setUser("root");
            cpds.setPassword("123456");
            conn = cpds.getConnection();
            String sql = "select * from account";
            pstm = conn.prepareStatement(sql);
            rs = pstm.executeQuery();
            while (rs.next()) {
                System.out.println(rs.getInt("id") + "\t"
                        + rs.getString("name") + "\t" + rs.getDouble("money"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.release(rs, pstm, conn);
        }
    }
}

上面的代码直接将与数据库的连接属性都写到了程序之中,平时开发中肯定是不会这样做的,因为这样不利于程序的维护,通常都是将连接池相关的属性放到一个配置文件当中,然后在程序运行时自动去加载,需要注意的是 c3p0 数据库连接池配置文件放置的位置,这里是创建的 Java 工程,所以应该将相关的配置文件放到 src 目录下,具体的配置文件应该如何书写以及应该存放的位置,API 中都会详细地介绍。

c3p0-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
    <default-config>
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="jdbcUrl">jdbc:mysql://localhost:3306/test</property>
        <property name="user">root</property>
        <property name="password">123456</property>
        <property name="automaticTestTable">con_test</property>
        <property name="checkoutTimeout">30000</property>
        <property name="idleConnectionTestPeriod">30</property>
        <property name="initialPoolSize">10</property>
        <property name="maxIdleTime">30</property>
        <property name="maxPoolSize">100</property>
        <property name="minPoolSize">10</property>
        <property name="maxStatements">200</property>
    </default-config>
</c3p0-config>

只要像上面这样正确地配置了相关属性,以及将配置文件放到了恰当地位置,程序在启动时便会自动加载该配置文件。

public class C3p0Demo2 {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstm = null;
        ResultSet rs = null;
        ComboPooledDataSource cpds = new ComboPooledDataSource();
        try {
            conn = cpds.getConnection();
            String sql = "select * from account";
            pstm = conn.prepareStatement(sql);
            rs = pstm.executeQuery();
            while (rs.next()) {
                System.out.println(rs.getInt("id") + "\t"
                        + rs.getString("name") + "\t" + rs.getDouble("money"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.release(rs, pstm, conn);
        }
    }
}

4.JDBC工具类改进

下面想说一下 JDBC 的工具类,其实上面的 JDBCUtils 就是一个工具类,不过在我们使用连接池之后,比如 c3p0 连接池,连接池是为了减少频繁地创建连接以及销毁连接,但我们之前在使用连接池的时候,每次运行程序都会创建一个连接池,这样就是适得其反了,因为很显然频繁地创建连接池比创建连接是更耗费资源的,因此这里应该做优化,最主要的就是连接池只用创建一个就好了。

public class JDBCUtils2 {
    private static final ComboPooledDataSource dataSource = new ComboPooledDataSource();

    /**
     * 返回连接池对象
     * @return
     */
    public static ComboPooledDataSource getDataSource() {
        return dataSource;
    }

    /**
     * 获取数据库连接
     * @return
     * @throws SQLException 
     */
    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

    /**
     * 释放资源
     * @param statement 语句执行对象
     * @param conn  数据库连接对象
     */
    public static void release(Statement statement, Connection conn) {
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            statement = null;
        }

        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            conn = null;
        }
    }

    /**
     * 释放连接
     * @param rs 结果集对象
     * @param statement 语句执行对象
     * @param conn 数据库连接对象
     */
    public static void release(ResultSet rs, Statement statement,
            Connection conn) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            rs = null;
        }

        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            statement = null;
        }

        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            conn = null;
        }
    }
}

5.总结

关于数据库连接池,重点就在于理解它解决了什么问题,以及如何去使用它,其实在各个连接池的 API 中都会给出如何书写配置信息的详细介绍,当然配置文件的放置位置也是应该要注意的。

坚持原创技术分享,您的支持将鼓励我继续创作!