为什么在 Spring Boot 应用程序中加载大量数据时 JPA/Hibernate 会变慢?

GJo*_*nes 3 spring hibernate jpa spring-data spring-boot

当在 RestCall 中使用 Spring Data 时,会有一个 Session,Hibernate 在其中缓存数据。在一个 RestCall 中加载一个新实体会导致 Hibernate 缓存该实体,直到 RestCall 完成。当加载大量数据时,Sprng 会显着减慢,因为 Hibernate 正在积累更多数据。Java 实体没有被收集,因为 Hibernate 仍然有对它的引用。

在以下代码中,前约 100 个实体按预期进行处理。之后,此 RestCall 中的执行速度逐渐减慢。现在的问题是为什么这会减慢 RestCall 代码的执行速度。有足够的 RAM 可用,即使没有,我也希望抛出 OOM。为什么越来越慢?元素数量少于 1500,对于 Java/Spring 来说不应该成为问题。

        public void someMethodCalledFromRestController() {
          List<String> allRelevantNumbers = someService.findNumbers();
          for (String number : allRelevantNumbers) {
              List<Customers> customersGroup = someService.findActiveCustomersByNumber(number); // includes a repository.find(); method
              this.workOnCustomerGroup(customersGroup);
          }
        }

        ....


        public void workOnCustomerGroup(List<Customers> customersGroup) {
        try {
            LocalDateTime startWorkTime = LocalDateTime.now();
            Result result = someService.doSomeWork(customersGroup);
            resultService.createResult(result); // includes repository.save();

//            entityManager.clear();
//            Map.Entry<Object, EntityEntry>[] entries = entityManager.unwrap(SessionImplementor.class).getPersistenceContext().reentrantSafeEntityEntries();

            log.info("Work took " + ChronoUnit.MILLIS.between(startWorkTime, LocalDateTime.now()) +
                    " ms. ");
         catch ( ...
Run Code Online (Sandbox Code Playgroud)

相应的Log显示性能随着时间的推移而下降。

2023-03-27 08:29:37.904  INFO 12084 --- [io-12710-exec-1] Work took 352 ms. 
2023-03-27 08:29:38.338  INFO 12084 --- [io-12710-exec-1] Work took 384 ms. 
2023-03-27 08:29:38.748  INFO 12084 --- [io-12710-exec-1] Work took 357 ms. 
2023-03-27 08:29:39.133  INFO 12084 --- [io-12710-exec-1] Work took 354 ms. 
....
2023-03-27 08:37:54.978  INFO 12084 --- [io-12710-exec-1] Work took 2685 ms.
2023-03-27 08:37:57.695  INFO 12084 --- [io-12710-exec-1] Work took 2699 ms.
2023-03-27 08:38:00.472  INFO 12084 --- [io-12710-exec-1] Work took 2745 ms.
2023-03-27 08:38:03.211  INFO 12084 --- [io-12710-exec-1] Work took 2726 ms.
Run Code Online (Sandbox Code Playgroud)

使用entityManager.clear(); 解决了性能问题。下面的行显示 Hibernate Context 的大小在没有entityManager.clear(); 的情况下不断增长。此示例中明确没有使用任何事务。

Oli*_*ohm 9

这个问题需要稍微更详细的解释,因为您遇到的情况既受到 JPA 工作方式的影响,也受到 Spring Boot 用于适应常见 Web 应用程序用例的默认配置的影响。

语境

Spring Boot 的 JPA 默认配置设置假设通常可以通过加载两到一百个实体来响应 Web 请求,或者可能触发一些状态转换并因此呈现某种表示形式(JSON / HTML)。它注册一个OpenEntityManagerInViewInterceptor打开EntityManager由所有存储库交互使用的单个实例的实例。这允许在请求处理的所有阶段中,可以按其类型公开的方式访问实体。这些实体的某些关系可能由惰性关系支持,这些关系在访问时需要额外的数据库查找。选择默认配置后,结果呈现(用于 JSON 的 Jackson 或用于 HTML 的 Thymeleaf 之类的东西)将能够使用 JPA 托管实体,而不会将这些关系的惰性本质暴露给开发人员。这为他们提供了便利,无需为标准用例处理它。

问题

偏离该模型的用例(例如您的用例)实际上会从不同的权衡中受益,因为单个EntityManager实例将成为更昂贵的操作的瓶颈。从记忆的角度和计算的角度来看都是如此。我认为延长的响应时间主要源于 Hibernate 在EntityManager.

潜在的解决方案

坚持EntityManager.clear()

虽然不一定是最干净的选择,但简单地坚持EntityManager.clear()可能是一个合理的解决方法,因为所有其他选择都有更广泛的影响,而不仅仅是需要改进的代码。

为昂贵的操作切换到不同的持久性方法

如果您仍然想使用 Spring Boot 的默认方法,您可以研究是否有方法可以针对此特定操作使用替代数据访问处理方法。您能否避免迭代初始结果并触发额外的查询?您能否将结果读入(非托管)DTO 而不是(托管)实体?使用 JPA 本机查询,甚至是 jOOQ 或 JDBC 等较低级别的 API 是否有意义?

限制范围EntityManager

处理这个问题的一个简单方法是通过EntityManager将应用程序配置属性设置为 来完全收回创建spring.jpa.open-in-view的控制权false。这将导致EntityManager创建与事务边界的声明相关联(通常通过@Transactional)。请注意,这意味着您在确定所有数据访问范围时必须考虑这一点,包括符合上述标准请求模型的其他请求。一个常见的策略是用 注释控制器或服务。通过显式设置-flag 来配置 Hibernate 以完全跳过脏检查,可以针对只读场景进一步优化。@TransactionalreadOnlytrue

这样做的一个显着缺点是,在此类事务边界之外直接或间接触发 JPA 惰性关系解析的任何响应都会失败LazyInitializationException(除非该关系已在事务边界内显式初始化)。这种方法带来了额外的复杂性和显着的不透明性(“在哪些情况下我需要初始化哪种关系?”)是这种方法不是 Spring Boot 应用程序默认方法的主要原因。

您还可以尝试通过禁用 的默认注册来找到中间立场OSIVI,并注册来WebMvcConfigurer装饰OSIVI拦截器,MappedInterceptor以捕获适合您的情况的包含和排除模式。

对 API 进行建模以适应长时间运行的操作

最后一个选择是设计您的 HTTP 资源和应用程序状态以接受原始请求,将请求转换为可引用的应用程序状态(即计算标识符),然后返回202 Accepted该请求。然后,您可以异步触发长时间运行的功能,最终将结果存储在最初计算的标识符下。然后,客户端将发出第二个请求(通过原始请求的响应标头公布的 URI Location)来获取昂贵的计算结果。