Spring JPA批量处理时查询耗时递增,求性能优化方案
我是Spring Boot新手,正尝试向空表批量插入/更新50k-100k条记录。当批量大小设为10k时,首批10k记录的内循环处理耗时约80秒,但后续批次耗时大幅增长,最后一批耗时超1000秒。我尝试了30k、50k等不同批量大小,大批次总耗时更短但失去了分页的初衷。注意到saveAll执行后平均查询耗时骤增,若移除内循环查询,全程仅需1分钟。请问该现象的原因是什么?如何提升Spring JPA的处理性能?
相关代码
业务处理代码
int offset = 0; int bulkSize = eodFileConfig.getBulkSize(); // sample of 10k setDateTimeFormat(); //Get total record from Temp table long max = extQRMerchantTrxHistService.getTotalRecords(); do { log.debug("[execute] start to write to actual table"); // bulk size represent how many items in a page, offset is the page Page<ExtensionQRMerchantTrxHistEntity> records = extQRMerchantTrxHistService.findRecordsWithPagination(offset, bulkSize); List<TransactionHistoryExtEntity> transactions = new ArrayList<>(); for (ExtensionQRMerchantTrxHistEntity tempEntity : records) { log.debug("Record: {} ", tempEntity); Date a = new Date(); //Query from T_TRXN_DETAIL_EXT Date dateTime = sf.parse(tempEntity.getTransactionDate()); List<TransactionHistoryExtEntity> histories = transactionHistoryInquiryService.retrieveHistoryBasedOnRefNoDateAmt( tempEntity.getTransactionRefNo(), dateTime, tempEntity.getTransactionAmount()); Date b = new Date(); System.out.println("Query Time: " + Math.abs(a.getTime() - b.getTime())); Date c = new Date(); TransactionHistoryExtEntity transaction; if (histories.isEmpty()) { //Insert record transaction = setTransactionHistory(Boolean.TRUE, tempEntity, null); } else { //Update record transaction = setTransactionHistory(Boolean.FALSE, tempEntity, histories.get(0)); } Date d = new Date(); System.out.println("Query Time: " + Math.abs(c.getTime() - d.getTime())); Date e = new Date(); //transactionHistoryExtRepository.saveAndFlush(transaction); Date f = new Date(); System.out.println("Query Time: " + Math.abs(e.getTime() - f.getTime())); //Add to list transactions.add(transaction); } //Save & Update all records transactionHistoryExtRepository.saveAll(transactions); offset++; } while ((long) offset * bulkSize < max);
JPA查询语句
List<TransactionHistoryExtEntity> findTopByReferenceNumberAndTransactionDateAndAmountOrderByTransactionDateDesc( String referenceNo, Date transactionDate, BigDecimal amount);
核心原因
EntityManager缓存膨胀:每次
saveAll执行后,JPA的EntityManager会将所有持久化实体存入一级缓存。随着批次推进,缓存中的实体数量持续累积,后续查询时EntityManager会先在缓存中做全量比对,导致查询耗时呈指数级上升——这就是saveAll后查询变慢的根本原因。Offset分页低效:基于
offset的分页逻辑,在数据量增大后,数据库需要扫描所有前置偏移量的数据才能返回目标批次,后续批次的数据库查询本身就会越来越慢。N+1查询问题:内循环中每条临时记录都单独触发一次数据库查询,50k条记录就会产生50k次数据库请求,叠加缓存膨胀的影响,耗时会被进一步放大。
优化方案
1. 定期清理EntityManager缓存
每个批次处理完成后,手动清除EntityManager缓存,避免缓存堆积:
// 在saveAll之后添加 transactionHistoryExtRepository.flush(); entityManager.clear(); // 需要注入EntityManager实例
2. 替换Offset分页为Keyset分页
用主键或唯一有序字段(如id)替代offset,避免数据库扫描大量前置数据:
- 每次查询记录当前批次的最大
id - 下一批次仅查询
id大于该最大值的记录 - 直到无数据可查为止
示例思路代码:
long lastId = 0; do { List<ExtensionQRMerchantTrxHistEntity> records = extQRMerchantTrxHistService.findRecordsByLastId(lastId, bulkSize); if (records.isEmpty()) break; // 业务处理逻辑... lastId = records.get(records.size()-1).getId(); } while (true);
对应的JPA查询:
List<ExtensionQRMerchantTrxHistEntity> findByIdGreaterThanOrderByIdAsc(Long lastId, Pageable pageable);
3. 批量预查询消除N+1问题
收集当前批次所有查询条件,一次性批量查询出所有匹配实体,再在内存中做映射匹配:
// 收集当前批次所有查询条件 List<Tuple> queryConditions = records.stream() .map(temp -> { try { Date date = sf.parse(temp.getTransactionDate()); return Tuple.of(temp.getTransactionRefNo(), date, temp.getTransactionAmount()); } catch (ParseException e) { throw new RuntimeException(e); } }) .collect(Collectors.toList()); // 批量查询所有匹配实体 List<TransactionHistoryExtEntity> allHistories = transactionHistoryInquiryService.findBatchByRefNoDateAmt(queryConditions); // 内存中建立映射,快速查找 Map<String, TransactionHistoryExtEntity> historyMap = allHistories.stream() .collect(Collectors.toMap( h -> h.getReferenceNumber() + "_" + h.getTransactionDate().getTime() + "_" + h.getAmount(), Function.identity() )); // 循环处理时直接从映射取数据 for (ExtensionQRMerchantTrxHistEntity tempEntity : records) { Date dateTime = sf.parse(tempEntity.getTransactionDate()); String key = tempEntity.getTransactionRefNo() + "_" + dateTime.getTime() + "_" + tempEntity.getTransactionAmount(); TransactionHistoryExtEntity history = historyMap.get(key); // 后续插入/更新逻辑... }
对应的JPA批量查询:
@Query("SELECT h FROM TransactionHistoryExtEntity h WHERE " + "(h.referenceNumber, h.transactionDate, h.amount) IN :conditions") List<TransactionHistoryExtEntity> findBatchByRefNoDateAmt(@Param("conditions") List<Tuple> conditions);
4. 开启JPA批量写入支持
在application.properties中配置Hibernate批量参数(若使用Hibernate作为JPA实现),减少数据库交互次数:
spring.jpa.properties.hibernate.jdbc.batch_size=1000 spring.jpa.properties.hibernate.order_inserts=true spring.jpa.properties.hibernate.order_updates=true spring.jpa.properties.hibernate.batch_versioned_data=true
5. 关闭调试级日志
代码中的log.debug和System.out.println会产生大量IO操作,拖慢大批次处理速度,建议批量处理期间关闭调试日志。
内容的提问来源于stack exchange,提问作者Teck Chuan

