JDBC操作事务
JDBC 事务操作详解:ACID 与隔离级别实践
事务是数据库操作的基本单元,确保多个数据库操作要么全部成功,要么全部失败,从而保证数据的一致性。JDBC 提供了完整的事务控制 API,支持事务的提交、回滚及隔离级别的设置。本文将从事务的核心特性(ACID)出发,详解 JDBC 事务操作的实现、隔离级别及并发问题的解决。
事务的核心特性(ACID)
事务必须满足四大特性,即 ACID:
| 特性 | 定义 | 示例场景 |
|---|---|---|
| 原子性(Atomicity) | 事务是不可分割的最小单位,操作要么全执行,要么全不执行。 | 转账时,“扣款” 和 “收款” 必须同时成功,若一方失败则全部回滚。 |
| 一致性(Consistency) | 事务执行前后,数据库从一个一致性状态切换到另一个一致性状态。 | 转账前 A 有 100 元、B 有 200 元,转账后 A+B 仍为 300 元(总额不变)。 |
| 隔离性(Isolation) | 多个事务并发执行时,彼此互不干扰,结果等同于串行执行。 | 事务 T1 读取数据时,事务 T2 的未提交修改不会影响 T1 的结果。 |
| 持久性(Durability) | 事务提交后,对数据的修改永久生效,即使系统崩溃也不会丢失。 | 提交转账后,即使数据库重启,A 和 B 的余额仍保持更新后的值。 |
JDBC 事务操作的核心 API
JDBC 通过 Connection 接口控制事务,默认情况下,每条 SQL 语句都是一个独立事务(自动提交)。如需手动管理事务,需通过以下方法:
| 方法 | 功能描述 |
|---|---|
conn.setAutoCommit(false) |
关闭自动提交,开启手动事务模式(后续 SQL 需显式提交或回滚)。 |
conn.commit() |
提交事务,将所有未提交的修改永久写入数据库。 |
conn.rollback() |
回滚事务,撤销所有未提交的修改。 |
conn.setSavepoint() |
设置事务保存点,允许部分回滚(回滚到保存点,而非整个事务)。 |
conn.rollback(Savepoint) |
回滚到指定保存点,保存点之后的操作被撤销,之前的操作仍可提交。 |
基本事务操作(提交与回滚)
示例:转账事务(确保原子性)
/**
* 转账事务:从 fromId 扣钱,向 toId 加钱,确保原子性
*/
public static boolean transfer(int fromId, int toId, double amount) {
Connection conn = null;
PreparedStatement pstmt1 = null; // 扣钱 SQL
PreparedStatement pstmt2 = null; // 加钱 SQL
try {
conn = JDBCUtil.getConnection(); // 获取连接(自定义工具类)
// 1. 关闭自动提交(开启手动事务)
conn.setAutoCommit(false);
// 2. 执行扣钱操作
String sql1 = "UPDATE account SET balance = balance - ? WHERE id = ?";
pstmt1 = conn.prepareStatement(sql1);
pstmt1.setDouble(1, amount);
pstmt1.setInt(2, fromId);
int rows1 = pstmt1.executeUpdate();
// 3. 执行加钱操作
String sql2 = "UPDATE account SET balance = balance + ? WHERE id = ?";
pstmt2 = conn.prepareStatement(sql2);
pstmt2.setDouble(1, amount);
pstmt2.setInt(2, toId);
int rows2 = pstmt2.executeUpdate();
// 4. 验证操作是否成功(如扣钱和加钱都影响了一行数据)
if (rows1 == 1 && rows2 == 1) {
conn.commit(); // 提交事务
System.out.println("转账成功");
return true;
} else {
conn.rollback(); // 操作失败,回滚
System.out.println("转账失败,已回滚");
return false;
}
} catch (SQLException e) {
// 发生异常时回滚
if (conn != null) {
try {
conn.rollback();
System.out.println("异常导致回滚");
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
return false;
} finally {
// 关闭资源(恢复自动提交,避免影响其他操作)
if (conn != null) {
try {
conn.setAutoCommit(true); // 恢复默认自动提交
} catch (SQLException e) {
e.printStackTrace();
}
}
JDBCUtil.close(conn, pstmt1, null);
JDBCUtil.close(null, pstmt2, null);
}
}
关键逻辑:
- 关闭自动提交后,所有 SQL 操作处于 “未提交” 状态,直到调用
commit()。 - 若任一操作失败(如扣钱成功但加钱失败)或发生异常,调用
rollback()撤销所有修改,保证原子性。
保存点(Savepoint):部分回滚
当事务包含多个步骤时,可通过保存点回滚到中间状态,而不影响保存点之前的操作。
示例:多步骤事务与保存点
/**
* 多步骤事务:使用保存点实现部分回滚
*/
public static void multiStepTransaction() {
Connection conn = null;
PreparedStatement pstmt = null;
Savepoint savepoint = null; // 保存点
try {
conn = JDBCUtil.getConnection();
conn.setAutoCommit(false);
// 步骤1:插入用户 A
pstmt = conn.prepareStatement("INSERT INTO user (name) VALUES (?)");
pstmt.setString(1, "UserA");
pstmt.executeUpdate();
System.out.println("步骤1:插入 UserA 完成");
// 设置保存点(后续操作可回滚到此处)
savepoint = conn.setSavepoint("afterUserA");
// 步骤2:插入用户 B(假设此处可能失败)
pstmt.setString(1, "UserB");
pstmt.executeUpdate();
System.out.println("步骤2:插入 UserB 完成");
// 假设步骤2后发生错误(模拟异常)
throw new SQLException("模拟步骤2后异常");
// 若未发生异常,提交事务
// conn.commit();
} catch (SQLException e) {
if (conn != null) {
try {
if (savepoint != null) {
// 回滚到保存点(仅撤销步骤2,保留步骤1)
conn.rollback(savepoint);
conn.commit(); // 提交保存点之前的操作(步骤1)
System.out.println("回滚到保存点,步骤1已提交,步骤2已撤销");
} else {
// 无保存点,回滚整个事务
conn.rollback();
System.out.println("回滚整个事务");
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
if (conn != null) {
try {
conn.setAutoCommit(true);
} catch (SQLException e) {
e.printStackTrace();
}
}
JDBCUtil.close(conn, pstmt, null);
}
}
执行结果:
- 步骤 1(插入 UserA)成功,设置保存点。
- 步骤 2(插入 UserB)后模拟异常,回滚到保存点,最终仅 UserA 被提交。
事务隔离级别:解决并发问题
多个事务并发执行时,若隔离性不足,会导致三类典型问题:
并发事务的三大问题
| 问题 | 定义 | 示例 |
|---|---|---|
| 脏读(Dirty Read) | 事务 T1 读取了 T2 未提交的修改,若 T2 回滚,T1 读取的数据无效。 | T2 转账给 T1 100 元(未提交),T1 读取到余额增加,随后 T2 回滚,T1 的读取结果为 “脏数据”。 |
| 不可重复读(Non-Repeatable Read) | T1 多次读取同一数据,期间 T2 修改并提交该数据,导致 T1 前后读取结果不一致。 | T1 首次读取 A 的余额为 100 元,T2 将 A 改为 200 元并提交,T1 再次读取时变为 200 元。 |
| 幻读(Phantom Read) | T1 按条件查询数据,期间 T2 插入符合条件的新数据并提交,T1 再次查询时多出 “幻影” 行。 | T1 查询所有余额 < 100 的用户(共 2 人),T2 插入 1 个新用户(余额 50)并提交,T1 再次查询时变为 3 人。 |
数据库的四大隔离级别
为解决上述问题,数据库定义了四种隔离级别(从低到高),级别越高,一致性越好,但并发性能越差:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 描述 |
|---|---|---|---|---|
| READ UNCOMMITTED(读未提交) | 可能 | 可能 | 可能 | 允许读取未提交的修改,性能最高,但一致性最差。 |
| READ COMMITTED(读已提交) | 不可能 | 可能 | 可能 | 仅允许读取已提交的修改,避免脏读(Oracle 默认级别)。 |
| REPEATABLE READ(可重复读) | 不可能 | 不可能 | 可能 | 确保同一事务内多次读取结果一致,避免脏读和不可重复读(MySQL 默认级别)。 |
| SERIALIZABLE(串行化) | 不可能 | 不可能 | 不可能 | 事务串行执行,避免所有并发问题,但性能最差。 |
JDBC 设置隔离级别
通过 Connection.setTransactionIsolation(int level) 设置隔离级别,参数为以下常量:
// 对应四大隔离级别
conn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
示例:设置隔离级别为 “读已提交”
public static void setIsolationLevel() {
try (Connection conn = JDBCUtil.getConnection()) {
// 设置隔离级别为 READ COMMITTED
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
// 查看当前隔离级别(返回对应常量值)
int level = conn.getTransactionIsolation();
System.out.println("当前隔离级别:" + level);
} catch (SQLException e) {
e.printStackTrace();
}
}
MySQL 中配置隔离级别
MySQL 可通过 SQL 语句查看或修改隔离级别(会话级或全局级):
-- 查看当前会话隔离级别(MySQL 5.7+ 用 transaction_isolation)
SELECT @@tx_isolation;
-- 查看全局隔离级别
SELECT @@global.tx_isolation;
-- 设置当前会话隔离级别为 READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置全局隔离级别为 REPEATABLE READ
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
事务操作的最佳实践
- 最小化事务范围:事务包含的操作应尽可能少,减少锁持有时间,提升并发性能。
- 显式关闭自动提交:手动事务必须调用
setAutoCommit(false),避免默认自动提交导致的事务拆分。 - 异常必回滚:在
catch块中强制回滚,确保异常时数据一致性。 - 合理选择隔离级别:
- 高并发场景(如电商):优先
READ COMMITTED,平衡一致性与性能。 - 强一致性场景(如金融):使用
REPEATABLE READ或SERIALIZABLE。
- 高并发场景(如电商):优先
- 避免长事务:长事务可能导致锁竞争加剧,甚至死锁(如两个事务互相等待对方释放资源)