Java
Java Web基础

事务和数据库连接池

简介:事务指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部不成功。

1. 数据库设计三大范式

为了建立冗余较小、结构合理的数据库,设计数据库时必须遵循一定的规则。在关系型数据库中这种规则就称为范式。范式是符合某一种设计要求的总结。要想设计一个结构合理的关系型数据库,必须满足一定的范式。

在实际开发中最为常见的设计范式有三个:

  1. 第一范式:确保每列保持原子性。

第一范式是最基本的范式。如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式。

第一范式的合理遵循需要根据系统的实际需求来定。比如某些数据库系统中需要用到“地址”这个属性,本来直接将“地址”属性设计成一个数据库表的字段就行。但是如果系统经常会访问“地址”属性中的“城市”部分,那么就非要将“地址”这个属性重新拆分为省份、城市、详细地址等多个部分进行存储,这样在对地址中某一部分操作的时候将非常方便。这样设计才算满足了数据库的第一范式,如下表所示:

编号 姓名 性别 年龄 联系电话 省份 城市 详细地址
1 小张 20 021-11111111 上海 上海 浦东大道100号
2 小李 22 010-22222222 北京 北京 朝阳区新华路1号
3 小刘 21 020-33333333 广东 广州 海珠区丰收路20号
4 小王 20 0755-44444444 广东 深圳 宝安区大学路15号

上表所示的用户信息遵循了第一范式的要求,这样在对用户使用城市进行分类的时候就非常方便,也提高了数据库的性能。

  1. 第二范式:确保表中的每列都和主键相关。

第二范式在第一范式的基础之上更进一层。第二范式需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)。也就是说在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中。

比如要设计一个订单信息表,因为订单中可能会有多种商品,所以要将订单编号和商品编号作为数据库表的联合主键,如下表所示。

订单编号 商品编号 商品名称 数量 单位 商品单价 客户 所属公司 联系方式
001 1 牙刷 400 2.00 小张 上海公司 021-11111111
002 2 卷纸 500 3.00 小李 北京公司 010-22222222
001 3 沐浴露 300 10.00 小张 上海公司 021-11111111
003 4 牙膏 200 15.00 小王 深圳公司 0755-4444444

这样就产生一个问题:这个表中是以订单编号和商品编号作为联合主键。这样在该表中商品名称、单位、商品单价等信息不与该表的主键相关,而仅仅是与商品编号相关。所以在这里违反了第二范式的设计原则。

而如果把这个订单信息表进行拆分,把商品信息分离到另一个表中,把订单项目表也分离到另一个表中,就非常完美了。如下所示:

  • 订单信息表:
订单编号 客户 所属公司 联系方式
001 小张 上海公司 021-11111111
002 小李 北京公司 010-22222222
003 小王 深圳公司 0755-4444444
  • 订单项目表:
订单编号 商品编号 数量
001 1 400
001 3 300
002 2 500
003 4 200
  • 商品信息表:
商品编号 商品名称 单位 商品单价
1 牙刷 2.00
2 卷纸 3.00
3 沐浴露 10.00
4 牙膏 15.00

这样设计,在很大程度上减小了数据库的冗余。如果要获取订单的商品信息,使用商品编号到商品信息表中查询即可。

  1. 第三范式(确保每列都和主键列直接相关,而不是间接相关)

第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关。

比如在设计一个订单数据表的时候,可以将客户编号作为一个外键和订单表建立相应的关系。而不可以在订单表中添加关于客户其它信息(比如姓名、所属公司等)的字段。如下面这两个表所示的设计就是一个满足第三范式的数据库表。

  • 订单信息表
订单编号 订单项目 负责人 业务员 订单数量 单位 客户编号
001 牙刷 负责人1 业务员1 400 1
002 卷纸 负责人2 业务员2 500 2
001 沐浴露 负责人3 业务员3 300 1
003 牙膏 负责人4 业务员4 200 4
  • 客户信息表
编号 姓名 联系电话 所属公司
1 小张 021-11111111 上海公司
2 小李 010-22222222 北京公司
3 小刘 020-33333333 广州公司
4 小王 0755-44444444 深圳公司

这样在查询订单信息的时候,就可以使用客户编号来引用客户信息表中的记录,也不必在订单信息表中多次输入客户信息的内容,减小了数据冗余。

2. 数据库事务

事务指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部不成功。

以MySQL为例,MySQL引擎是支持事务的,同时MySQL默认自动提交事务,每条语句都处在单独的事务中。开发者可以手动控制事务:

  • -- 开启事务
  • start transaction;
  • -- 提交事务
  • commit;
  • -- 回滚事务
  • rollback;

注:MySQL中set autocommit = 0start transaction区别:set autocommit=0指事务非自动提交,自此句执行以后,每个SQL语句或者语句块所在的事务都需要显式调用commit才能提交事务。
1. 不管autocommit是1还是0,start transaction后,只有当commit数据才会生效,rollback后就会回滚。
2. 当autocommit为0时,不管有没有start transaction,只有当commit数据才会生效,rollback后就会回滚。
3. 如果autocommit为1 ,并且没有start transaction,调用rollback是没有用的。即便设置了SAVEPOINT。

2.1. JDBC如何控制事务

JDBC中使用代码控制事务,需要首先关闭自动提交,然后根据代码逻辑来进行提交和回滚操作,类似示例在之前的使用JDBC操作数据库一文中有相应代码,这里再次贴上:

Java
  • public static void testInsert() {
  • // 1. 指定对象引用
  • Connection connection = null;
  • PreparedStatement pstmt = null;
  • try {
  • // 2. 创建连接
  • connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc_test", "root", "12345678");
  • connection.setAutoCommit(false);
  • // 3. 得到执行sql语句的Statement对象
  • String sql = "insert into users(name, gender, email, birthday) values (?, ?, ?, ?);";
  • pstmt = connection.prepareStatement(sql);
  • pstmt.setString(1, "John");
  • pstmt.setString(2, "Male");
  • pstmt.setString(3, "John@coderap.com");
  • pstmt.setDate(4, new Date(new java.util.Date().getTime()));
  • // 4. 执行sql语句,并返回结果
  • int result = pstmt.executeUpdate();
  • // 提交事务
  • connection.commit();
  • System.out.println("Result: " + result);
  • } catch (Exception e) {
  • e.printStackTrace();
  • // 回滚事务
  • try {
  • connection.rollback();
  • } catch (SQLException e1) {
  • e1.printStackTrace();
  • }
  • } finally {
  • // 6. 关闭资源
  • if (connection != null) {
  • try {
  • connection.close();
  • } catch (SQLException e) {
  • e.printStackTrace();
  • }
  • }
  • if (pstmt != null) {
  • try {
  • pstmt.close();
  • } catch (SQLException e) {
  • e.printStackTrace();
  • }
  • }
  • }
  • }

2.2. 事务的四大特性

  1. 原子性:事务包含的所有数据库操作要么全部成功,要不全部失败回滚。
  2. 一致性:一个事务执行之前和执行之后都必须处于一致性状态。拿转账来说,假设用户A和用户B两者的钱加起来一共是5000元,那么不管A和B之间如何转账,转几次账,事务结束两个用户的钱相加起来应该还得是5000元,这就是事务的一致性。
  3. 隔离性:一个事务未提交的业务结果是否对于其它事务可见。级别一般有read_uncommit、read_commit、read_repeatable、串行化访问。
  4. 持久性:一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

2.3. 事务的隔离级别

  1. 脏读:脏数据所指的就是未提交的数据。也就是说,事务A正在对一条记录做修改,在事务A完成并提交之前,这条数据是处于待定状态的(可能提交也可能回滚),这时事务B读取了这条没有提交的数据,此时如果事务A发生错误并执行回滚操作,那么事务B之前读取到的数据就是脏数据。这种现象被称为脏读。
时序 转账事务B 取款事务A
1 开始事务
2 开始事务
3 查询账户余额为2000元
4 取款1000元,余额被更改为1000元
5 查询账户余额为1000元(产生脏读)
6 取款操作发生未知错误,事务回滚,余额变更为2000元
7 转入2000元,余额被更改为3000元(脏读的1000+2000)
8 提交事务
备注:按照正确逻辑,此时账户余额应该为4000元
  1. 不可重复读(Non-Repeatable Reads):一个事务先后读取同一条记录,而事务在两次读取之间该数据被其它事务所修改,则两次读取的数据不同,我们称之为不可重复读。
时序 事务A 事务B
1 开始事务
2 第一次查询,账户余额为1000元
3 开始事务
4 其他操作
5 转入1000元,余额被更改为2000元
6 提交事务
7 第二次查询,账户余额为2000元
备注:按照正确逻辑,事务A在时序2和7读取到的数据应该一致
  1. 幻读(Phantom Reads):事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。
时序 事务A 事务B
1 开始事务
2 第一次查询,数据总量为100条
3 开始事务
4 其他操作
5 新增100条数据
6 提交事务
7 第二次查询,数据总量为200条
备注:按照正确逻辑,事务A在时序2和7读取到的数据总量应该一致

注1:不可重复读和幻读到底有什么区别呢?

不可重复读:是读取了其他事务更改的数据,针对insertupdate操作。

解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。

幻读:是读取了其他事务新增的数据,针对insertdelete操作。

解决:使用表级锁(范围锁RangeS,锁定检索范围为只读),锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。

数据库通过设置事务的隔离级别防止以上情况的发生。数据库事务的隔离级别有4个,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。

隔离级别 脏读 不可重复读 幻读
Read uncommitted
Read committed ×
Repeatable read × ×
Serializable × × ×
注:表示可能出现,×表示不会出现

我们讨论隔离级别的场景,主要是在多个事务并发的情况下。

  1. Read uncommitted(读未提交):当隔离级别设置为Read uncommitted时,就可能出现脏读,如何避免脏读,请看下一个隔离级别。
  2. Read committed(读已提交):当隔离级别设置为Read committed时,避免了脏读,但是可能会造成不可重复读。

注:大多数数据库的默认级别就是Read committed,比如SQL Server、Oracle。

  1. Repeatable read(可重复读):Repeatable read避免了不可重复读,但还有可能出现幻读。

注1:MySQL的默认隔离级别就是Repeatable read。
注2:不可重复读和脏读的区别是:脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
注3:不可重复读和幻读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。

  1. Serializable(串行化):最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻读。

2.4. 如何设置隔离级别

MySQL中,使用SELECT @@TX_ISOLATION;查看当前的事务隔离级别;修改隔离级别则有两种方式:

  • 永久修改:可以在my.cnf文件的[mysqld]节点里如下设置该选项transaction-isolation={READ-UNCOMMITTED / READ-COMMITTED / REPEATABLE-READ / SERIALIZABLE}
  • 临时修改:使用SET [SESSION / GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED / READ COMMITTED / REPEATABLE READ / SERIALIZABLE};设置当前的事务隔离级别,设置隔离级别必须在事务之前。如:
  • -- 查看隔离级别
  • mysql> SELECT @@TX_ISOLATION;
  • +-----------------+
  • | @@TX_ISOLATION |
  • +-----------------+
  • | REPEATABLE-READ |
  • +-----------------+
  • 1 row in set (0.00 sec)
  • -- 修改隔离级别
  • mysql> SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
  • Query OK, 0 rows affected (0.09 sec)
  • -- 再次查看隔离级别
  • mysql> SELECT @@TX_ISOLATION;
  • +----------------+
  • | @@TX_ISOLATION |
  • +----------------+
  • | SERIALIZABLE |
  • +----------------+
  • 1 row in set (0.00 sec)

注意:
- 默认的行为(不带SESSION或GLOBAL)是为下一个(未开始)事务设置隔离级别。
- 如果你会用GLOBAL关键字,语句在全局对从那点开始创建所有新连接设置默认事务级别,需要管理员权限来做这个。
- 使用SESSION关键字为将来在当前连接上执行的事务设置默认事务级别。
- 任何客户端都能自由改变会话隔离级别(甚至在事务的中间),或者为下一个事务设置隔离级别。

在JDBC中也可以使用Connection对象控制事务的隔离级别:

  • connection.setTransactionIsolation(Connection.TRANSACTION_NONE);

可传入的值有五个:

  • Connection.TRANSACTION_NONE(0)
  • Connection.TRANSACTION_READ_UNCOMMITTED(1)
  • Connection.TRANSACTION_READ_COMMITTED(2)
  • Connection.TRANSACTION_REPEATABLE_READ(4)
  • Connection.TRANSACTION_SERIALIZABLE(8)

3. 连接池

数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现的尤为突出。对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响到程序的性能指标。数据库连接池正式针对这个问题提出来的。数据库连接池负责分配,管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。

数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数来设定的。无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量。连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。

数据库连接池的最小连接数和最大连接数的设置要考虑到以下几个因素:

  • 最小连接数是连接池一直保持的数据库连接,所以如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费。
  • 最大连接数是连接池能申请的最大连接数,如果数据库连接请求超过次数,后面的数据库连接请求将被加入到等待队列中,这会影响以后的数据库操作。
  • 如果最小连接数与最大连接数相差很大,那么最先连接请求将会获利,之后超过最小连接数量的连接请求等价于建立一个新的数据库连接。不过,这些大于最小连接数的数据库连接在使用完不会马上被释放,他将被放到连接池中等待重复使用或是空间超时后被释放。

3.1. 编写数据库连接池

编写连接池需实现java.sql.DataSource接口,这个接口中定义了两个重载的getConnection方法:

  • Connection getConnection() throws SQLException;
  • Connection getConnection(String username, String password) throws SQLException;

实现DataSource接口,并实现连接池功能的步骤:

  1. 在DataSource构造函数中批量创建与数据库的连接,并把创建的连接加入LinkedList对象中。
  2. 实现getConnection方法,让getConnection方法每次调用时,从LinkedList中取一个Connection返回给用户。
  3. 当用户使用完Connection,调用Connection.close()方法时,Collection对象应保证将自己返回到LinkedList中,而不要把Connection还给数据库。Collection保证将自己返回到LinkedList中是此处编程的难点。

数据库连接池编写示例:

Java
  • package com.coderap.datasource;
  • import javax.sql.DataSource;
  • import java.io.InputStream;
  • import java.io.PrintWriter;
  • import java.lang.reflect.InvocationHandler;
  • import java.lang.reflect.Method;
  • import java.lang.reflect.Proxy;
  • import java.sql.Connection;
  • import java.sql.DriverManager;
  • import java.sql.SQLException;
  • import java.util.LinkedList;
  • import java.util.Properties;
  • public class JdbcPool implements DataSource {
  • // 用于装载所有连接的连接池
  • private static LinkedList<Connection> connectionLinkedList = new LinkedList<Connection>();
  • static {
  • // 获取配置文件
  • InputStream resourceAsStream = JdbcPool.class.getClassLoader().getResourceAsStream("db.properties");
  • Properties properties = new Properties();
  • try {
  • properties.load(resourceAsStream);
  • String driver = properties.getProperty("driver");
  • String url = properties.getProperty("url");
  • String username = properties.getProperty("username");
  • String password = properties.getProperty("password");
  • int jdbcPoolInitSize = Integer.parseInt(properties.getProperty("jdbcPoolInitSize"));
  • Class.forName(driver);
  • // 初始化创建一些数据库连接
  • for (int i = 0; i < jdbcPoolInitSize; i++) {
  • Connection connection = DriverManager.getConnection(url, username, password);
  • connectionLinkedList.add(connection);
  • System.out.println("添加Connection, index: " + (i + 1));
  • }
  • } catch (Exception e) {
  • throw new ExceptionInInitializerError(e);
  • }
  • }
  • @Override
  • public Connection getConnection() throws SQLException {
  • if (connectionLinkedList.size() > 0) {
  • final Connection connection = connectionLinkedList.removeFirst();
  • System.out.println("获取数据库连接,剩余连接数: " + connectionLinkedList.size());
  • return (Connection) Proxy.newProxyInstance(JdbcPool.class.getClassLoader(), connection.getClass().getInterfaces(), new InvocationHandler() {
  • @Override
  • public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  • if (!method.getName().equals("close")) {
  • // 不是调用close方法,即执行相应的操作
  • return method.invoke(connection, args);
  • } else {
  • // 如果是调用close方法,则将数据库连接归还
  • connectionLinkedList.add(connection);
  • System.out.println("已归还数据库连接,剩余连接数: " + connectionLinkedList.size());
  • return null;
  • }
  • }
  • });
  • } else {
  • throw new RuntimeException("数据库连接失败");
  • }
  • }
  • @Override
  • public Connection getConnection(String username, String password) throws SQLException {
  • return null;
  • }
  • @Override
  • public <T> T unwrap(Class<T> iface) throws SQLException {
  • return null;
  • }
  • @Override
  • public boolean isWrapperFor(Class<?> iface) throws SQLException {
  • return false;
  • }
  • @Override
  • public PrintWriter getLogWriter() throws SQLException {
  • return null;
  • }
  • @Override
  • public void setLogWriter(PrintWriter out) throws SQLException {
  • }
  • @Override
  • public void setLoginTimeout(int seconds) throws SQLException {
  • }
  • @Override
  • public int getLoginTimeout() throws SQLException {
  • return 0;
  • }
  • }

在本例中,db.properties配置文件如下:

  • driver=com.mysql.jdbc.Driver
  • url=jdbc:mysql://localhost:3306/jdbc_test
  • username=root
  • password=12345678
  • jdbcPoolInitSize=10

另外,可以写一个JdbcUtil工具类来封装获取数据库连接和归还数据库连接的操作:

Java
  • package com.coderap.datasource;
  • import java.sql.Connection;
  • import java.sql.ResultSet;
  • import java.sql.SQLException;
  • import java.sql.Statement;
  • public class JdbcUtil {
  • // 数据库连接池
  • private static JdbcPool pool = new JdbcPool();
  • /**
  • * 从数据库连接池中获取数据库连接对象
  • */
  • public static Connection getConnection() throws SQLException {
  • return pool.getConnection();
  • }
  • /**
  • * 释放的资源包括Connection数据库连接对象,负责执行SQL命令的Statement对象,存储查询结果的ResultSet对象
  • */
  • public static void release(Connection conn, Statement st, ResultSet rs) {
  • if (rs != null) {
  • try {
  • //关闭存储查询结果的ResultSet对象
  • rs.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • rs = null;
  • }
  • if (st != null) {
  • try {
  • //关闭负责执行SQL命令的Statement对象
  • st.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • if (conn != null) {
  • try {
  • //关闭Connection数据库连接对象
  • conn.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • }
  • }

有了工具类,可以进行下面的测试:

Java
  • @Test
  • public void testDataSource() {
  • Connection connection = null;
  • Statement statement = null;
  • ResultSet resultSet = null;
  • try {
  • connection = JdbcUtil.getConnection();
  • String sql = "select name, gender, email from users;";
  • statement = connection.createStatement();
  • resultSet = statement.executeQuery(sql);
  • while (resultSet.next()) {
  • String name = resultSet.getString(1);
  • String gender = resultSet.getString(2);
  • String email = resultSet.getString(3);
  • System.out.println("name: " + name + ", gender: " + gender + ", email: " + email);
  • }
  • } catch (SQLException e) {
  • e.printStackTrace();
  • } finally {
  • JdbcUtil.release(connection, statement, resultSet);
  • }
  • }

测试打印结果如下:

  • 添加Connection, index: 1
  • 添加Connection, index: 2
  • 添加Connection, index: 3
  • 添加Connection, index: 4
  • 添加Connection, index: 5
  • 添加Connection, index: 6
  • 添加Connection, index: 7
  • 添加Connection, index: 8
  • 添加Connection, index: 9
  • 添加Connection, index: 10
  • 获取数据库连接,剩余连接数: 9
  • name: Tom, gender: Male, email: Tom@coderap.com
  • name: Jack, gender: Male, email: Jack@coderap.com
  • name: Marry, gender: Female, email: Marry@coderap.com
  • name: John, gender: Male, email: John@coderap.com
  • name: John, gender: Male, email: John@coderap.com
  • 已归还数据库连接,剩余连接数: 10

3.2. 开源数据库连接池

现在很多WEB服务器(Weblogic、WebSphere、Tomcat)都提供了DataSoruce的实现,即连接池的实现。通常我们把DataSource的实现,按其英文含义称之为数据源,数据源中都包含了数据库连接池的实现。也有一些开源组织提供了数据源的独立实现,常见的有以下几种:

  • DBCP数据库连接池
  • C3P0数据库连接池
  • HikariCP数据库连接池(是最近两年的新秀)

在使用了数据库连接池之后,在项目的实际开发中就不需要编写连接数据库的代码了,直接从数据源获得数据库的连接。这里分别介绍这三中数据库连接池

3.2.1. DBCP数据库连接池

要使用DBCP数据库连接池,首先需要引入它的相关Jar包,这里我们使用Maven来管理:

  • <!-- https://mvnrepository.com/artifact/commons-dbcp/commons-dbcp -->
  • <dependency>
  • <groupId>commons-dbcp</groupId>
  • <artifactId>commons-dbcp</artifactId>
  • <version>1.2.2</version>
  • </dependency>

从Maven管理的相关Jar包来看,DBCP依赖两个Jar包:commons-dbcp:commons-dbcp.jar和commons-pool:commons-pool.jar。

引入Jar包后,需要编写对应的配置文件,DBCP的配置文件也可以用Properties文件来编写,下面的示例配置列出了大多数常用的配置项:

  • # 连接设置
  • driverClassName=com.mysql.jdbc.Driver
  • url=jdbc:mysql://localhost:3306/jdbc_test
  • username=root
  • password=12345678
  • # 初始化连接
  • initialSize=10
  • # 最大连接数量
  • maxActive=50
  • # 最大空闲连接
  • maxIdle=20
  • # 最小空闲连接
  • minIdle=5
  • # 超时等待时间以毫秒为单位 6000毫秒/1000等于60秒
  • maxWait=60000
  • # JDBC驱动建立连接时附带的连接属性属性的格式必须为这样:[属性名=property;]
  • # 注意:"user" 与 "password" 两个属性会被明确地传递,因此这里不需要包含他们。
  • connectionProperties=useUnicode=true;characterEncoding=UTF8
  • # 指定由连接池所创建的连接的自动提交(auto-commit)状态。
  • defaultAutoCommit=true
  • # driver default 指定由连接池所创建的连接的只读(read-only)状态。
  • # 如果没有设置该值,则“setReadOnly”方法将不被调用。(某些驱动并不支持只读模式,如:Informix)
  • defaultReadOnly=
  • # driver default 指定由连接池所创建的连接的事务级别(TransactionIsolation)。
  • # 可用值为下列之一:NONE、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE
  • defaultTransactionIsolation=READ_UNCOMMITTED

接下来编写一个DBCPUtil的类封装DBCP对数据库连接的操作:

Java
  • package com.coderap.datasource;
  • import org.apache.commons.dbcp.BasicDataSourceFactory;
  • import javax.sql.DataSource;
  • import java.io.InputStream;
  • import java.sql.Connection;
  • import java.sql.ResultSet;
  • import java.sql.SQLException;
  • import java.sql.Statement;
  • import java.util.Properties;
  • public class DBCPUtil {
  • private static DataSource dataSource = null;
  • static {
  • InputStream resourceAsStream = DBCPUtil.class.getClassLoader().getResourceAsStream("dbcp.properties");
  • Properties properties = new Properties();
  • try {
  • properties.load(resourceAsStream);
  • dataSource = BasicDataSourceFactory.createDataSource(properties);
  • } catch (Exception e) {
  • e.printStackTrace();
  • System.out.println("连接数据库失败");
  • }
  • }
  • public static Connection getConnection() throws SQLException {
  • return dataSource.getConnection();
  • }
  • public static void release(Connection connection, Statement statement, ResultSet resultSet) {
  • if (resultSet != null) {
  • try {
  • resultSet.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • if (statement != null) {
  • try {
  • statement.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • if (connection != null) {
  • try {
  • // 这里的close操作其实是调用了DBCP中BasicDataSource实例的close,该类在DataSource的基础上做了封装
  • connection.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • }
  • }

下面是测试代码:

Java
  • @Test
  • public void testDBCPDataSource() {
  • Connection connection = null;
  • Statement statement = null;
  • ResultSet resultSet = null;
  • try {
  • connection = DBCPUtil.getConnection();
  • String sql = "select name, gender, email from users;";
  • statement = connection.createStatement();
  • resultSet = statement.executeQuery(sql);
  • while (resultSet.next()) {
  • String name = resultSet.getString(1);
  • String gender = resultSet.getString(2);
  • String email = resultSet.getString(3);
  • System.out.println("name: " + name + ", gender: " + gender + ", email: " + email);
  • }
  • } catch (SQLException e) {
  • e.printStackTrace();
  • } finally {
  • DBCPUtil.release(connection, statement, resultSet);
  • }
  • }

运行结果如下:

  • name: Tom, gender: Male, email: Tom@coderap.com
  • name: Jack, gender: Male, email: Jack@coderap.com
  • name: Marry, gender: Female, email: Marry@coderap.com
  • name: John, gender: Male, email: John@coderap.com
  • name: John, gender: Male, email: John@coderap.com

3.2.2. C3P0数据库连接池

与DBCP类似,要使用C3P0数据库连接池,首先需要引入它的相关Jar包,这里我们使用Maven来管理:

  • <!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
  • <dependency>
  • <groupId>com.mchange</groupId>
  • <artifactId>c3p0</artifactId>
  • <version>0.9.5</version>
  • </dependency>

从Maven管理的相关Jar包来看,DBCP依赖两个Jar包:com.mchange:c3p0.jar和com.mchange:mchange-commons-java.jar。

引入Jar包后,需要编写对应的配置文件,C3P0的配置文件需要用XML文件来编写,同时该文件的名称需要是c3p0-config.xml,该文件一般放在CLASSPATH根路径下:

Xml
  • <?xml version="1.0" encoding="UTF-8"?>
  • <c3p0-config>
  • <!--
  • C3P0的默认配置,ComboPooledDataSource ds = new ComboPooledDataSource();表示使用的是C3P0的默认配置信息来创建数据源
  • -->
  • <default-config>
  • <property name="driverClass">com.mysql.jdbc.Driver</property>
  • <property name="jdbcUrl">jdbc:mysql://localhost:3306/jdbc_test</property>
  • <property name="user">root</property>
  • <property name="password">12345678</property>
  • <property name="acquireIncrement">5</property>
  • <property name="initialPoolSize">10</property>
  • <property name="minPoolSize">5</property>
  • <property name="maxPoolSize">20</property>
  • </default-config>
  • <!--
  • C3P0的命名配置,ComboPooledDataSource ds = new ComboPooledDataSource("MySQL");表示使用的是name是MySQL的配置信息来创建数据源
  • -->
  • <named-config name="MySQL">
  • <property name="driverClass">com.mysql.jdbc.Driver</property>
  • <property name="jdbcUrl">jdbc:mysql://localhost:3306/jdbc_test</property>
  • <property name="user">root</property>
  • <property name="password">12345678</property>
  • <property name="acquireIncrement">5</property>
  • <property name="initialPoolSize">10</property>
  • <property name="minPoolSize">5</property>
  • <property name="maxPoolSize">20</property>
  • </named-config>
  • </c3p0-config>

C3P0支持多数据源的配置,只需要在配置文件中为每组配置命名,然后在加载的时候使用特定命名配置即可,下面是C3P0Util封装类:

Java
  • package com.coderap.datasource;
  • import com.mchange.v2.c3p0.ComboPooledDataSource;
  • import javax.sql.DataSource;
  • import java.sql.Connection;
  • import java.sql.ResultSet;
  • import java.sql.SQLException;
  • import java.sql.Statement;
  • public class C3P0Util {
  • private static DataSource dataSource = null;
  • static {
  • dataSource = new ComboPooledDataSource();
  • // dataSource = new ComboPooledDataSource("MySQL"); // 获取命名配置的连接
  • }
  • public static Connection getConnection() throws SQLException {
  • return dataSource.getConnection();
  • }
  • public static void release(Connection connection, Statement statement, ResultSet resultSet) {
  • if (resultSet != null) {
  • try {
  • resultSet.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • if (statement != null) {
  • try {
  • statement.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • if (connection != null) {
  • try {
  • // 这里的close操作其实是调用了C3P0中NewProxyConnectio代理的close,该类在DataSource的基础上做了封装
  • connection.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • }
  • }

下面是测试代码:

Java
  • @Test
  • public void testC3p0DataSource() {
  • Connection connection = null;
  • Statement statement = null;
  • ResultSet resultSet = null;
  • try {
  • connection = C3P0Util.getConnection();
  • String sql = "select name, gender, email from users;";
  • statement = connection.createStatement();
  • resultSet = statement.executeQuery(sql);
  • while (resultSet.next()) {
  • String name = resultSet.getString(1);
  • String gender = resultSet.getString(2);
  • String email = resultSet.getString(3);
  • System.out.println("name: " + name + ", gender: " + gender + ", email: " + email);
  • }
  • } catch (SQLException e) {
  • e.printStackTrace();
  • } finally {
  • C3P0Util.release(connection, statement, resultSet);
  • }
  • }

运行结果如下:

  • name: Tom, gender: Male, email: Tom@coderap.com
  • name: Jack, gender: Male, email: Jack@coderap.com
  • name: Marry, gender: Female, email: Marry@coderap.com
  • name: John, gender: Male, email: John@coderap.com
  • name: John, gender: Male, email: John@coderap.com

3.2.3. HikariCP数据库连接池

HikariCP数据库连接池是近段时间兴起的一种新的数据库连接池,号称是性能最好的数据库连接池。它的使用也非常简单,首先需要引入Jar包。需要注意的是,HikariCP各个JDK版本的Jar都是不同的,例如如果使用Maven进行管理,它有以下的几个版本:

  • <!--Java 8/9 maven artifact:-->
  • <dependency>
  • <groupId>com.zaxxer</groupId>
  • <artifactId>HikariCP</artifactId>
  • <version>3.1.0</version>
  • </dependency>
  • <!--Java 7 maven artifact (maintenance mode):-->
  • <dependency>
  • <groupId>com.zaxxer</groupId>
  • <artifactId>HikariCP-java7</artifactId>
  • <version>2.4.13</version>
  • </dependency>
  • <!--Java 6 maven artifact (maintenance mode):-->
  • <dependency>
  • <groupId>com.zaxxer</groupId>
  • <artifactId>HikariCP-java6</artifactId>
  • <version>2.3.13</version>
  • </dependency>

这里我们使用JDK1.6的环境,引入第三个依赖即可,HikariCP依赖了com.zaxxer:HikariCP-java6.jar和org.slf4j:slf4j-api两个Jar包。

接下来需要编写它的配置文件,这里使用MySQL,配置文件如下,依旧是Properties文件hikari.properties,位于CLASSPATH根路径下:

  • jdbcUrl=jdbc:mysql://localhost:3306/jdbc_test
  • username=root
  • password=12345678

注:需要注意的是,HikariCP的Github主页给出了多种配置方式,可以通过指定DataSource的方式,也可以通过JDBC URL的方式,但由于MySQL使用DataSource的方式会出现网络超时断线的问题,官方建议使用JDBC URL的方式进行配置。具体可以查看HikariCP项目Github主页说明。

接下来编写一个HikariUtil的类封装HikariCP对数据库连接的操作:

Java
  • package com.coderap.datasource;
  • import com.zaxxer.hikari.HikariConfig;
  • import com.zaxxer.hikari.HikariDataSource;
  • import javax.sql.DataSource;
  • import java.io.InputStream;
  • import java.sql.Connection;
  • import java.sql.ResultSet;
  • import java.sql.SQLException;
  • import java.sql.Statement;
  • import java.util.Properties;
  • public class HikariUtil {
  • private static DataSource dataSource;
  • static {
  • InputStream resourceAsStream = HikariUtil.class.getClassLoader().getResourceAsStream("hikari.properties");
  • Properties properties = new Properties();
  • try {
  • properties.load(resourceAsStream);
  • HikariConfig config = new HikariConfig(properties);
  • // 手动指定链接驱动,否则会报Unable to get driver instance for jdbcUrl的错误
  • config.setDriverClassName("com.mysql.jdbc.Driver");
  • dataSource = new HikariDataSource(config);
  • } catch (Exception e) {
  • e.printStackTrace();
  • System.out.println("连接数据库失败");
  • }
  • }
  • public static Connection getConnection() throws SQLException {
  • return dataSource.getConnection();
  • }
  • public static void release(Connection connection, Statement statement, ResultSet resultSet) {
  • if (resultSet != null) {
  • try {
  • resultSet.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • if (statement != null) {
  • try {
  • statement.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • if (connection != null) {
  • try {
  • // 这里的close操作其实是调用了HikariCP中HikariConnectionProxy代理实例的close,该类在DataSource的基础上做了封装
  • connection.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • }
  • }

下面是测试代码:

Java
  • @Test
  • public void testHikariDataSource() {
  • Connection connection = null;
  • Statement statement = null;
  • ResultSet resultSet = null;
  • try {
  • connection = HikariUtil.getConnection();
  • String sql = "select name, gender, email from users;";
  • statement = connection.createStatement();
  • resultSet = statement.executeQuery(sql);
  • while (resultSet.next()) {
  • String name = resultSet.getString(1);
  • String gender = resultSet.getString(2);
  • String email = resultSet.getString(3);
  • System.out.println("name: " + name + ", gender: " + gender + ", email: " + email);
  • }
  • } catch (SQLException e) {
  • e.printStackTrace();
  • } finally {
  • HikariUtil.release(connection, statement, resultSet);
  • }
  • }

运行结果如下:

  • name: Tom, gender: Male, email: Tom@coderap.com
  • name: Jack, gender: Male, email: Jack@coderap.com
  • name: Marry, gender: Female, email: Marry@coderap.com
  • name: John, gender: Male, email: John@coderap.com
  • name: John, gender: Male, email: John@coderap.com

3.3. JNDI实现容器连接池

JNDI(Java Naming and Directory Interface),Java命名和目录接口,它对应于J2SE中的javax.naming包,这套API的主要作用在于它可以把Java对象放在一个容器中(JNDI容器),并为容器中的Java对象取一个名称,以后程序想获得Java对象,只需 通过名称检索即可。其核心API为Context,它代表JNDI容器,其lookup方法为检索容器中对应名称的对象。

Tomcat服务器创建的数据源是以JNDI资源的形式发布的,因此可以通过JNDI为Tomcat服务器配置一个数据源,Tomcat文档介绍了如下的方式配置Tomcat服务器的数据源,我们需要在Web项目的META-INFO目录下创建名为context.xml的文件,并编写以下内容:

  • <?xml version="1.0" encoding="UTF-8"?>
  • <Context path="/">
  • <Resource name="jdbc/datasource"
  • auth="Container"
  • type="javax.sql.DataSource"
  • username="root"
  • password="12345678"
  • driverClassName="com.mysql.jdbc.Driver"
  • url="jdbc:mysql://localhost:3306/jdbc_test"
  • maxActive="8"
  • maxIdle="4" />
  • </Context>

Tomcat服务器创建好数据源之后是以JNDI的形式绑定到一个JNDI容器中的,我们可以把JNDI想象成一个大大的容器,我们可以往这个容器中存放一些对象或者资源,JNDI容器中存放的对象和资源都会有一个独一无二的名称,应用程序想从JNDI容器中获取资源时,只需要告诉JNDI容器要获取的资源的名称,JNDI根据名称去找到对应的资源后返回给应用程序。我们平时做JavaEE开发时,服务器会为我们的应用程序创建很多资源,比如request对象,response对象,服务器创建的这些资源有两种方式提供给我们的应用程序使用:第一种是通过方法参数的形式传递进来,比如我们在Servlet中写的doPostdoGet方法中使用到的request对象和response对象就是服务器以参数的形式传递给我们的。第二种就是JNDI的方式,服务器把创建好的资源绑定到JNDI容器中去,应用程序想要使用资源时,就直接从JNDI容器中获取相应的资源即可。

例如,对于上面的name="jdbc/datasource"数据源资源,在应用程序中可以用如下的代码去获取:

  • InitialContext initialContext = new InitialContext();
  • Context context = (Context) initialContext.lookup("java:comp/env");
  • DataSource dataSource = (DataSource) context.lookup("jdbc/datasource");

注:上述的配置方式下,需要将数据库的驱动Jar包拷贝到Tomcat安装目录的lib目录下。

接下来编写一个JNDIUtil的类封装对数据库连接的操作:

Java
  • package com.coderap.datasource;
  • import javax.naming.Context;
  • import javax.naming.InitialContext;
  • import javax.sql.DataSource;
  • import java.sql.Connection;
  • import java.sql.ResultSet;
  • import java.sql.SQLException;
  • import java.sql.Statement;
  • public class JNDIUtil {
  • private static DataSource dataSource = null;
  • static {
  • try {
  • // 初始化JNDI上下文
  • InitialContext initialContext = new InitialContext();
  • // 得到JNDI容器
  • Context context = (Context) initialContext.lookup("java:comp/env");
  • // 从容器中检索数据源
  • dataSource = (DataSource) context.lookup("jdbc/datasource");
  • } catch (Exception e) {
  • System.out.println("数据库连接失败");
  • throw new ExceptionInInitializerError(e);
  • }
  • }
  • public static Connection getConnection() throws SQLException {
  • return dataSource.getConnection();
  • }
  • public static void release(Connection connection, Statement statement, ResultSet resultSet) {
  • if (resultSet != null) {
  • try {
  • resultSet.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • if (statement != null) {
  • try {
  • statement.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • if (connection != null) {
  • try {
  • connection.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • }
  • }

因为需要使用到Tomcat容器,这里编写了一个简单的Servlet,关键代码如下:

Java
  • @Override
  • protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {
  • Connection connection = null;
  • Statement statement = null;
  • ResultSet resultSet = null;
  • StringBuilder stringBuilder = new StringBuilder();
  • try {
  • connection = JNDIUtil.getConnection();
  • String sql = "select name, gender, email from users;";
  • statement = connection.createStatement();
  • resultSet = statement.executeQuery(sql);
  • stringBuilder.append("<table border=1><tbody>");
  • stringBuilder.append("<tr>");
  • stringBuilder.append("<th>name</th>");
  • stringBuilder.append("<th>gender</th>");
  • stringBuilder.append("<th>email</th>");
  • stringBuilder.append("</tr>");
  • while (resultSet.next()) {
  • String name = resultSet.getString(1);
  • String gender = resultSet.getString(2);
  • String email = resultSet.getString(3);
  • // 拼装表格
  • stringBuilder.append("<tr>");
  • stringBuilder.append("<td>" + name + "</td>");
  • stringBuilder.append("<td>" + gender + "</td>");
  • stringBuilder.append("<td>" + email + "</td>");
  • stringBuilder.append("</tr>");
  • }
  • stringBuilder.append("</tbody></table>");
  • } catch (SQLException e) {
  • e.printStackTrace();
  • } finally {
  • JNDIUtil.release(connection, statement, resultSet);
  • }
  • // 打印
  • httpServletResponse.getOutputStream().print(stringBuilder.toString());
  • }

上面的代码使用JDNIUtil工具类进行了数据查询,并将数据拼接为一个表格,最后请求页面输出的HTML代码如下:

  • <table border=1>
  • <tbody>
  • <tr>
  • <th>name</th>
  • <th>gender</th>
  • <th>email</th>
  • </tr>
  • <tr>
  • <td>Tom</td>
  • <td>Male</td>
  • <td>Tom@coderap.com</td>
  • </tr>
  • <tr>
  • <td>Jack</td>
  • <td>Male</td>
  • <td>Jack@coderap.com</td>
  • </tr>
  • <tr>
  • <td>Marry</td>
  • <td>Female</td>
  • <td>Marry@coderap.com</td>
  • </tr>
  • <tr>
  • <td>John</td>
  • <td>Male</td>
  • <td>John@coderap.com</td>
  • </tr>
  • <tr>
  • <td>John</td>
  • <td>Male</td>
  • <td>John@coderap.com</td>
  • </tr>
  • </tbody>
  • </table>