登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

Java JDBC 批量写入的事务和参数绑定

来源:17golang原创

时间:2026-10-17 15:28:54 156浏览 收藏

Java JDBC 批量写入的关键不是简单地把多条 SQL 放进循环,而是把三个边界同时设计清楚:同一条 PreparedStatement 如何绑定多组参数、多少行组成一个 batch,以及这一批数据何时提交或回滚。最小可靠做法是关闭自动提交,复用预编译语句,达到窗口大小后调用 executeBatch(),成功就 commit(),失败就 rollback()。

官方参考:https://docs.oracle.com/en/java/javase/26/docs/api/java.sql/java/sql/PreparedStatement.html

批量更新接口:https://docs.oracle.com/en/java/javase/26/docs/api/java.sql/java/sql/Statement.html

如果一批写入必须作为一个业务单元,就把 setAutoCommit(false)、executeBatch() 和 commit() 放在同一个明确的事务窗口里;如果数据量很大,则用固定窗口分批提交,不要让整个文件导入永远占着一个事务。

先区分参数批量和事务批量

PreparedStatement.addBatch() 的含义是把当前已经绑定好的参数组加入语句自己的 batch;它不会自动提交,也不会因为调用了 executeBatch() 就替你决定事务边界。事务仍由 Connection 控制。这样设计的好处是,SQL 模板只解析一次,参数按行替换,调用方可以把多个 batch 放进一个事务,也可以每个 batch 独立提交。

PreparedStatement 复用同一条 SQL 并把多组参数加入 batch 的结构说明图
图1:PreparedStatement、参数组与 batch 的静态结构说明图,不是运行截图。

第 1 步:关闭自动提交并确定窗口

默认自动提交时,每条完成的更新都可能成为独立提交单元,批量调用就很难表达“这一窗口要一起成功”。先调用 connection.setAutoCommit(false),再根据业务选择窗口大小。订单导入通常可以按 500 或 1000 行划分窗口;真正的数值要结合单行大小、数据库日志和失败重试成本调整。

下面的示例把每个窗口作为一个事务。代码中的注释说明了提交边界、资源关闭和异常回滚的关系:

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;

record UserRow(long id, String name, String email) {}

static void insertUsers(Connection connection, List rows, int batchSize)
        throws SQLException {
    String sql = "INSERT INTO app_user (id, name, email) VALUES (?, ?, ?)";

    // 关闭自动提交,让一个窗口内的写入由 commit/rollback 统一决定。
    connection.setAutoCommit(false);
    try (PreparedStatement statement = connection.prepareStatement(sql)) {
        int inWindow = 0;
        for (UserRow row : rows) {
            // 每次循环只替换参数,不拼接 SQL,避免参数错位和 SQL 注入风险。
            statement.setLong(1, row.id());
            statement.setString(2, row.name());
            statement.setString(3, row.email());
            statement.addBatch();
            inWindow++;

            if (inWindow == batchSize) {
                // 更新计数按加入顺序返回;执行成功后才提交当前窗口。
                statement.executeBatch();
                connection.commit();
                statement.clearBatch();
                inWindow = 0;
            }
        }

        if (inWindow > 0) {
            // 处理不足一个完整窗口的尾部数据,避免最后几行被遗漏。
            statement.executeBatch();
            connection.commit();
            statement.clearBatch();
        }
    } catch (SQLException error) {
        // 当前事务窗口失败时回滚;原异常继续抛给上层决定是否重试。
        try {
            connection.rollback();
        } catch (SQLException rollbackError) {
            error.addSuppressed(rollbackError);
        }
        throw error;
    }
}

第 2 步:正确绑定每组参数

对 PreparedStatement 来说,addBatch() 不接受 SQL 字符串,它记录的是当前参数值。因此每次加入前都要覆盖所有参数位置。不要只修改“变化的字段”,否则某一行可能沿用上一行的旧值。

调用作用常见误区
setLong/setString绑定当前行参数漏绑位置或复用错误索引
addBatch()保存一组参数误以为已经写入数据库
executeBatch()提交当前语句的批处理请求忽略返回的更新计数
clearBatch()清空已执行的语句批次窗口循环时重复执行旧数据

第 3 步:把 executeBatch 放在提交之前

一个稳定的顺序是“加入参数 → 执行 batch → 提交事务 → 清空 batch”。executeBatch() 返回的数组与加入顺序对应,每个元素可能是实际更新行数,也可能是驱动无法提供精确数量时的 Statement.SUCCESS_NO_INFO。所以它适合用来记录窗口结果,但不能把所有驱动的返回值都当成精确行数。

如果业务要求整批原子成功,可以把多个 executeBatch() 都放到一次 commit() 前;如果更看重失败恢复速度,就按固定窗口提交。不要混用两种语义:调用方必须知道失败后是重做整个文件、重做当前窗口,还是只处理未确认的行。

JDBC 批量写入按窗口执行并在成功提交失败回滚的事务关系说明图
图2:批量执行、提交与回滚的静态关系说明图,不是数据库运行结果截图。

第 4 步:处理 BatchUpdateException

批处理中某条命令失败时,驱动可能已经处理了前面的命令,也可能继续尝试后续命令;具体行为不能靠猜。捕获 BatchUpdateException 后,可以读取它携带的更新计数数组,并把 SQLState、错误码和当前窗口的业务范围记录下来。是否重试要先确认失败是临时连接问题、约束冲突,还是输入本身不可写。

try {
    // 只执行当前窗口,不在异常分支里自动把同一批再次发送。
    int[] counts = statement.executeBatch();
    connection.commit();
    System.out.println("window updates=" + counts.length);
} catch (java.sql.BatchUpdateException error) {
    // 更新计数只用于诊断,不能据此假定驱动一定执行了剩余命令。
    int[] counts = error.getUpdateCounts();
    connection.rollback();
    System.err.println("batch failed, processed markers=" + counts.length);
    System.err.println("sqlState=" + error.getSQLState()
            + ", vendorCode=" + error.getErrorCode());
    throw error;
} catch (SQLException error) {
    // 非批处理异常同样回滚当前事务,再交给上层决定补偿策略。
    connection.rollback();
    throw error;
}

回滚只能撤销仍在当前事务里的变化。如果上一窗口已经提交,就不能期待后一个窗口的回滚把它一起撤销。因此需要在日志中记录窗口编号、首尾主键、输入来源和提交结果,重试时按窗口定位,而不是盲目重放全部数据。

第 5 步:选择合适的批量窗口

窗口越大,网络往返和提交次数通常越少,但单次失败的重试成本、事务日志压力和驱动内存占用也越高。可以先从固定窗口开始,并在日志中记录耗时、更新计数和异常类型,再根据真实负载调整。

场景推荐边界失败后的动作
少量配置变更一个业务事务整体回滚并提示调用方修正输入
大文件导入固定行数窗口记录窗口范围,只重做失败窗口
可幂等同步按主键或业务键分组校验幂等条件后再重试
约束冲突密集较小窗口或先预校验区分坏数据与临时故障,不自动无限重试

最后核对四个结果

完成实现后,重点确认:每行参数都在 addBatch() 前重新绑定;尾部不足一个窗口的数据也执行过;executeBatch() 成功后才 commit();任何失败路径都会回滚当前未提交窗口并保留原始异常。满足这四点,JDBC 批量写入的事务和参数语义就比较清楚。

常见问题

调用 addBatch 后是不是已经写入数据库?

不是。它只是把当前参数组加入语句的批列表,真正执行要等到 executeBatch()。

executeBatch 失败后能不能直接再次调用?

不要默认可以。先回滚当前事务,判断失败原因和驱动已经处理的范围,再决定按窗口重试还是修正坏数据后重新构造 batch。

为什么不一直把所有数据放进一个事务?

单事务容易带来更长的锁持有时间、更大的日志和更高的失败重做成本。大批量导入通常应使用可观测的固定窗口,但最终边界仍要服从业务原子性要求。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>