news 2026/10/7 3:55:19

JDBC批处理性能优化实战:从逐条更新到批量提交的原理与踩坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JDBC批处理性能优化实战:从逐条更新到批量提交的原理与踩坑指南

1. 批处理到底解决了什么问题:从一次线上事故说起

先讲一个我实际经历过的场景。数据库表一共几千万条记录,业务方一次性要回填几百万条数据的统计状态。第一版代码用的是老实的逐条UPDATE,PreparedStatement循环执行,跑了两分钟才写完十分之一不到,照这个速度下去得跑半个多小时,业务那边根本等不起。后来我换成JDBC Batch Update,把几千条SQL攒成一个批次再提交,整个回填任务从半小时压缩到了两分钟以内。

这个数字差异背后不是玄学,而是机制问题。JDBC Batch Update(批量更新)本质上是把多条SQL语句打包提交给数据库一次性执行,减少客户端和数据库之间的网络往返次数。我们逐条执行时,每一条SQL都要走一遍“客户端发送请求 -> 数据库解析SQL -> 执行 -> 返回结果”的完整链路,网络来回一次少说零点几毫秒,多则几毫秒;几百万条累积下来,光网络开销就足够把人逼疯。

这篇文章不是教科书式地罗列API方法,而是站在实际项目的角度,把批处理的原理、常见写法、参数调优和踩坑点全部串起来讲。不管你是刚接触JDBC的新手,还是已经用过一批但没搞明白底层机制的开发老手,这篇文章都能帮你把“批量更新”这块拼图补完整。文章里所有的代码示例都来自我在真实项目中跑过的方案,你可以直接把它们抄过去改改用。

2. 三个核心概念:Statement、PreparedStatement和真正的批处理

2.1 三种逐条执行的效率对比

很多人分不清Statement和PreparedStatement的区别,更不清楚它们和批处理搭配时各自的性能影响。先看最基础的:Statement是JDBC里的SQL执行器接口,它直接拼接SQL字符串发送给数据库;PreparedStatement则先对SQL做预编译,再通过占位符传参。

为什么不推荐用Statement来做批量操作?因为Statement.executeBatch()虽然也能批处理,但每一条SQL在数据库端都需要重新解析、重新优化。而PreparedStatement提前预编译一次,后面只是在复用执行计划,只是参数不同。这有点像一个厨师把菜谱背熟了再做十道菜,和每做一道菜都要重新看一遍菜谱的区别。在我实测过的MySQL场景里,同样的批处理逻辑,PreparedStatement比Statement大概快20%到40%,数据库CPU压力也明显更低。

再强调一个重要细节:PreparedStatement的预编译不是万能的,它受数据库连接URL参数的影响。MySQL数据库需要设置useServerPrepStmts=true才会真正走服务端预编译,否则预编译只是发生在客户端驱动内部;而PostgreSQL默认就支持服务端预处理语句,只是每条连接需要单独开启和关闭。后面我会专门讲这套参数配置。

2.2 批量提交的三个关键方法

JDBC批处理的核心机制,其实就落在PreparedStatement的三个方法上:

  • addBatch():把当前参数值加入批处理队列,但这时候SQL并没有真正发送到数据库。
  • executeBatch():把队列里的所有SQL一次性发给数据库执行,返回一个int数组,数组里每个值表示该条SQL影响的记录数。
  • clearBatch():清空队列,一般在executeBatch()之后调用,或者在批处理中途想取消当前累积时调用。

每个方法都有讲究。addBatch()攒的是PreparedStatement对象里的参数绑定状态,所以每绑定一次参数,就要调用一次addBatch(),否则后面的参数会覆盖前面的。executeBatch()返回的int数组长度和addBatch()的次数一致,千万不要大意地忽略这个返回值,后面排查问题时它能给你很大帮助。

但这里有个非常反直觉的点:executeBatch()返回的int数组,在MySQL驱动里,默认情况下的值可能不是真实的更新行数,而只是-2或者SUCCESS_NO_INFO这样的常量。这是因为MySQL Connector/J在没有开启rewriteBatchedStatements=true时,并不会真正把多条SQL合并成一条多VALUES的插入语句发送,而是退化成逐条发送。表面上看你用了executeBatch(),实际上每条SQL仍然是独立网络请求。这在MySQL里是一个极其常见的性能陷阱,下一节我会展开讲。

3. MySQL的rewriteBatchedStatements参数:性能差距的根源

3.1 为什么很多人的executeBatch并没有真正变快

我在很多项目里见过这样的写法:写了一大段addBatch()和executeBatch()的代码,然后测出来性能和逐条执行差不多,就得出结论说“JDBC批处理没用”。这个结论其实错怪了JDBC,真正的问题是MySQL驱动的默认行为。

MySQL的JDBC驱动(Connector/J)在早期版本里,executeBatch()的实现方式是把批处理列表里的SQL逐条发送给数据库服务器。也就是说,虽然在代码层面看起来你是“批处理”,但是网络层面上并没有减少请求次数。这么说吧,executeBatch()只是把SQL打包在了客户端内存里,发送的时候还是一句一句发的。

要让MySQL驱动真正把多条SQL合并成一条复合SQL(比如把多条INSERT合并成INSERT INTO table VALUES (...), (...), (...)),必须要在JDBC URL里加上rewriteBatchedStatements=true这个参数。加了之后,驱动会尝试把批处理中的多条SQL语句重写为一条具备多个VALUES的语句,从而显著减少网络往返。

以我压测过的数据为例,向MySQL插入10万条记录,每批1000条。不开启rewriteBatchedStatements时耗时约12秒;开启后耗时直接降到1.5秒左右,效果差了接近一个数量级。这个差异在网络延时高、部署在不同主机的场景下会被进一步放大。

3.2 参数配置的完整清单

rewriteBatchedStatements=true在面对批量INSERT时效果最明显,但它在处理批量UPDATE时也有效,只是重写逻辑会更保守。MySQL驱动只有在满足一定条件时才会重写UPDATE,比如更新条件统一、SET子句结构一致等。所以在批量更新场景,我建议同时配置下面这些参数,整体链路才会顺畅:

参数推荐值作用
rewriteBatchedStatementstrue将多条SQL重写为一条,显著减少网络往返
useServerPrepStmtstrue让预编译在MySQL服务端执行,降低重复解析开销
cachePrepStmtstrue缓存预编译语句,避免重复预编译
prepStmtCacheSize250预编译语句缓存数量,根据业务灵活调整
useSSLfalse内网环境建议关闭SSL,减少握手耗时(注意:生产环境需结合安全要求)

这些参数最终拼在连接URL里,大概是这个样子:

jdbc:mysql://localhost:3306/test_db?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useServerPrepStmts=true&cachePrepStmts=true&prepStmtCacheSize=250

值得说明的是,这里有个隐含的权衡。rewriteBatchedStatements=true虽然提升了批量写入性能,但它会改变驱动与数据库交互的语义。最典型的影响是:当批量中某条SQL执行失败时,驱动可能无法精确定位到具体是哪一条失败(因为SQL已经被重写了),整体错误处理逻辑需要设计得更加健壮。这就引出一个我反复强调的经验——批处理不是越多越好,批次大小要控制在一个合理的范围。

4. 实战:完整可运行的批量更新代码示例

4.1 批量插入:最常用的场景

先上最常见的批量插入示例。业务场景是往用户积分流水表里插入大量记录,MySQL 8.0,表结构为:id(自增主键)、user_id、points、reason、create_time。

public void batchInsert(List<UserPointRecord> records) throws SQLException { String sql = "INSERT INTO t_user_points (user_id, points, reason, create_time) VALUES (?, ?, ?, ?)"; int batchSize = 500; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // 手动开启事务,避免每条SQL自动提交 conn.setAutoCommit(false); for (int i = 0; i < records.size(); i++) { UserPointRecord record = records.get(i); ps.setLong(1, record.getUserId()); ps.setInt(2, record.getPoints()); ps.setString(3, record.getReason()); ps.setTimestamp(4, new Timestamp(record.getCreateTime().getTime())); ps.addBatch(); // 每满batchSize条执行一次批次提交 if ((i + 1) % batchSize == 0) { ps.executeBatch(); ps.clearBatch(); } } // 处理最后不足一个批次的部分 if (records.size() % batchSize != 0) { ps.executeBatch(); ps.clearBatch(); } conn.commit(); } catch (SQLException e) { // 可以根据实际需要增加事务回滚逻辑 // conn.rollback(); throw e; } }

这段代码有四个细节值得注意。

第一,conn.setAutoCommit(false)这一步非常关键。JDBC默认的自动提交模式下,每一条SQL执行完就会立即提交事务,即使做了批处理,也相当于每一条都单独提交事务,频繁的提交操作会显著增加磁盘IO和日志写入压力。关闭自动提交、程序控制一次性提交,性能会有明显提升。

第二,批次大小不是越大越好。批次太大会导致内存中堆积大量参数对象,也可能让MySQL服务器一次性处理太多语句,出现性能抖动;批次太小又起不到减少网络往返的效果。经过多组压测,我觉得500到1000是一个比较合适的区间,具体取值建议结合单条SQL的复杂度和数据库配置去调整。

第三,ps.clearBatch()的执行时机不能忘。如果不清理批处理队列,下一次累积会让批次无限增大,最终可能导致内存溢出。

第四,还要处理“尾巴”。当总记录数不能被batchSize整除时,最后剩余的记录也要执行一次批量提交,否则这些记录会被遗漏。

4.2 批量更新:动态条件怎么处理

批量更新比批量插入稍微复杂一点,因为很多业务场景下,每条记录的更新条件不一样。比如要给一批用户分别增加不同的积分值:

public void batchUpdatePoints(Map<Long, Integer> userIdToPoints) throws SQLException { String sql = "UPDATE t_user_points SET points = points + ? WHERE user_id = ?"; int batchSize = 300; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { conn.setAutoCommit(false); int count = 0; for (Map.Entry<Long, Integer> entry : userIdToPoints.entrySet()) { ps.setInt(1, entry.getValue()); ps.setLong(2, entry.getKey()); ps.addBatch(); count++; if (count % batchSize == 0) { ps.executeBatch(); ps.clearBatch(); } } // 处理剩余批次 ps.executeBatch(); ps.clearBatch(); conn.commit(); } }

这里的UPDATE虽然只改了一张表,但执行计划在数据库端是根据WHERE user_id = ?来查询索引的。PreparedStatement预编译的SQL会让MySQL复用同一个执行计划,这样一来,真正变化的只有参数值,比每次拼新SQL要快得多。

有人可能会问,能不能把SQL写成UPDATE t_user_points SET points = CASE WHEN user_id = ? THEN ? ... END这种形式,一条SQL更新多条记录?可以,但这种方式有几个前提:一是更新条件必须在同一个主键或者同一个索引上;二是CASE WHEN分支越多,SQL字符串越长,数据库解析SQL的开销会线性增加;三是这种写法完全绕过了驱动层面的批量重写机制。我的建议是:当更新逻辑简单、条件统一时,可以尝试这种一条SQL更新的方案;当更新条件复杂多变时,老老实实用PreparedStatement批处理反而更稳。

4.3 MySQL JDBC URL完整示例

把上面说的参数组合起来,一个实际可用的连接串如下:

public static final String JDBC_URL = "jdbc:mysql://127.0.0.1:3306/test_db" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai" + "&rewriteBatchedStatements=true" + "&useServerPrepStmts=true" + "&cachePrepStmts=true" + "&prepStmtCacheSize=250" + "&allowMultiQueries=false";

allowMultiQueries=false是我刻意加上的。这个参数控制的是是否允许一条SQL中包含多个分号分隔的语句,默认就是关闭的。在批处理场景中,如果误开启了它,同时大批量addBatch(),反而可能引起SQL语义混乱,建议保持关闭。

5. PostgreSQL、Oracle等其他数据库的表现与差异

5.1 PostgreSQL的批量更新默认行为

PostgreSQL的JDBC驱动走的是另一条路线——它不通过URL参数来控制批处理行为,而是默认就会将批处理中的语句打包到一条隐式的批量消息中发送,配合服务端的PREPARE机制,性能表现通常不错。但也有一个经典坑:PGBouncer连接池在事务模式下,如果开启了服务端预处理语句,可能因为连接被复用导致PREPARE语句找不到,直接抛PreparedStatement was already closed或者prepared statement "S_1" does not exist错误。遇到这种情况,建议在JDBC URL中显式添加prepareThreshold=0来禁用服务端预处理,或者使用连接池的语句缓存特性来代替。

5.2 Oracle的批处理与JDBC标准

Oracle JDBC驱动完全遵循JDBC标准规范,同时提供了Connection.setDefaultExecuteBatch(int)这样的扩展方法,允许在连接级别设置默认批处理大小。和MySQL不同的是,Oracle的批处理对executeBatch()返回值的处理更严格,数组中的每个元素都明确表示该条SQL影响的记录数。如果你在Oracle里做批量更新后,发现返回值数组的每个元素都是SUCCESS_NO_INFO或EXECUTE_FAILED,说明你的语句可能触发了Oracle的数组绑定限制,需要调小批次大小。

下面这张表可以帮你快速对照三类数据库的批处理特性:

数据库批处理关键机制典型配置注意事项
MySQLrewriteBatchedStatements重写SQLURL加rewriteBatchedStatements=true影响返回行数语义
PostgreSQL默认批量消息 + PREPARE无特殊配置搭配PGBouncer时注意禁用服务端预处理
OracleJDBC标准批处理 + 扩展方法setDefaultExecuteBatch(n)批次过大可能触发数组绑定限制

6. Spring JdbcTemplate和MyBatis中的批处理封装

6.1 JdbcTemplate的batchUpdate简化开发

实际项目里,直接操作PreparedStatement的场景越来越少,大部分都是走Spring的JdbcTemplate或者MyBatis。先说JdbcTemplate,它把addBatch、executeBatch、clearBatch这些操作全部封装了起来,代码可以精简到:

public void batchInsertWithJdbcTemplate(List<UserPointRecord> records) { String sql = "INSERT INTO t_user_points (user_id, points, reason, create_time) VALUES (?, ?, ?, ?)"; jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { UserPointRecord record = records.get(i); ps.setLong(1, record.getUserId()); ps.setInt(2, record.getPoints()); ps.setString(3, record.getReason()); ps.setTimestamp(4, new Timestamp(record.getCreateTime().getTime())); } @Override public int getBatchSize() { return records.size(); } }); }

BatchPreparedStatementSetter内部帮你维护了批处理和批次大小的管理,但前提是你需要提前知道批处理的总大小。现实中如果数据的来源是流式的(比如从消息队列里一条条拿),那么用JdbcTemplate就不太方便了,还是得回到手写PreparedStatement的方式,用计数器自行控制批次。

6.2 MyBatis的ExecutorType.BATCH模式

MyBatis的批处理相比JdbcTemplate更隐蔽一些。大部分MyBatis教程都在讲动态SQL和Mapper XML,很少有人专门讲批处理。但MyBatis确实支持批处理,只需要把SqlSession的ExecutorType设为BATCH:

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH); try { UserPointRecordMapper mapper = session.getMapper(UserPointRecordMapper.class); for (UserPointRecord record : records) { mapper.insert(record); // 手动控制每500条flush一次 if (count % 500 == 0) { session.flushStatements(); session.clearCache(); } } session.commit(); } finally { session.close(); }

这里面有几个隐蔽的坑。第一,ExecutorType.BATCH模式下,Executor不会立即执行单条SQL,而是把SQL和参数缓存在Executor内部,直到调用flushStatements()或commit()时才真正发送给数据库。所以你在一个批量循环里调用mapper.insert(),它的返回结果永远是固定的影响行数,也就是不会返回真实的插入结果。第二,如果在批量执行后立刻执行一个需要依赖刚插入数据的SELECT,可能会查不到数据,因为数据还在Executor的内存缓冲区里没落库。遇到这种场景,需要先flush再查询。这和直接操作JDBC是完全不同的行为模式,新手很容易在这里栽跟头。

6.3 对框架选型的结论

我的经验是,如果项目里已经用了Spring,那JdbcTemplate是最高效的选择,代码量少、出错概率低;如果项目里已经深度依赖MyBatis,那ExecutorType.BATCH也够用,但要理解清楚flush机制的执行时机;而手写PreparedStatement的最大优势在于控制粒度最细——你可以精确掌握每个批次何时执行、何时清理、何时提交,这对特殊场景(比如大批量数据导入中需要处理热数据)非常有用。

7. 关键参数调优:批次大小、事务边界和语句重用

7.1 批次大小的选择逻辑

先说结论:批次大小没有绝对标准,它是一个需要结合实际压测结果调整的参数。但可以给一个经验公式作为初值参考:单批SQL的总体网络请求大小控制在1MB到4MB之间,然后结合数据库服务器的CPU和IO负载来微调。

以MySQL为例,假设单条INSERT语句平均200字节,那么500条一组的批次大小,一次发送的数据量大约是100KB,这个量级在网络传输中非常流畅。以5000条一组,数据量就达到了1MB,虽然单次请求时间变长了,但总请求次数减少,整体未必更慢。我曾经在一次项目里分别测试了100、500、1000、2000、5000五个档次,最终在2000档时性能达到峰值,再往上走反而因为数据库端解析和执行单条大SQL的开销增加,性能出现回落。

需要注意的是,批次大小还会影响到事务的长度。批次越大,一个事务里累积的未提交数据越多,如果中途发生异常回滚,回滚的代价也越大。所以在一个复杂的多表操作流程中,我倾向于用较小的批次(200-500)来降低事务故障的爆炸半径。

7.2 事务边界的正确控制

手动管理事务在批处理中是不可回避的,无论你用连接池还是裸Connection,都要把批处理包在一个显式事务里。常见错误是:先执行了conn.setAutoCommit(true)(默认就是true),然后调executeBatch(),这种情况下每个批次都会被立即提交,一旦数据量很大,磁盘刷盘压力会迅速飙升,整体性能直接腰斩。

正确过程是:开启事务(setAutoCommit(false))-> 循环执行addBatch和定期executeBatch -> 全部完成后再commit。异常路径上应该回滚事务,或者至少记录错误日志然后重新处理相应批次。

有一种情况需要特别小心:MySQL中的DDL操作会隐式提交事务。如果在批处理过程中,忽略了连接上之前执行过一次DDL(比如CREATE TABLE或ALTER TABLE),那么这个事务边界可能已经被自动提交了,后续的批处理实际上是独立事务。排查这类问题时,可以利用SHOW ENGINE INNODB STATUS查看最近的事务提交情况。

7.3 PreparedStatement的重用与缓存

PreparedStatement的定义是“一次预编译,多次执行”。在批处理循环中,应该确保PreparedStatement对象被完全复用,不要在循环内部重复调用prepareStatement()。否则每次都会触发一次SQL解析和预编译,性能开销又会涨回去。

在MySQL连接的URL参数里,cachePrepStmts=true会把预编译的Statement缓存到驱动内部,加上prepStmtCacheSize=250后,相同SQL模板的prepareStatement()调用就会直接命中缓存,避免了重复的预编译。这个参数在高频批处理场景里收益非常明显,但对内存会有一定占用,需要根据SQL模板数量来设置缓存大小。

8. 实战中的坑:从数据不一致到连接池异常

8.1 批量更新时返回行数不准确

前面提到过,MySQL开启rewriteBatchedStatements后,executeBatch()返回的int数组不能完全相信。实测中,它的返回值往往不是真实的更新行数,甚至可能都是-2。如果你依赖这个返回值去判断数据是否更新成功,那一定要做好兼容。一个稳妥的做法是:在批量更新后,再用SELECT语句验证关键数据;或者在业务逻辑中不依赖返回值,而是以最终提交成功为准。

8.2 大批量操作导致数据库超时

还有一个非常常见的坑:批量任务执行时间过长,超过了数据库连接或者驱动的socketTimeout,导致驱动抛出连接超时异常。尤其在批量UPDATE涉及大量行锁和索引更新时,单次executeBatch()的执行时间很容易飙升。

这种情况下,可以分两层来解决。第一层是减少单个批次的大小,把每批次控制在200到500条,让单次executeBatch()的耗时维持在几百毫秒内;第二层是提高JDBC URL中的socketTimeout值(单位是毫秒),比如设置成60000或更高,但要结合数据库端的wait_timeout、interactive_timeout参数一起考虑,避免客户端超时而服务端仍占用连接。

8.3 连接池中的连接被耗尽

高并发的批处理任务会在短时间内占用大量数据库连接。如果你的应用用的是HikariCP,默认的maximumPoolSize是10,假设有20个线程同时在做批处理,每一个都要占用一个连接且事务较长,剩下的连接池必然崩溃。这就是我们在真实项目中遇到的“flink的jdbc连接器异常”这类问题的背后逻辑——上游任务并发度高,JDBC连接池参数没有跟上,连接获取超时,整个任务链路报错。

我的建议是,批处理任务尽量串行化或者控制并发度,不要让太多线程同时执行批处理。如果必须并发,就要按批处理任务的实际并发数来调整连接池大小,并设置合理的连接获取超时时间。在HikariCP里,connectionTimeout默认是30000毫秒,如果长期并发,建议把minimumIdle和maximumPoolSize设置为一致,避免频繁创建和销毁连接。

8.4 批量操作遇到死锁

批量更新很容易触发死锁,尤其是并发更新同一张表的不同行,或者先更新一个表后排他锁升级。死锁的典型报错是Deadlock found when trying to get lock; try restarting transaction。

从实践来看,规避死锁有几个实用技巧:一是让批处理中UPDATE语句的条件集中在同一个索引列上,且让多条记录的顺序一致,避免两个事务以不同顺序锁定数据行;二是把大事务拆小,尽快提交释放锁;三是如果死锁偶发,可以在应用层捕获死锁异常后做有限次重试。

8.5 新手最容易犯的错误速查表

下面这张表汇总了我见过的最典型的批处理错误,你可以直接拿去做排查手册:

错误类型具体表现解决方案
循环内重复prepareStatementCPU高,执行慢提取prepareStatement到循环外
忘记clearBatch内存增长,SQL积压每次executeBatch后执行clearBatch
忘记处理批次“尾巴”部分记录缺失循环结束后对余量再执行一次executeBatch
依赖executeBatch返回值结果与预期不符改用SELECT验证或依赖事务提交
批处理内部穿插其他查询结果与顺序不一致先flush再查询
批处理没有关闭自动提交每次executeBatch都提交事务显式setAutoCommit(false)+commit
批次大小设置极端性能波动或OOM从500或1000开始压测调整

9. 性能对比:实测数据的参考意义

为了让大家对批处理的提升有更直观的感知,我把一次实际压测结果整理出来。测试环境:MySQL 8.0,CentOS虚拟机,应用与数据库同机,Java 11,数据量10万条。

执行方式耗时备注
逐条INSERT,自动提交58秒每条一次网络往返
逐条INSERT,手动提交47秒减少了每次提交的开销
批处理500条,不开启rewrite12.5秒批量发送但仍逐条执行
批处理500条,开启rewrite1.8秒多VALUES合并执行
批处理1000条,开启rewrite1.5秒批次变大略提升
批处理2000条,开启rewrite1.4秒接近本次压测最佳值

从数据中可以得出几个明确结论:开启rewriteBatchedStatements的收益远大于调整批次数目;手动关闭自动提交是基础优化,不做这个操作,其他优化都会被拖累;批次从500调到2000只减少了0.4秒,说明到达一定量级后边际收益很小,不必盲目增加批次大小。

10. 连接池与批处理的搭配实践

批处理性能再高,最终还是要通过连接池来访问数据库。连接池的几个关键参数,和批处理协同工作时,我会这样设定:

  • maximumPoolSize:串行批处理任务设置5-10即可,并发批处理按并发数乘以2估算。
  • connectionTimeout:默认30秒,高并发批量任务建议加大到45秒或60秒,避免正常排队被误杀。
  • idleTimeout:不建议设置太短,否则连接频繁回收重建,对批处理的连接建立开销很不友好。
  • maxLifetime:保持默认即可,但要注意小于数据库的wait_timeout。

HikariCP官方文档里特别强调了一个隐性问题:连接建立后,如果长时间空闲,数据库会主动断开它。如果批处理任务有间歇性(比如每隔一小时处理一次),连接池里的连接可能早被数据库断开了。此时如果没有开启连接的有效性检测,第一次批处理调用会直接抛异常。开启connectionTestQuery或设置validationTimeout是很有必要的。

11. 更多延伸:批量DELETE与其他数据库的注意事项

讲完了批量INSERT和UPDATE,批量DELETE其实也值得单独提一下。DELETE的批量执行同样可以使用PreparedStatement + addBatch,但性能提升幅度不如INSERT那么夸张,因为DELETE本身不涉及太多索引重建,但它更容易引发锁竞争。大批量DELETE时,MySQL会对扫描到的行加锁,如果条件没有命中合适的索引,锁的范围可能退化到全表。批量DELETE的核心经验:先依据主键或唯一键定位到记录ID,再用ID集合分批删除,而不是直接按业务条件批量删除。

关于批量DELETE,还有一个容易被忽略的点:大批量删除后,MySQL的索引碎片会明显增加,表空间不一定自动收缩。生产环境建议在批量删除后执行OPTIMIZE TABLE或ANALYZE TABLE来整理表空间,尤其是面对频繁插入删除的表。

12. 最后分享一点个人体会

从最早的逐条执行,到后来慢慢摸索出批处理、连接池、驱动参数的配合方式,我最大的感受是:JDBC批处理的性能瓶颈通常不在JDBC本身,而在你没有理解数据库驱动为你做了什么事情、没做什么事情。MySQL的rewriteBatchedStatements就是个非常典型的例子——它驱动层干了很多重活,但没告诉开发者它默认不开启。

在实际项目中,我建立了一套固定的开发习惯:任何批量操作上线前,先在一个小数据集上对比“逐条执行、批处理不开重写、批处理开启重写”三者的耗时和数据库负载,再决定使用哪种方案;任何连接池配置变更,都要和批处理任务的并发度、单批次大小做联动评估,否则性能优化很可能被连接瓶颈抵消。这些经验不是来自任何一份官方文档,而是真金白银踩坑踩出来的。希望这篇文章能让你少走一些弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 3:55:18

JDBC批量更新性能优化:从原理到实战避坑指南

接到过一个线上任务&#xff0c;日终批量给一张千万级的用户表打标签&#xff0c;几千条数据逐条 update&#xff0c;跑了快二十分钟还带超时。后来换成 JDBC Batch Update&#xff0c;压到几十秒收工&#xff0c;这是第一次直观感受到批量提交的差距。JDBC 批处理不是什么新东…

作者头像 李华
网站建设 2026/10/7 3:54:32

Agent-Reach 实战:用 CLI 和 Python 为 AI Agent 构建工具调用能力

1. 从零认识 Agent-Reach&#xff1a;一个 CLI 工具到底在解决什么问题第一次看到 Agent-Reach 这个名字&#xff0c;很多人会下意识把它归类成又一个"AI Agent 框架"。但如果你真的动手跑过几个 Agent 项目&#xff0c;就会发现一个很现实的问题&#xff1a;Agent 的…

作者头像 李华
网站建设 2026/10/7 3:54:31

Docker安装配置全指南:Windows、Mac、Linux三平台镜像加速与避坑

1. 从"在我这儿能跑"说起&#xff1a;Docker到底在解决什么问题做开发这些年&#xff0c;几乎每个人都被同一句话折磨过&#xff1a;"代码我这边跑得好好的&#xff0c;你那儿怎么就不行&#xff1f;"环境不一致带来的问题&#xff0c;远比代码本身多得多。…

作者头像 李华
网站建设 2026/10/7 3:54:11

MH系列土壤湿度传感器调试与自动浇灌系统实战指南

做土壤湿度传感器这类项目&#xff0c;最容易被忽略的反而不是“怎么接”&#xff0c;而是“你拿到的到底是个什么东西”。MH-Sensor-Series这个名字在各大电子商城里随处可见&#xff0c;但同系列下不同后缀、不同探头形状的模块&#xff0c;电气特性和输出逻辑差异很大。这篇…

作者头像 李华
网站建设 2026/10/7 3:53:52

CSO-LSSVM多输出回归预测:原理、代码与调参实战

最近一直在捣鼓多输出回归预测这个方向&#xff0c;说白了就是让模型一次性预测多个连续目标变量。以前做单输出预测&#xff0c;一个目标建一个模型&#xff0c;看着简单&#xff0c;但到了真实工业场景里&#xff0c;你会发现很多问题是天然多输出的——你预测一个设备的剩余…

作者头像 李华
网站建设 2026/10/7 3:53:32

agent-skills 实战:用 skills CLI 为 Claude Code 构建可复用技能体系

1. 从"agent-skills"这个标题能读出什么第一次看到agent-skills这个仓库名&#xff0c;我的直觉是&#xff1a;这不是又一个"提示词大全"&#xff0c;而是一套把 AI coding agent 当"新员工"来培养的技能体系。事实也确实如此——它把散落在各种…

作者头像 李华