为啥 Spring 在配置时并不总是使用批量插入/更新?

Posted

技术标签:

【中文标题】为啥 Spring 在配置时并不总是使用批量插入/更新?【英文标题】:Why does Spring not always use batch insert/update when configured?为什么 Spring 在配置时并不总是使用批量插入/更新? 【发布时间】:2020-02-25 22:08:28 【问题描述】:

在我基于 Spring JpaRepository 的项目中,我有一个方法总是从调用 saveAll(List list) 的方法中一一加载记录...当我使用 List 时,我在其中创建对象并将它们添加到列表中.下面的代码大约需要 5 分钟...


@Transactional(readOnly = false)
public List<Data> saveAll(List<Data> dataList) throws Exception 
 try 
        for(Data d : dataList)

            if(d.getCol04() == null)
                d.setCol04("TEST");
            else
                d.setCol04(null);

            log.info(new Gson().toJson(d));

        

        return dataRepository.saveAll(dataList);

    catch (Exception e) 
        log.error("Unexpected error.", e);
        return null;
    


2019-10-30 09:07:11 调试 org.hibernate.loader.Loader - 结果集行:0 2019-10-30 09:07:11 调试 org.hibernate.loader.Loader - 结果行:EntityKey [ com.test.Data#component[batchSeq,batchId]batchSeq=866, batchId=6012113450] 2019-10-30 09:07:11 调试 o.h.engine.internal.TwoPhaseLoad - 解决关联 [com.test.Data#component[batchSeq,batchId]batchSeq=866, 批号=6012113450] 2019-10-30 09:07:11 调试 o.h.engine.internal.TwoPhaseLoad - 完成实体化 [com.test.Data#component[batchSeq,batchId]batchSeq=866, 批号=6012113450] 2019-10-30 09:07:11 调试 org.hibernate.loader.Loader - 完成实体加载 2019-10-30 09:07:11 调试 org.hibernate.loader.Loader - 加载实体:[com.test.Data#component[batchSeq,batchId]batchSeq=867, 批号=6012113450] 2019-10-30 09:07:11 调试 org.hibernate.SQL - 选择 datae0_.batch_seq 作为 batch_seq1_0_0_, datae0_.batch_id 为 batch_id2_0_0_, datae0_.col01 作为 col3_0_0_, datae0_.col02 作为 col4_0_0_, datae0_.col03 作为 col5_0_0_, datae0_.col04 作为 col6_0_0_, datae0_.col05 作为 col7_0_0_, datae0_.col06 作为 col8_0_0_, datae0_.col07 作为 col9_0_0_, datae0_.col08 作为 col10_0_0_, datae0_.col09 作为 col11_0_0_, datae0_.col10 作为 col12_0_0_, datae0_.group_id 作为 group_id13_0_0_, datae0_.parent_trans_id 作为 parent_trans_id14_0_0_, datae0_.result_message 作为 result_message15_0_0_, datae0_.result_status 作为 result_status16_0_0_, datae0_.tn 作为 tn17_0_0_, datae0_.tn_quantity_assigned 为 tn_quantity_assig18_0_0_, datae0_.tn_quantity_requested 为 tn_quantity_reque19_0_0_, datae0_.xml_data 作为 xml_data20_0_0_ 从 owner.batch_data datae0_ 在哪里 datae0_.batch_seq=? 和 datae0_.batch_id=? 2019-10-30 09:07:12 调试 org.hibernate.loader.Loader - 结果集行:0 2019-10-30 09:07:12 调试 org.hibernate.loader.Loader - 结果行:EntityKey[com.test.Data#component[batchSeq,batchId]batchSeq=867, 批号=6012113450] 2019-10-30 09:07:12 调试 o.h.engine.internal.TwoPhaseLoad - 解决关联 [com.test.Data#component[batchSeq,batchId]batchSeq=867, 批号=6012113450] 2019-10-30 09:07:12 调试 o.h.engine.internal.TwoPhaseLoad - 完成实体化实体 [com.test.Data#component[batchSeq,batchId]batchSeq=867, 批号=6012113450] 2019-10-30 09:07:12 调试 org.hibernate.loader.Loader - 完成实体加载 2019-10-30 09:07:12 调试 org.hibernate.loader.Loader - 加载实体:[com.test.Data#component[batchSeq,batchId]batchSeq=868, 批号=6012113450] 2019-10-30 09:07:12 调试 org.hibernate.SQL - . . . 2019-10-30 09:10:34 调试 o.h.e.j.b.internal.AbstractBatchImpl - 重用批处理语句 2019-10-30 09:10:34 调试 org.hibernate.SQL - 更新 owner.batch_data 放 col01=?, col02=?, col03=?, col04=?, col05=?, col06=?, col07=?, col08=?, col09=?, col10=?, group_id=?, parent_trans_id=?, result_message=?, 结果状态=?, tn=?, tn_quantity_assigned=?, tn_quantity_requested=?, xml_data=? 在哪里 批处理序列=? 和 batch_id=? 2019-10-30 09:10:34 调试 o.h.e.j.batch.internal.BatchingBatch - 执行批处理大小:500 2019-10-30 09:10:36 调试 o.s.orm.jpa.JpaTransactionManager - 关闭 JPA EntityManager [SessionImpl(2081270212)] 之后 交易 2019-10-30 09:10:36 信息 o.h.e.i.StatisticalLoggingSessionEventListener - 会话指标 获取 1 个 JDBC 连接花费了 77841799 纳秒; 0 纳秒释放 0 个 JDBC 连接; 准备 1501 条 JDBC 语句花费了 89273975 纳秒; 执行 1500 条 JDBC 语句花费了 250886005163 纳秒; 执行 3 个 JDBC 批处理花费了 4812997147 纳秒; 执行 0 个 L2C put 花费了 0 纳秒; 执行 0 个 L2C 命中花费了 0 纳秒; 0 纳秒执行 0 次 L2C 未命中; 执行 1 次刷新花费了 5462588455 纳秒(共刷新 1500 个实体和 0 个集合); 0 纳秒执行 0 次部分刷新(共刷新 0 个实体和 0 个集合) 2019-10-30 09:10:36 调试 o.h.e.j.internal.JdbcCoordinatorImpl - HHH000420:关闭未发布的批次 2019-10-30 09:10:36 DEBUG o.s.orm.jpa.JpaTransactionManager - 使用名称创建新事务 [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findById]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,只读 2019-10-30 09:10:36 调试 o.s.orm.jpa.JpaTransactionManager - 为 JPA 打开新的 EntityManager [SessionImpl(1953492703)] 交易 2019-10-30 09:10:36 调试 o.s.jdbc.datasource.DataSourceUtils - 设置 JDBC 连接 [HikariProxyConnection@113673215 包装 oracle.jdbc.driver.T4CConnection@5f71a000] 只读 2019-10-30 09:10:36 DEBUG o.h.e.t.internal.TransactionImpl - 关于 TransactionImpl 创建, JpaCompliance#isJpaTransactionComplianceEnabled == false 2019-10-30 09:10:36 调试 o.h.e.t.internal.TransactionImpl - 开始

当我查询 findAll(List id) 并使用返回的列表作为 saveAll 的参数时,SaveAll(List list) 会批量加载。下面的代码大约需要 8 秒。


//Working method

@Transactional(readOnly = false)
public List<Data> saveAll() throws Exception 

    try 
        List<Data> dataList2 = dataRepository.findAllByBatchId(6000000L);

        for(Data d : dataList2)

            if(d.getCol04() == null)
                d.setCol04("TEST");
            else
                d.setCol04(null);

            log.info(new Gson().toJson(d));

        

        return dataRepository.saveAll(dataList2);

    catch (Exception e) 
        log.error("Unexpected error.", e);
        return null;
    


。 . . 2019-10-30 09:00:25 调试 o.h.e.j.batch.internal.BatchingBatch - 执行批量大小:500 2019-10-30 09:00:27 调试 o.s.orm.jpa.JpaTransactionManager - 关闭 JPA EntityManager [SessionImpl(2038854767)] 之后 交易 2019-10-30 09:00:27 信息 o.h.e.i.StatisticalLoggingSessionEventListener - 会话指标 获取 1 个 JDBC 连接花费了 89300419 纳秒; 0 纳秒释放 0 个 JDBC 连接; 准备 2 条 JDBC 语句花费了 1037028 纳秒; 执行 1 条 JDBC 语句花费了 330435552 纳秒; 执行 3 个 JDBC 批处理花费了 6423148647 纳秒; 执行 0 个 L2C put 花费了 0 纳秒; 执行 0 个 L2C 命中花费了 0 纳秒; 0 纳秒执行 0 次 L2C 未命中; 7100764566 纳秒执行 1 次刷新(共刷新 1500 个实体和 0 个集合); 执行 1 次部分刷新花费了 2082345 纳秒(共刷新 0 个实体和 0 个集合) 2019-10-30 09:00:27 调试 o.h.e.j.internal.JdbcCoordinatorImpl - HHH000420:关闭未发布的批次 2019-10-30 09:00:27 调试 o.s.w.s.m.m.a.HttpEntityMethodProcessor - 使用 'application/json',给定 [/] 并支持 [application/json] 2019-10-30 09:00:27 调试 o.s.w.s.m.m.a.HttpEntityMethodProcessor - 写入 [""data":0"] 2019-10-30 09:00:27 调试 o.s.o.j.s.OpenEntityManagerInViewInterceptor - 关闭 JPA OpenEntityManagerInViewInterceptor中的EntityManager

任何想法为什么会发生这种情况?

【问题讨论】:

我很难确定真正的问题是什么? 使用基本相同的方法,一种耗时8秒,批量更新,另一种耗时5分钟,一条一条更新记录。唯一的区别是,在慢速更新中,我传入了我在代码中构建的对象列表,而在更快的方法中,我从数据库中查询列表。 他们都使用批量更新saveAll()? 他们都使用 saveAll(List list)。 Spring 方法签名是 List saveAll(Iterableentities)。当我打开统计数据时,运行速度快的日志生成的日志要少得多。 这些方法都没有一个一个地更新记录...它们每个都调用存储库saveAll()方法...。 【参考方案1】:

这可能是因为脏检查功能,在第一种方法中,当您在保存数据之前保存 dataList 时,休眠可能会加载内存中的所有数据以检查哪些数据已被修改。这可能是需要时间和如果数据(实体)进一步与其他实体相关联,那么这会按时加起来。与第二种方法一样,在保存到数据库之前预加载了 dataList2,因此在保存时不进行任何解析。为了测试这个理论,你可能会急切地加载所有数据(连同关联的实体)然后保存,如果它与第二种方法花费的时间相同,那么我们可能会得出结论。也请参考此链接https://forum.hibernate.org/viewtopic.php?f=1&t=994162&view=previous,它可能会有所帮助

【讨论】:

现在按预期工作!

以上是关于为啥 Spring 在配置时并不总是使用批量插入/更新?的主要内容,如果未能解决你的问题,请参考以下文章

如何为批量插入配置spring boot和data jpa

Spring Boot 集成 Druid 批量插入数据和效率监控配置

使用 Spring Boot 和 Spring Data JPA 批量插入不起作用

为啥 MySQL 在这里并不总是使用索引合并?

为啥 Spring 的 jdbcTemplate.batchUpdate() 这么慢?

Spring Boot 集成 Druid 批量插入数据和效率监控配置