ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

JDBC批处理性能优化实战:从逐条更新到批量提交的原理与踩坑指南 1. 批处理到底解决了什么问题从一次线上事故说起先讲一个我实际经历过的场景。数据库表一共几千万条记录业务方一次性要回填几百万条数据的统计状态。第一版代码用的是老实的逐条UPDATEPreparedStatement循环执行跑了两分钟才写完十分之一不到照这个速度下去得跑半个多小时业务那边根本等不起。后来我换成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数据库需要设置useServerPrepStmtstrue才会真正走服务端预编译否则预编译只是发生在客户端驱动内部而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在没有开启rewriteBatchedStatementstrue时并不会真正把多条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里加上rewriteBatchedStatementstrue这个参数。加了之后驱动会尝试把批处理中的多条SQL语句重写为一条具备多个VALUES的语句从而显著减少网络往返。以我压测过的数据为例向MySQL插入10万条记录每批1000条。不开启rewriteBatchedStatements时耗时约12秒开启后耗时直接降到1.5秒左右效果差了接近一个数量级。这个差异在网络延时高、部署在不同主机的场景下会被进一步放大。3.2 参数配置的完整清单rewriteBatchedStatementstrue在面对批量INSERT时效果最明显但它在处理批量UPDATE时也有效只是重写逻辑会更保守。MySQL驱动只有在满足一定条件时才会重写UPDATE比如更新条件统一、SET子句结构一致等。所以在批量更新场景我建议同时配置下面这些参数整体链路才会顺畅参数推荐值作用rewriteBatchedStatementstrue将多条SQL重写为一条显著减少网络往返useServerPrepStmtstrue让预编译在MySQL服务端执行降低重复解析开销cachePrepStmtstrue缓存预编译语句避免重复预编译prepStmtCacheSize250预编译语句缓存数量根据业务灵活调整useSSLfalse内网环境建议关闭SSL减少握手耗时注意生产环境需结合安全要求这些参数最终拼在连接URL里大概是这个样子jdbc:mysql://localhost:3306/test_db?useUnicodetruecharacterEncodingutf8rewriteBatchedStatementstrueuseServerPrepStmtstruecachePrepStmtstrueprepStmtCacheSize250值得说明的是这里有个隐含的权衡。rewriteBatchedStatementstrue虽然提升了批量写入性能但它会改变驱动与数据库交互的语义。最典型的影响是当批量中某条SQL执行失败时驱动可能无法精确定位到具体是哪一条失败因为SQL已经被重写了整体错误处理逻辑需要设计得更加健壮。这就引出一个我反复强调的经验——批处理不是越多越好批次大小要控制在一个合理的范围。4. 实战完整可运行的批量更新代码示例4.1 批量插入最常用的场景先上最常见的批量插入示例。业务场景是往用户积分流水表里插入大量记录MySQL 8.0表结构为id自增主键、user_id、points、reason、create_time。public void batchInsert(ListUserPointRecord 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(MapLong, 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.EntryLong, 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 ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai rewriteBatchedStatementstrue useServerPrepStmtstrue cachePrepStmtstrue prepStmtCacheSize250 allowMultiQueriesfalse;allowMultiQueriesfalse是我刻意加上的。这个参数控制的是是否允许一条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中显式添加prepareThreshold0来禁用服务端预处理或者使用连接池的语句缓存特性来代替。5.2 Oracle的批处理与JDBC标准Oracle JDBC驱动完全遵循JDBC标准规范同时提供了Connection.setDefaultExecuteBatch(int)这样的扩展方法允许在连接级别设置默认批处理大小。和MySQL不同的是Oracle的批处理对executeBatch()返回值的处理更严格数组中的每个元素都明确表示该条SQL影响的记录数。如果你在Oracle里做批量更新后发现返回值数组的每个元素都是SUCCESS_NO_INFO或EXECUTE_FAILED说明你的语句可能触发了Oracle的数组绑定限制需要调小批次大小。下面这张表可以帮你快速对照三类数据库的批处理特性数据库批处理关键机制典型配置注意事项MySQLrewriteBatchedStatements重写SQLURL加rewriteBatchedStatementstrue影响返回行数语义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(ListUserPointRecord 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设为BATCHSqlSession 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参数里cachePrepStmtstrue会把预编译的Statement缓存到驱动内部加上prepStmtCacheSize250后相同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.0CentOS虚拟机应用与数据库同机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就是个非常典型的例子——它驱动层干了很多重活但没告诉开发者它默认不开启。在实际项目中我建立了一套固定的开发习惯任何批量操作上线前先在一个小数据集上对比“逐条执行、批处理不开重写、批处理开启重写”三者的耗时和数据库负载再决定使用哪种方案任何连接池配置变更都要和批处理任务的并发度、单批次大小做联动评估否则性能优化很可能被连接瓶颈抵消。这些经验不是来自任何一份官方文档而是真金白银踩坑踩出来的。希望这篇文章能让你少走一些弯路。
返回列表