JDBC 事务和批量
# 20.JDBC 事务和批量
讲讲 JDBC 中如何使用事务
# 什么是事务
数据库事务(Transaction)是由若干个 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,两人在数据库中的记录主键分别是 123
和 456
,那么用两条 SQL 语句操作如下:
UPDATE accounts SET balance = balance - 100 WHERE id = 123 AND balance >= 100;
UPDATE accounts SET balance = balance + 100 WHERE id = 456;
2
这两条语句必须以事务方式执行才能保证业务的正确性,因为一旦第一条 SQL 执行成功而第二条 SQL 失败的话,系统的钱就会凭空减少 100,而有了事务,要么这笔转账成功,要么转账失败,双方账户的钱都不变。
这里我们不讨论详细的 SQL 事务,那是数据库教程里的内容,本文是讲 JDBC 的。
# JDBC 中的事务
要在 JDBC 中执行事务,本质上就是如何把多条 SQL 包裹在一个数据库事务中执行。我们来看 JDBC 的事务代码:
Connection conn = openConnection();
try {
// 关闭自动提交:
conn.setAutoCommit(false);
// 执行多条 SQL 语句:
insert();
update();
delete();
// 提交事务:
conn.commit();
} catch (SQLException e) {
// 回滚事务:
conn.rollback();
} finally {
conn.setAutoCommit(true);
conn.close();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
代码说明:
- 开启事务的关键代码是
conn.setAutoCommit(false)
,表示关闭自动提交。 - 提交事务的代码在执行完指定的若干条 SQL 语句后,调用
conn.commit()
。 - 要注意事务不是总能成功,如果事务提交失败,会抛出 SQL 异常(也可能在执行 SQL 语句的时候就抛出了),此时我们必须捕获并调用
conn.rollback()
回滚事务。 - 最后,在
finally
中通过conn.setAutoCommit(true)
把Connection
对象的状态恢复到初始值。
实际上,默认情况下,我们获取到 Connection
连接后,总是处于“自动提交”模式,也就是每执行一条 SQL 都是作为事务自动执行的,这也是为什么前面几节我们的更新操作总能成功的原因:因为默认有这种“隐式事务”。
只要关闭了 Connection
的 autoCommit
,那么就可以在一个事务中执行多条语句,事务以 commit()
方法结束。
# 设定事务的隔离级别
如果要设定事务的隔离级别,可以使用如下代码:
// 设定隔离级别为 READ COMMITTED:
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
2
如果没有调用上述方法,那么会使用数据库的默认隔离级别。MySQL 默认隔离级别是 REPEATABLE_READ
。
# JDBC Batch
使用 JDBC 操作数据库的时候,经常会执行一些批量操作:
例如,一次性给会员增加可用优惠券若干,我们可以执行以下 SQL 代码:
INSERT INTO coupons (user_id, type, expires) VALUES (123, 'DISCOUNT', '2030-12-31');
INSERT INTO coupons (user_id, type, expires) VALUES (234, 'DISCOUNT', '2030-12-31');
INSERT INTO coupons (user_id, type, expires) VALUES (345, 'DISCOUNT', '2030-12-31');
INSERT INTO coupons (user_id, type, expires) VALUES (456, 'DISCOUNT', '2030-12-31');
...
2
3
4
5
实际上执行 JDBC 时,因为只有占位符参数不同,所以 SQL 实际上是一样的:
for (var params : paramsList) {
PreparedStatement ps = conn.preparedStatement("INSERT INTO coupons (user_id, type, expires) VALUES (?,?,?)");
ps.setLong(params.get(0));
ps.setString(params.get(1));
ps.setString(params.get(2));
ps.executeUpdate();
}
2
3
4
5
6
7
通过一个循环来执行每个 PreparedStatement
虽然可行,但是性能很低。SQL 数据库对 SQL 语句相同,但只有参数不同的若干语句可以作为 batch 执行,即批量执行,这种操作有特别优化,速度远远快于循环执行每个 SQL。
在 JDBC 代码中,我们可以利用 SQL 数据库的这一特性,把同一个 SQL 但参数不同的若干次操作合并为一个 batch 执行。我们以批量插入为例,示例代码如下:
try (PreparedStatement ps = conn.prepareStatement("INSERT INTO students (name, gender, grade, score) VALUES (?, ?, ?, ?)")) {
// 对同一个 PreparedStatement 反复设置参数并调用 addBatch():
for (Student s : students) {
ps.setString(1, s.name);
ps.setBoolean(2, s.gender);
ps.setInt(3, s.grade);
ps.setInt(4, s.score);
ps.addBatch(); // 添加到 batch
}
// 执行 batch:
int[] ns = ps.executeBatch();
for (int n : ns) {
System.out.println(n + " inserted."); // batch 中每个 SQL 执行的结果数量
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
执行 batch 和执行一个 SQL 不同点在于:
- 需要对同一个
PreparedStatement
反复设置参数并调用addBatch()
,这样就相当于给一个 SQL 加上了多组参数,相当于变成了“多行”SQL。 - 第二个不同点是调用的不是
executeUpdate()
,而是executeBatch()
,因为我们设置了多组参数,相应地,返回结果也是多个int
值,因此返回类型是int[]
,循环int[]
数组即可获取每组参数执行后影响的结果数量。
# 总结
JDBC 提供了事务的支持,使用 Connection 可以开启、提交或回滚事务。
使用 JDBC 的 batch 操作会大大提高执行效率,对内容相同,参数不同的 SQL,要优先考虑 batch 操作。