JDBC 事务管理
数据库事务是由若干个 SQL 语句构成的一个操作序列。数据库系统保证在一个事务中的所有 SQL 语句要么全部执行成功,要么全部不执行,即数据库事务具有 ACID 特性:
- Atomicity: 原子性
- Consistency: 一致性
- Isolation: 隔离性
- Durability: 持久性
数据库事务可以并发执行,而数据库系统从效率考虑,对事务定义了不同的隔离级别。SQL 标准定义了 4 种隔离级别,分别对应可能出现的数据不一致的情况:
Isolation Level | 脏读(Dirty Read) | 不可重复读(Non Repeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
Read Uncommitted | Yes | Yes | Yes |
Read Committed | - | Yes | Yes |
Repeatable Read | - | - | Yes |
Serializable | - | - | - |
数据库事务的必要性
数据库事务的一个经典案例就是银行转账操作。现假定小明要给小红转账 100 元,两人的银行账户(accountId,假定为 Primary Key)分别 123,456。那么银行那边应该执行的 SQL 语句如下:
-- 小明账户的操作
UPDATE accounts SET balance = balance - 100 WHERE id = 123 AND balance >= 100;
-- 小红账户的操作
UPDATE accounts SET balance = balance + 100 WHERE id = 456;
假定银行后台在处理完小明账户的操作后,突然发生了不可抗拒的因素使得小红账户的操作无法执行,那么系统上的总额就会凭空少了 100 元。而有了事务后,这两个操作要么执行成功,要么都不执行,保证了系统上的总额不变。
数据库事务不是本文的重点,本文的重点是如何通过 JDBC 进行事务管理。
JDBC 的事务管理
事务的本质就是一些列的 SQL 操作序列,因此要在 JDBC 中执行事务,本质上就是如何把多条 SQL 语句包裹在一个数据库事务中执行。JDBC 的事务处理相对固定,如下:
Connection conn = null;
try {
conn = JDBCUtils.getMyConnection();
// 关闭自动提交
conn.setAutoCommit(false);
// 执行多条SQL语句
insert();
update();
delete();
// 提交事务
conn.commit();
}catch (Exception e){
// 执行过程中出错,则回滚事务
conn.rollback();
}finally{
conn.setAutoCommit(true);
}
其中,开启事务的关键代码是conn.setAutoCommit(false),表示关闭自动提交。提交事务的代码在执行完指定的若干条 SQL 语句后,调用conn.commit()。要注意事务不是总能成功,如果事务提交失败,会抛出 SQL 异常(也可能在执行 SQL 语句的时候就抛出了),此时我们必须捕获并调用conn.rollback()回滚事务。最后,在 finally 中通过conn.setAutoCommit(true)把Connection对象的状态恢复到初始值。
下面的代码就是事务处理的一个例子:
@Test
public void transactionTest(){
/**
* 向order表中添加一条order_name为JDBC的数据
*/
Connection conn = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try{
conn = JDBCUtils.getJDBCConnection();
// 事务开始
conn.setAutoCommit(false);
String sql = "INSERT INTO `order` VALUES (4,'JDBC');";
Statement statement = conn.createStatement();
int i = statement.executeUpdate(sql);
// 事务提交
conn.commit();
System.out.println("插入成功");
}catch (Exception e){
try {
// 事务回滚
conn.rollback();
// 恢复
conn.setAutoCommit(true);
}catch (Exception setE){
setE.printStackTrace();
}
e.printStackTrace();
}finally {
try {
JDBCUtils.closeResource(conn,preparedStatement,resultSet);
}catch (Exception e){
e.printStackTrace();
}
}
}
代码执行后,我们可以正常的插入一条数据,现在我们使用下一段代码来演示当我们可以成功插入语句时,在事务提交前出现了异常后的事务回滚:
@Test
public void transactionTest(){
/**
* 向order表中添加一条order_name为JDBC的数据
*/
Connection conn = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try{
conn = JDBCUtils.getJDBCConnection();
// 事务开始
conn.setAutoCommit(false);
String sql = "INSERT INTO `order` VALUES (6,'Python');";
Statement statement = conn.createStatement();
int i = statement.executeUpdate(sql);
System.out.println("成功修改了" + i + "条记录");
// 当成功修改记录时,主动抛出一个异常,i的值表示的是修改了i条记录
if(i > 0){
System.out.println("出现异常!!!");
throw new Exception();
}
// 事务提交
conn.commit();
System.out.println("插入成功");
}catch (Exception e){
try {
// 事务回滚
conn.rollback();
conn.setAutoCommit(true);
System.out.println("更新失败!");
}catch (Exception setE){
setE.printStackTrace();
}
e.printStackTrace();
}finally {
try {
JDBCUtils.closeResource(conn,preparedStatement,resultSet);
}catch (Exception e){
e.printStackTrace();
}
}
}
控制台信息:
成功修改了1条记录
出现异常!!!
更新失败!
order 表信息:
可以发现 尽管插入操作执行成功,但因为后序出现了异常,导致我们的插入操作失效。
还原点回滚技术
大部分现代的数据库管理系统的环境都支持设定还原点,例如 Oracle 的 PL/SQL。当你在事务中设置一个还原点来定义一个逻辑回滚点后,如果在一个还原点之后发生错误,那么可以使用 rollback 方法来撤消所有的修改或在该还原点之后所做的修改。
Connection 对象有两个新的方法来管理还原点:
- setSavepoint(String savepointName):定义了一个新的还原点,返回一个 Savepoint 对象。
- releaseSavepoint(Savepoint savepointName):删除一个还原点。它需要一个作为参数的 Savepoint 对象,这个对象通常是由 setSavepoint()方法生成的一个还原点。
还原点的使用其实与事务回滚的使用类似,如下:
try{
/
conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
//设置一个名为“Savepoint1”还原点
Savepoint savepoint1 = conn.setSavepoint("Savepoint1");
// 一条正确的SQL语句
String SQL = "INSERT INTO Employees " +
"VALUES (106, 20, 'Rita', 'Tez')";
stmt.executeUpdate(SQL);
//错误的SQL语句
String SQL = "INSERTED IN Employees " +
"VALUES (107, 22, 'Sita', 'Tez')";
stmt.executeUpdate(SQL);
// 提交
conn.commit();
}catch(SQLException se){
// 回滚到还原点Savepoint1
conn.rollback(savepoint1);
}