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 独立提交。

第 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() 前;如果更看重失败恢复速度,就按固定窗口提交。不要混用两种语义:调用方必须知道失败后是重做整个文件、重做当前窗口,还是只处理未确认的行。

第 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。
为什么不一直把所有数据放进一个事务?
单事务容易带来更长的锁持有时间、更大的日志和更高的失败重做成本。大批量导入通常应使用可观测的固定窗口,但最终边界仍要服从业务原子性要求。
-
238 收藏
-
348 收藏
-
Golang · Go问答 | 2个月前 | golang · 连接池 · database/sql · Go问答 · 连接池 事务 DBStats rows.Close Go database/sql374 收藏
-
360 收藏
-
397 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习