You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);

原因分析与优化方案

核心原因

  1. EntityManager缓存膨胀:每次saveAll执行后,JPA的EntityManager会将所有持久化实体存入一级缓存。随着批次推进,缓存中的实体数量持续累积,后续查询时EntityManager会先在缓存中做全量比对,导致查询耗时呈指数级上升——这就是saveAll后查询变慢的根本原因。

  2. Offset分页低效:基于offset的分页逻辑,在数据量增大后,数据库需要扫描所有前置偏移量的数据才能返回目标批次,后续批次的数据库查询本身就会越来越慢。

  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 04:55:23