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) |
回滚到指定保存点,保存点之后的操作被撤销,之前的操作仍可提交。 |
基本事务操作(提交与回滚)
示例:转账事务(确保原子性)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
|
public static boolean transfer(int fromId, int toId, double amount) { Connection conn = null; PreparedStatement pstmt1 = null; PreparedStatement pstmt2 = null; try { conn = JDBCUtil.getConnection(); conn.setAutoCommit(false);
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();
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();
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):部分回滚
当事务包含多个步骤时,可通过保存点回滚到中间状态,而不影响保存点之前的操作。
示例:多步骤事务与保存点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
|
public static void multiStepTransaction() { Connection conn = null; PreparedStatement pstmt = null; Savepoint savepoint = null; try { conn = JDBCUtil.getConnection(); conn.setAutoCommit(false);
pstmt = conn.prepareStatement("INSERT INTO user (name) VALUES (?)"); pstmt.setString(1, "UserA"); pstmt.executeUpdate(); System.out.println("步骤1:插入 UserA 完成");
savepoint = conn.setSavepoint("afterUserA");
pstmt.setString(1, "UserB"); pstmt.executeUpdate(); System.out.println("步骤2:插入 UserB 完成");
throw new SQLException("模拟步骤2后异常");
} catch (SQLException e) { if (conn != null) { try { if (savepoint != null) { conn.rollback(savepoint); conn.commit(); 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) 设置隔离级别,参数为以下常量:
1 2 3 4 5
| conn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
|
示例:设置隔离级别为 “读已提交”
1 2 3 4 5 6 7 8 9 10 11
| public static void setIsolationLevel() { try (Connection conn = JDBCUtil.getConnection()) { conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); int level = conn.getTransactionIsolation(); System.out.println("当前隔离级别:" + level); } catch (SQLException e) { e.printStackTrace(); } }
|
MySQL 中配置隔离级别
MySQL 可通过 SQL 语句查看或修改隔离级别(会话级或全局级):
1 2 3 4 5 6 7 8 9 10 11
| SELECT @@tx_isolation;
SELECT @@global.tx_isolation;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
事务操作的最佳实践
- 最小化事务范围:事务包含的操作应尽可能少,减少锁持有时间,提升并发性能。
- 显式关闭自动提交:手动事务必须调用
setAutoCommit(false),避免默认自动提交导致的事务拆分。
- 异常必回滚:在
catch 块中强制回滚,确保异常时数据一致性。
- 合理选择隔离级别:
- 高并发场景(如电商):优先
READ COMMITTED,平衡一致性与性能。
- 强一致性场景(如金融):使用
REPEATABLE READ 或 SERIALIZABLE。
- 避免长事务:长事务可能导致锁竞争加剧,甚至死锁(如两个事务互相等待对方释放资源)
v1.3.10