文章目录
- 前言
- 2.7 JDBC单连接事务
- 2.7.1 事务的开启与提交
- 2.7.2 事务隔离级别
- 2.7.2.1 并发访问问题
- (1)脏读
- (2)不可重复读
- (3)幻读
- 2.7.2.2 事务隔离级别
- (1)TRANSACTION_NONE:不支持事务
- (2)TRANSACTION_READ_UNCOMMITTED:读未提交
- (3)TRANSACTION_READ_COMMITTED:读提交
- (4)TRANSACTION_REPEATABLE_READ:可重复读
- (5)TRANSACTION_SERIALIZABLE:串行化
- 2.7.3 事务中的保存点
- 2.8 小结
前言
DatabaseMetaData接口中有一个supportsTransactions()
方法,用于判断当前数据源是否支持事务。
事务用于提供数据完整性、正确的应用程序语义和并发访问的数据一致性。所有遵循JDBC规范的驱动程序都需要提供事务支持。
本节研究JDBC中的单连接事务。
2.7 JDBC单连接事务
2.7.1 事务的开启与提交
在JDBC API中,没有对应的方法显式地开启事务,因此何时开启一个新的事务是由JDBC驱动程序或数据库隐式决定的。
通常情况下,当SQL语句需要开启事务但目前还没有事务时,会自动地开启一个新的事务。
对于什么时候提交或回滚事务,Connection接口提供了setAutoCommit(boolean autoCommit)
、commit()
、rollback()
等方法来进行控制。
setAutoCommit(boolean autoCommit)
方法用于设置事务是否自动提交,默认情况下事务自动提交是开启的,每个SQL语句执行完毕后会自动地提交事务。- 如果使用
setAutoCommit(boolean autoCommit)
方法禁用了事务的自动提交,则需要显式地调用commit()
方法提交事务,或者调用rollback()
方法回滚事务。
禁用事务自动提交一般适用于需要将多个SQL语句作为一个事务提交或者事务由应用服务器管理的情况。
2.7.2 事务隔离级别
事务隔离级别用于表示事务中对数据的操作对其他事务的“可见性”,主要用于解决数据并发访问中的可能会出现的问题,且会直接影响到并发访问的效率。
Connection接口中提供了一个setTransactionIsolation(int level)
方法,用于设置当前驱动程序的事务隔离级别。
2.7.2.1 并发访问问题
(1)脏读
脏读是指在一个事务中读取到另一个事务中未提交的数据。例如,A事务修改了一条数据,但是未提交修改,此时A事务对数据的修改对其他事务是可见的,因此B事务中能够读取A事务未提交的修改。一旦A事务回滚,B事务中读取的就是不正确的数据。
下面用一个简单例子来解释。
在数据库中插入一条数据:
编写A事务和B事务的测试代码:
@Test
public void testA() {Connection connection = null;Statement statement = null;ResultSet resultSet = null;try {connection = DbUtils.getConnection();// 关闭事务自动提交connection.setAutoCommit(false);// 设置事务隔离级别为:读未提交(下文解释)connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);// 读取一条记录String sql = "select * from user where id = 1";statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);resultSet = statement.executeQuery(sql);resultSet.next();System.out.println("A事务读取的用户信息:" + new User(resultSet).toString());// 修改这条记录resultSet.updateString("name", "孙悟空");resultSet.updateRow();System.out.println("A事务修改后的用户信息:" + new User(resultSet).toString());// 此处打一个断点 ...// 回滚事务connection.rollback();} catch (Exception e) {e.printStackTrace();} finally {DbUtils.close(resultSet, statement, connection);}
}@Test
public void testB() {Connection connection = null;Statement statement = null;ResultSet resultSet = null;try {connection = DbUtils.getConnection();// 关闭事务自动提交connection.setAutoCommit(false);// 设置事务隔离级别为:读未提交connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);// 读取一条记录String sql = "select * from user where id = 1";statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);resultSet = statement.executeQuery(sql);resultSet.next();System.out.println("B事务读取的用户信息:" + new User(resultSet).toString());// 此处打一个断点 ...// 提交事务connection.commit();} catch (Exception e) {e.printStackTrace();} finally {DbUtils.close(resultSet, statement, connection);}
}
下面开始模拟脏读产生的过程:
- 以Debug方式执行A事务中的查询记录、修改记录操作,停到断点处,控制台打印修改前和修改后的用户数据;
- 以Debug方式执行B事务中的查询记录操作,停到断点处,控制台打印查询出来的用户数据,确实是A事务修改后但未提交的用户数据;
- 继续执行A事务,回滚修改操作,但B事务已经拿到了A事务修改后的用户数据,如果B事务对修改后的数据进一步处理,就是不符合要求的,这就产生脏读。
(2)不可重复读
不可重复读是指在同一个事务中,对于同一份数据的多次读取可能返回不同的结果。例如,A事务读取了一行数据,但此时B事务中修改了该行数据,A事务中再次读取该行数据将得到不同的结果。
下面用一个简单例子来解释。
在数据库中只有一条数据:
编写A事务和B事务的测试代码:
@Test
public void testA() {Connection connection = null;Statement statement = null;ResultSet resultSet = null;try {connection = DbUtils.getConnection();// 关闭事务自动提交connection.setAutoCommit(false);// 设置事务隔离级别为:读未提交connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);// 读取一条记录String sql = "select * from user where id = 1";statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);resultSet = statement.executeQuery(sql);resultSet.next();System.out.println("A事务首次读取的用户信息:" + new User(resultSet).toString());// 此处打一个断点 ...// 再次读取记录resultSet = statement.executeQuery(sql);resultSet.next();System.out.println("A事务再次读取的用户信息:" + new User(resultSet).toString());// 提交事务connection.commit();} catch (Exception e) {e.printStackTrace();} finally {DbUtils.close(resultSet, statement, connection);}
}@Test
public void testB() {Connection connection = null;Statement statement = null;ResultSet resultSet = null;try {connection = DbUtils.getConnection();// 关闭事务自动提交connection.setAutoCommit(false);// 设置事务隔离级别为:读未提交connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);// 读取一条记录String sql = "select * from user where id = 1";statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);resultSet = statement.executeQuery(sql);resultSet.next();System.out.println("B事务读取的用户信息:" + new User(resultSet).toString());// 修改这条记录resultSet.updateString("name", "孙悟空");resultSet.updateRow();System.out.println("B事务修改后的用户信息:" + new User(resultSet).toString());// 提交事务connection.commit();} catch (Exception e) {e.printStackTrace();} finally {DbUtils.close(resultSet, statement, connection);}
}
下面开始模拟不可重复读产生的过程:
- 以Debug方式执行A事务中的首次查询记录操作,停到断点处,控制台打印首次查询的用户数据;
- 直接执行B事务中的查询记录、修改记录操作,控制台打印处理结果,此时数据库记录也已被修改;
- 继续执行A事务,再次以相同的SQL查询用户数据,发现查询的数据是修改后的,这就产生了不可重复读的问题。
(3)幻读
幻读发生在多个事务同时读取和修改数据时。例如,当A事务正在读取一系列数据时,B事务可能会插入一些新的数据,然后提交事务。当A事务再次查询相同的记录集时,它可能会发现一些原本不存在的记录,这会导致数据的不一致性,给用户造成幻觉。
幻读和不可重复读的区别在于,不可重复读侧重于已存在数据的更改,而幻读侧重于新增数据的插入。
下面用一个简单例子来解释。
在数据库中只有一条数据:
编写A事务和B事务的测试代码:
@Test
public void testA() {Connection connection = null;Statement statement = null;ResultSet resultSet = null;try {connection = DbUtils.getConnection();// 关闭事务自动提交connection.setAutoCommit(false);// 设置事务隔离级别为:读未提交connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);// 查询记录集String sql = "select * from user";statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);resultSet = statement.executeQuery(sql);System.out.println("A事务首次读取的用户信息有:");while (resultSet.next()) {System.out.println(new User(resultSet).toString());}System.out.println("-----------------------");// 此处打一个断点 ...// 再次查询记录集resultSet = statement.executeQuery(sql);System.out.println("A事务再次读取的用户信息有:");while (resultSet.next()) {System.out.println(new User(resultSet).toString());}// 提交事务connection.commit();} catch (Exception e) {e.printStackTrace();} finally {// 关闭资源DbUtils.close(resultSet, statement, connection);}
}@Test
public void testB() {Connection connection = null;Statement statement = null;ResultSet resultSet = null;try {connection = DbUtils.getConnection();// 关闭事务自动提交connection.setAutoCommit(false);// 设置事务隔离级别为:读未提交connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);// 插入一条记录String sql = "INSERT INTO USER (NAME, age, phone, birthday) VALUES('user1', 18, '18705464523', '2000-02-21 10:24:30');";statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);statement.executeUpdate(sql);// 提交事务connection.commit();} catch (Exception e) {e.printStackTrace();} finally {// 关闭资源DbUtils.close(resultSet, statement, connection);}
}
下面开始模拟幻读产生的过程:
- 以Debug方式执行A事务中的首次查询记录集操作,停到断点处,控制台打印首次查询的用户数据;
- 直接执行B事务中的插入记录操作,控制台打印处理结果,此时数据库记录也增加了一条;
- 继续执行A事务,再次以相同的SQL查询用户数据集,发现查询的数据还包括B事务新增的,这就产生了幻读的问题。
2.7.2.2 事务隔离级别
JDBC遵循SQL:2003规范,定义了5种事务隔离级别:
(1)TRANSACTION_NONE:不支持事务
(2)TRANSACTION_READ_UNCOMMITTED:读未提交
这种事务隔离级别允许某一事务读取另一事务未提交更改的数据,这意味着可能会出现脏读、不可重复读、幻读现象。
【2.7.2.1 并发访问问题】的三个案例均将事务隔离级别设置为“读未提交”,经过实际测试,确实会发生脏读、不可重复读、幻读现象。
(3)TRANSACTION_READ_COMMITTED:读提交
这种事务隔离级别表示在某一事务中进行任何数据的更改,在提交之前对其他事务都是不可见的,这样可以防止脏读,但不能解决不可重复读、幻读问题。
这是MySQL驱动程序默认的事务隔离级别。
下面继续使用【2.7.2.1 并发访问问题】中的三个案例进行测试。
首先手动将事务隔离级别设置为TRANSACTION_READ_COMMITTED读提交,其余代码保持不变。
// 设置事务隔离级别为:读提交
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
- 测试脏读问题:已解决
- 以Debug方式执行A事务中的查询记录、修改记录操作,停到断点处,控制台打印修改前和修改后的用户数据;
- 以Debug方式执行B事务中的查询记录操作,停到断点处,控制台打印查询出来的用户数据,确实是原来的数据,而不是A事务修改后但未提交的用户数据;
- 继续执行A事务,回滚修改操作,但B事务拿到的是A事务修改前的用户数据,符合要求,脏读问题已被解决。
-
测试不可重复读问题:未解决
-
测试幻读问题:未解决
(4)TRANSACTION_REPEATABLE_READ:可重复读
这种事务隔离级别表示在某一事务中对同一数据进行多次读取时,可以得到相同的结果,并且其他事务插入数据的操作对该事务不可见,这样可以防止脏读、不可重复读,但不能解决幻读问题;
下面继续使用【2.7.2.1 并发访问问题】中的三个案例进行测试。
首先手动将事务隔离级别设置为TRANSACTION_REPEATABLE_READ可重复读,其余代码保持不变。
// 设置事务隔离级别为:可重复读
connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
-
测试脏读问题:已解决
-
测试不可重复读问题:已解决
- 以Debug方式执行A事务中的首次查询记录操作,停到断点处,控制台打印首次查询的用户数据;
- 直接执行B事务中的查询记录、修改记录操作,控制台打印处理结果,此时记录已被修改;
- 继续执行A事务,再次以相同的SQL查询用户数据,发现查询的数据是修改前的,这就解决了不可重复读的问题。
- 测试幻读问题:未解决
(5)TRANSACTION_SERIALIZABLE:串行化
这种事务隔离级别是最高的事务隔离级别,保证数据的一致性和完整性,可以防止脏读、不可重复读,、幻读问题,但是并发性较差。
下面继续使用【2.7.2.1 并发访问问题】中的三个案例进行测试。
首先手动将事务隔离级别设置为TRANSACTION_SERIALIZABLE串行化,其余代码保持不变。
// 设置事务隔离级别为:串行化
connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
-
测试脏读问题:已解决
-
测试不可重复读问题:已解决
-
测试幻读问题:已解决
- 以Debug方式执行A事务中的首次查询记录集操作,停到断点处,控制台打印首次查询的用户数据;
- 直接执行B事务中的插入记录操作,发现报错,无法打开新的事务。
- 继续执行A事务,再次以相同的SQL查询用户数据集,发现查询的数据是一样的,这就解决了幻读的问题。
- A事务提交后,再次执行B事务,发现可以成功执行,说明串行化等级下一次只能打开一个事务。
2.7.3 事务中的保存点
保存点是指通过事务中标记的一个中间点来对事务进行更细粒度的控制,一旦设置保存点,事务就可以归滚到保存点,而不影响保存点之前的操作。
DatabaseMetaData接口提供了supportsSavepoints()
方法用于判断JDBC驱动程序是否支持保存点。
Connection接口提供了``setSavepoint()```方法用于在当前事务中设置保存点。如果该方法在事务外中调用,则会在该方法调用处开启一个新的事务。
该方法的返回值是一个Savepoint对象,该对象可作为COnnection接口的rollback()
方法的参数,用于回滚到对应的保存点。
示例代码如下:
// ......
// 关闭事务自动提交
connection.setAutoCommit(false);
// 读取一条记录
String sql = "select * from user where id = 1";
statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
resultSet = statement.executeQuery(sql);
resultSet.next();
System.out.println("第一次读取的用户信息:" + new User(resultSet).toString());
// 第一次修改这条记录
resultSet.updateString("name", "孙悟空-修改1");
resultSet.updateRow();
System.out.println("第一次修改后的用户信息:" + new User(resultSet).toString());
// 设置保存点
Savepoint savepoint = connection.setSavepoint();
// 第二次修改这条记录
resultSet.updateString("name", "孙悟空-修改2");
resultSet.updateRow();
System.out.println("第二次修改后的用户信息:" + new User(resultSet).toString());
// 回滚到保存点
connection.rollback(savepoint);
// 再次读取这条记录
// 读取一条记录
resultSet = statement.executeQuery(sql);
resultSet.next();
System.out.println("第二次读取的用户信息:" + new User(resultSet).toString());
// 提交事务
connection.commit();
// ......
控制台打印执行结果:
第一次读取的用户信息:User{id=1, name='黑风怪', age=18, phone='18705464523', birthday=2000-02-21}
第一次修改后的用户信息:User{id=1, name='孙悟空-修改1', age=18, phone='18705464523', birthday=2000-02-21}
第二次修改后的用户信息:User{id=1, name='孙悟空-修改2', age=18, phone='18705464523', birthday=2000-02-21}
第二次读取的用户信息:User{id=1, name='孙悟空-修改1', age=18, phone='18705464523', birthday=2000-02-21}
在示例代码中,依次进行读取记录→第一次修改→设置保存点→第二次修改→回滚到保存点→再次读取记录→提交事务,第二次读取的结果恰好就是第一次修改后的结果,说明确实回滚到了保存点的位置。
保存点创建后,可以被手动释放。Connection接口提供了releaseSavepoint()
方法,接收一个Savepoint对象为参数,用于释放保存点。保存点被释放后,如果试图通过rollback()
方法回滚到保存点,则会抛出SQLException异常。
事务中创建的保存点在事务提交或回滚之后会自动释放,事务回滚到某一保存点之后,该保存点之后的保存点将会自动释放。
2.8 小结
第2章到此就梳理完毕了,本章的主题是:JDBC规范。回顾一下本章的梳理的内容:
(二)JDBC API简介
(三)Connection
(四)Statement
(五)ResultSet
(六)DatabaseMetaData
(七)JDBC单连接事务
更多内容请查阅分类专栏:MyBatis3源码深度解析
第3章主要梳理:MyBatis常用工具类。主要内容包括:
- 使用SQL类生成语句;
- 使用ScriptRunner执行脚本;
- 使用SqlRunner操作数据库;
- MetaObject详解;
- MetaClass详解;
- ObjectFactory详解;
- ProxyFactory详解。