2022-06-08:JDBC事务、批量(batch)操作
1、JDBC事务
数据库事务(Transaction)是由若干个SQL语句构成的一个操作序列,有点类似于Java的synchronized同步。数据库系统保证在同一个事务中的所有SQL要么全部执行成功,要么全部不执行,即数据库事务具有ACID特性:
- Atomicity:原子性
- Consistency:一致性
- Isolation:隔离性
- Durability:持久性
数据库事务可以并发执行,而数据库系统从效率考虑,对事务定义了不同的隔离级别。SQL标准定义了4种隔离级别,分别对应可能出现的数据不一致的情况:
对于应用程序而言,数据库事务很重要,很多运行着关键任务的应用程序,都必须依赖数据库事务来保证程序的结果正常。
举例说明:假设A打算给B转账100,A在数据库中的主键为123,B为456,那么用两条SQL语句操作如下:
UPDATE accounts SET balance = balance - 100 WHERE id=123 AND balance >= 100;
UPDATE accounts SET balance = balance + 100 WHERE id=456;
这两条语句必须以事务方式执行才能保证业务的正确性,因为如果第一条SQL执行成功而第二条SQL失败,那么系统中的钱会凭空少100,而有了事务就能保证要么转账成功,要么失败双方的钱都不变。
这里我们不讨论详细的SQL事务,如果对SQL事务不熟悉,可以参考SQL事务。
要在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(); }
其中,开启事务的关键代码是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);
如果没有调用上述方法,那么会使用数据库的默认隔离级别。MySQL的默认隔离级别是REPEATABLE READ。
小结
数据库事务具有ACID特性:
- Atomicity:原子性
- Consistency:一致性
- Isolation:隔离性
- Durability:持久性
JDBC提供了事务的支持,使用Connection可以开启、提交或回滚事务。
2、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'); ...
实际上执行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(); }
类似的还有,给每个员工薪水增加10%~30%:
UPDATE employees SET salary = salary * ? WHERE id = ?
通过一个循环来执行每个PreparedStatement虽然可行,但是性能很低。SQL数据库对语句相同,参数不同的若干语句可以作为batch执行,即批量执行,这种操作有特别优化,速度远远快于循环执行每个SQL。
在JDBC代码中,我们可以利用该特性,把同一个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执行的结果数量 } }
执行batch和执行一个SQL不同点在于,需要对同一个PreparedStatement反复设置参数并调用addBatch(),这样就相当于给一个SQL加上了多组参数,相当于变成了“多行”SQL。
第二个不同点是调用的不是executeUpdate(),而是executeBatch(),因为我们设置了多组参数,相应的,返回结果也是多个int值,因此返回类型是int[ ],循环int [ ]数组即可获取每组参数执行后影响的结果数量。
小结
使用JDBC的batch操作会大大提高执行效率,对于内容相同、参数不同的SQL,要优先考虑batch操作。