引言
数据库事务的概念和基础,总结在《MySQL 基础 ————事务与隔离级别总结》。
本篇博客通过“JDBC + 纯编码”方式实现事务控制,完成一个 A 给 B 转账的小功能,在进一步熟练JDBC的编程流程的同时,重点关注 Java 语言如何操作和控制事务。
一、事务自动提交的三种情况
事务默认自动提交的三种情况:
1、DDL操作执行后,会自动提交事务,SET autocommit=false 对该类语句不管用。不过,在DDL语言上一般不考虑事务。
2、DML(增、删、改)默认情况下,执行后会自动提交,不过可以通过 set autocommit=false;取消 DML的自动提交。
3、数据库连接关闭时,默认会自动提交事务。因此,如果两次DML操作在同一事务中,则事务中间不可以关闭连接。
一般的事务控制只需要考虑后两种情况,我们需要做的就是在当前连接中,取消自动提交,在整体逻辑完成后,提交事务或异常回滚,最后恢复自动提交。
二、逻辑介绍、库表及实体类
如下图,两条用户信息,完成 Morty 转账 100 块给 Jack 的转账功能。涉及两步操作:1、Morty 减少 100 2、Jack 增加 100
User 实体类:
public class User {
private Integer id;
private String name;
private Date birthDay;
private Integer balance;
public User() {
}
// ... getter setter...
}
三、数据库连接获取与资源关闭工具类
该工具类使用 JDBC API 完成对数据库连接的获取,及关闭操作,代码逻辑与注意事项参考《JDBC——概述与JDBC的使用》
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;
public class JDBCUtils {
public static Connection getConnection() {
Connection connection = null;
try {
// 默认的识别路径就是 src 目录下
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
Properties props = new Properties();
props.load(is);
String url = props.getProperty("url");
String username = props.getProperty("username");
String password = props.getProperty("password");
String driverName = props.getProperty("driverName");
// 加载驱动类
Class.forName(driverName);
// 获取连接
connection = DriverManager.getConnection(url, username, password);
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
public static void closeResource(Connection conn, Statement statement, ResultSet rs) {
try {
if (rs != null) {
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (statement != null) {
statement.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
四、转账操作逻辑实现
在《JDBC——概述与JDBC的使用》中,展示了简单的入库操作,这里可以将其优化为通用的更新操作:
/**
* 数据库更新操作
*/
private static int update(Connection connection, String sql, Object... args) throws Exception {
PreparedStatement ps = null;
try {
ps = connection.prepareStatement(sql);
// 填充属性
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
int rows = ps.executeUpdate();
return rows;
} finally {
JDBCUtils.closeResource(null, ps, null);
}
}
上述代码中,方法参数接收一个连接Connection 对象,这是因为,在执行完更新操作后,不可以直接关闭连接,否则会自动提交事务,在最后的finally块中,也要注意,不要将 Connection对象传入关闭资源的方法中,以免误关Connection。并且如果发生异常,直接抛出即可,这是因为如果在此过程中发生异常,既可以将情况反映给调用方,也可以在 finally 块中关闭必要的资源。
/**
* 转账(事务控制)
*/
private static void transferAccountsTx() {
Connection connection = null;
try {
connection = JDBCUtils.getConnection();
// 关闭自动提交
connection.setAutoCommit(false);
// 转账金额
Integer amount = 100;
// 减少金额
String sql1 = "UPDATE user SET balance = balance - ? WHERE name = ?";
update(connection, sql1, amount, "Morty");
// 模拟异常
System.out.println(10 / 0);
// 增加金额
String sql2 = "UPDATE user SET balance = balance + ? WHERE name = ?";
update(connection, sql2, amount, "Jack");
// 执行提交
connection.commit();
System.out.println("转账成功!");
} catch (Exception e) {
e.printStackTrace();
try {
// 异常回滚
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
} finally {
try {
// 恢复事务自动提交,针对连接池的情况需要额外注意该操作
connection.setAutoCommit(true);
} catch (SQLException e) {
e.printStackTrace();
}
JDBCUtils.closeResource(connection, null, null);
}
}
上述代码中,通过 .setAutoCommit(false) 关闭自动提交,开启事务, 并通过 10 ÷ 0 的操作,模拟了一个转账过程中的异常,捕获异常时,我们要执行回滚操作。如果逻辑可以正常执行,就使用 commit(); 完成提交操作。最后在整体逻辑后面,使用 finally 块恢复事务自动提交(实际上,如果是关闭Connection,并不需要恢复自动提交,但如果是连接池的情况,重复利用连接就需要这么做),并关闭 Connection 。
五、通过代码设置数据库事务隔离级别
一般情况,不需要通过代码层面更改数据库事务,不过可以了解,事务隔离级别非常简单,具体概念可以参考《MySQL 基础 ————事务与隔离级别总结》。
使用 Connection 对象可以设置和获取隔离级别:
// 获取事务隔离级别
connection.getTransactionIsolation();
// 设置事务隔离级别
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
Connection 中定义了 4 种事务隔离级别的常量,从低到高分别是:
int TRANSACTION_READ_UNCOMMITTED = 1;
int TRANSACTION_READ_COMMITTED = 2;
int TRANSACTION_REPEATABLE_READ = 4;
int TRANSACTION_SERIALIZABLE = 8;
总结
需要考虑事务自动提交的两种情况:
1、DML语句会自动提交事务,需要 set autocommit=false 关闭自动提交,对应 JDBC的 API 就是
connection.setAutoCommit(false);
2、关闭连接会自动提交事务,一定要在完成全部事务操作后才可以关闭 Connection 。代码中一定要注意 connection.close() 的位置!
三个事务控制的新方法:
// 关闭自动提交
connection.setAutoCommit(false);
...
// 手动提交事务
connection.commit();
...
// 异常时回滚事务
connection.rollback();
另外,每条SQL执行时也可能发生异常导致操作失败,一定要将异常抛给包含事务的调用方,以免调用方以为SQL执行成功,从而破坏了事务的一致性。
具体点说,比如上述案例中,Morty 账户减100 时在 update() 执行过程中发生了未知异常,但是如果update(..) 方法内部捕获了异常,方法正常退出,而没有抛给 transferAccountsTx(),那么transferAccountsTx() 就不会进入 catch() 块并回滚事务。这也是一个需要注意的点,即,我们要保证事务中的每一处发生异常时,都可以成功回滚。