0%

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) 回滚到指定保存点,保存点之后的操作被撤销,之前的操作仍可提交。

基本事务操作(提交与回滚)

示例:转账事务(确保原子性)
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
/**
* 转账事务:从 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):部分回滚

当事务包含多个步骤时,可通过保存点回滚到中间状态,而不影响保存点之前的操作。

示例:多步骤事务与保存点
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);

// 步骤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) 设置隔离级别,参数为以下常量:

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()) {
// 设置隔离级别为 READ COMMITTED
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
-- 查看当前会话隔离级别(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;

事务操作的最佳实践

  1. 最小化事务范围:事务包含的操作应尽可能少,减少锁持有时间,提升并发性能。
  2. 显式关闭自动提交:手动事务必须调用 setAutoCommit(false),避免默认自动提交导致的事务拆分。
  3. 异常必回滚:在 catch 块中强制回滚,确保异常时数据一致性。
  4. 合理选择隔离级别:
    • 高并发场景(如电商):优先 READ COMMITTED,平衡一致性与性能。
    • 强一致性场景(如金融):使用 REPEATABLE READSERIALIZABLE
  5. 避免长事务:长事务可能导致锁竞争加剧,甚至死锁(如两个事务互相等待对方释放资源)

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10