2022-06-08:JDBC事务、批量(batch)操作

1、JDBC事务

数据库事务(Transaction)是由若干个SQL语句构成的一个操作序列,有点类似于Javasynchronized同步。数据库系统保证同一个事务中的所有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都是作为事务自动执行的,这也是为什么之前几节我们的更新操作总是能成功的原因:因为默认有这种“隐式事务”。只要关闭了ConnectionautoCommit,那么就可以在一个事务执行多条语句,事务以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操作。

 

posted @ 2022-06-08 16:12  ShineLe  阅读(192)  评论(0)    收藏  举报