Contents

Hibernate 性能问题简介

1. 简介

您可能已经读到过一些关于 Hibernate 性能不佳的抱怨,或者您自己也可能遇到过其中一些问题。我使用 Hibernate 已经超过 15 年了,我遇到的这些问题已经够多了。

这些年来,我了解到这些问题是可以避免的,并且您可以在日志文件中找到很多问题。在这篇文章中,我想向您展示如何找到并修复其中的 3 个问题。

2. 查找并修复性能问题

2.1. 在生产环境中记录 SQL 语句

第一个性能问题非常容易发现并且经常被忽略。它是生产环境中 SQL 语句的日志记录。

写一些日志语句听起来没什么大不了的,而且有很多应用程序都可以做到这一点。但它非常低效,尤其是通过System.out.println时,如果您将Hibernate 配置中的show_sql参数设置为true ,Hibernate 就会执行此操作:

Hibernate: select 
    order0_.id as id1_2_, 
    order0_.orderNumber as orderNum2_2_, 
    order0_.version as version3_2_ 
  from purchaseOrder order0_
Hibernate: select 
    items0_.order_id as order_id4_0_0_, 
    items0_.id as id1_0_0_, 
    items0_.id as id1_0_1_, 
    items0_.order_id as order_id4_0_1_, 
    items0_.product_id as product_5_0_1_, 
    items0_.quantity as quantity2_0_1_, 
    items0_.version as version3_0_1_ 
  from OrderItem items0_ 
  where items0_.order_id=?
Hibernate: select 
    items0_.order_id as order_id4_0_0_, 
    items0_.id as id1_0_0_, 
    items0_.id as id1_0_1_, 
    items0_.order_id as order_id4_0_1_, 
    items0_.product_id as product_5_0_1_, 
    items0_.quantity as quantity2_0_1_, 
    items0_.version as version3_0_1_ 
  from OrderItem items0_ 
  where items0_.order_id=?
Hibernate: select 
    items0_.order_id as order_id4_0_0_, 
    items0_.id as id1_0_0_, 
    items0_.id as id1_0_1_, 
    items0_.order_id as order_id4_0_1_, 
    items0_.product_id as product_5_0_1_, 
    items0_.quantity as quantity2_0_1_, 
    items0_.version as version3_0_1_ 
  from OrderItem items0_ 
  where items0_.order_id=?

在我的一个项目中,通过将show_sql设置为false,我在几分钟内将性能提高了 20% 。这就是您希望在下一次站立会议中报告的那种成就🙂

很明显您可以如何解决此性能问题。只需打开您的配置(例如您的 persistence.xml 文件)并将show_sql参数设置为false。无论如何,您在生产中不需要这些信息。

但是您可能在开发过程中需要它们。如果不这样做,则使用 2 种不同的 Hibernate 配置(你不应该这样做),你也停用了在那里记录的 SQL 语句。解决方案是为开发和生产使用 2 种不同的日志配置 ,这些配置针对运行时环境的特定要求进行了优化。

开发配置

开发配置应提供尽可能多的有用信息,以便您了解 Hibernate 如何与数据库交互。因此,您至少应该在开发配置中记录生成的 SQL 语句。您可以通过激活org.hibernate.SQL类别的DEBUG消息来完成此操作。如果您还想查看绑定参数的值,则必须将org.hibernate.type.descriptor.sql的日志级别设置为TRACE

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n
log4j.rootLogger=info, stdout
# basic log level for all messages
log4j.logger.org.hibernate=info
# SQL statements and parameters
log4j.logger.org.hibernate.SQL=debug
log4j.logger.org.hibernate.type.descriptor.sql=trace

以下代码片段显示了 Hibernate 使用此日志配置写入的一些示例日志消息。如您所见,您可以获得有关已执行的 SQL 查询以及所有设置和检索的参数值的详细信息:

23:03:22,246 DEBUG SQL:92 - select 
    order0_.id as id1_2_, 
    order0_.orderNumber as orderNum2_2_, 
    order0_.version as version3_2_ 
  from purchaseOrder order0_ 
  where order0_.id=1
23:03:22,254 TRACE BasicExtractor:61 - extracted value ([id1_2_] : [BIGINT]) - [1]
23:03:22,261 TRACE BasicExtractor:61 - extracted value ([orderNum2_2_] : [VARCHAR]) - [order1]
23:03:22,263 TRACE BasicExtractor:61 - extracted value ([version3_2_] : [INTEGER]) - [0]

如果您激活 Hibernate 统计信息,Hibernate 会为您提供更多关于Session 的内部信息。您可以通过将系统属性hibernate.generate_statistics设置为 true 来实现。

但是请只激活您的开发或测试环境的统计信息。收集所有这些信息会减慢您的应用程序,如果您在生产中激活它,您可能会自己造成性能问题。

您可以在以下代码片段中看到一些示例统计信息:

23:04:12,123 INFO StatisticalLoggingSessionEventListener:258 - Session Metrics {
 23793 nanoseconds spent acquiring 1 JDBC connections;
 0 nanoseconds spent releasing 0 JDBC connections;
 394686 nanoseconds spent preparing 4 JDBC statements;
 2528603 nanoseconds spent executing 4 JDBC statements;
 0 nanoseconds spent executing 0 JDBC batches;
 0 nanoseconds spent performing 0 L2C puts;
 0 nanoseconds spent performing 0 L2C hits;
 0 nanoseconds spent performing 0 L2C misses;
 9700599 nanoseconds spent executing 1 flushes (flushing a total of 9 entities and 3 collections);
 42921 nanoseconds spent executing 1 partial-flushes (flushing a total of 0 entities and 0 collections)
}

我经常在日常工作中使用这些统计数据,以便在性能问题出现在生产环境之前发现它们,我可以就此写几篇文章。因此,让我们只关注最重要的部分。

第 2 行到第 5 行显示 Hibernate 在该会话期间使用了多少 JDBC 连接和语句,以及它花费了多少时间。您应该始终查看这些值并将它们与您的期望进行比较。

如果语句比您预期的多很多,您很可能遇到了最常见的性能问题,即 n+1 选择问题。您几乎可以在所有应用程序中找到它,并且它可能会在更大的数据库上产生巨大的性能问题。我将在下一节中更详细地解释这个问题。

第 7 行到第 9 行显示了 Hibernate 如何与二级缓存交互。这是 Hibernate 的 3 个缓存之一,它以独立于会话的方式存储实体。如果您在您的应用程序中使用第 2 级,您应该始终监视这些统计信息以查看 Hibernate 是否从那里获取实体。

生产配置

生产配置应该针对性能进行优化,并避免任何非紧急需要的消息。通常,这意味着您应该只记录错误消息。如果您使用 Log4j,则可以通过以下配置实现:

如果您使用 Log4j,则可以通过以下配置实现:

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n
log4j.rootLogger=info, stdout
# basic log level for all messages
log4j.logger.org.hibernate=error

2.2. N+1 选择问题

正如我已经解释过的,n+1 选择问题是最常见的性能问题。许多开发人员将此问题归咎于 OR-Mapping 概念,他们并非完全错误。但是,如果您了解 Hibernate 如何处理延迟获取的关系,就可以轻松避免它。因此,开发人员也应受到指责,因为避免此类问题是他的责任。因此,让我首先解释为什么存在此问题,然后向您展示一种简单的方法来防止它。如果您已经熟悉 n+1 select 问题,可以直接跳转到解决方案

Hibernate 为实体之间的关系提供了非常方便的映射。您只需要一个具有相关实体类型的属性和一些注解来定义它:

@Entity
@Table(name = "purchaseOrder")
public class Order implements Serializable {
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private Set<OrderItem> items = new HashSet<OrderItem>();
    ...
}

当您现在从数据库中加载一个Order实体时,您只需调用getItems()方法来获取该订单的所有项目。Hibernate 隐藏了从数据库中获取相关OrderItem实体所需的数据库查询。

当您开始使用 Hibernate 时,您可能了解到应该对大多数关系使用FetchType.LAZY,并且它是对多关系的默认设置。如果您使用映射关系的属性,这将告诉 Hibernate 仅获取相关实体。只获取你需要的数据通常是一件好事,但它也需要 Hibernate 执行额外的查询来初始化每个关系。如果您处理实体列表,这可能会导致大量查询,就像我在以下代码片段中所做的那样:

List<Order> orders = em.createQuery("SELECT o FROM Order o").getResultList();
for (Order order : orders) {
    log.info("Order: " + order.getOrderNumber());
    log.info("Number of items: " + order.getItems().size());
}

您可能不会期望这几行代码可以创建数百甚至数千个数据库查询。但如果您将FetchType.LAZY用于与OrderItem实体的关系,它就会这样做:

22:47:30,065 DEBUG SQL:92 - select 
    order0_.id as id1_2_, 
    order0_.orderNumber as orderNum2_2_, 
    order0_.version as version3_2_ 
  from purchaseOrder order0_
22:47:30,136 INFO NamedEntityGraphTest:58 - Order: order1
22:47:30,140 DEBUG SQL:92 - select 
    items0_.order_id as order_id4_0_0_, 
    items0_.id as id1_0_0_, 
    items0_.id as id1_0_1_, 
    items0_.order_id as order_id4_0_1_, 
    items0_.product_id as product_5_0_1_, 
    items0_.quantity as quantity2_0_1_, 
    items0_.version as version3_0_1_ 
  from OrderItem items0_ 
  where items0_.order_id=?
22:47:30,171 INFO NamedEntityGraphTest:59 - Number of items: 2
22:47:30,171 INFO NamedEntityGraphTest:58 - Order: order2
22:47:30,172 DEBUG SQL:92 - select 
    items0_.order_id as order_id4_0_0_, 
    items0_.id as id1_0_0_, 
    items0_.id as id1_0_1_, 
    items0_.order_id as order_id4_0_1_, 
    items0_.product_id as product_5_0_1_, 
    items0_.quantity as quantity2_0_1_, 
    items0_.version as version3_0_1_ 
  from OrderItem items0_ 
  where items0_.order_id=?
22:47:30,174 INFO NamedEntityGraphTest:59 - Number of items: 2
22:47:30,174 INFO NamedEntityGraphTest:58 - Order: order3
22:47:30,174 DEBUG SQL:92 - select 
    items0_.order_id as order_id4_0_0_, 
    items0_.id as id1_0_0_, 
    items0_.id as id1_0_1_, 
    items0_.order_id as order_id4_0_1_, 
    items0_.product_id as product_5_0_1_, 
    items0_.quantity as quantity2_0_1_, 
    items0_.version as version3_0_1_ 
  from OrderItem items0_ 
  where items0_.order_id=?
22:47:30,176 INFO NamedEntityGraphTest:59 - Number of items: 2

Hibernate 执行一个查询以获取所有Order实体,并为 n 个Order实体中的每一个实体执行一个附加查询以初始化orderItem关系。现在您知道为什么这种问题称为 n+1 选择问题以及为什么它会造成巨大的性能问题。

更糟糕的是,如果您没有检查 Hibernate 统计信息,您通常无法在小型测试数据库中识别它。如果测试数据库不包含很多订单,代码片段只需要几十个查询。但是,如果您使用包含数千个数据库的生产数据库,那将完全不同。

我之前说过,您可以轻松避免这些问题。这是真的。当您从数据库中选择Order实体时,您只需初始化 orderItem 关系。

但是,请仅在您在业务代码中使用关系并且不使用FetchType.EAGER始终获取相关实体时才这样做。那只是用另一个性能问题代替了您的 n+1 问题。

使用@NamedEntityGraph*初始化关系*

有几种不同的选项来初始化关系 。我更喜欢使用*@NamedEntityGraph*,这是我在 JPA 2.1 中引入的最喜欢的功能之一。它提供了一种独立于查询的方式来指定 Hibernate 应从数据库中获取的实体图。在下面的代码片段中,您可以看到一个简单图形的示例,它让 Hibernate 急切地获取实体的 items 属性:

@Entity
@Table(name = "purchase_order")
@NamedEntityGraph(
  name = "graph.Order.items", 
  attributeNodes = @NamedAttributeNode("items"))
public class Order implements Serializable {
    ...
}

使用*@NamedEntityGraph注解定义实体图不需要做太多事情。您只需为图形提供一个唯一的名称,并为每个属性提供一个@NamedAttributeNode注解,Hibernate 将立即获取。在此示例中,只有 items 属性映射了Order和多个OrderItem*实体之间的关系。

现在您可以使用实体图来控制抓取行为或特定查询。因此,您必须根据*@NamedEntityGraph定义实例化一个EntityGraph*,并将其作为提示提供给EntityManager.find()方法或您的查询。我在以下代码片段中执行此操作,其中我从数据库中选择 ID 为 1 的Order实体:

EntityGraph graph = this.em.getEntityGraph("graph.Order.items");
Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);
return this.em.find(Order.class, 1L, hints);

Hibernate 使用此信息创建一个 SQL 语句,该语句从数据库中获取Order实体的属性和实体图的属性:

17:34:51,310 DEBUG [org.hibernate.loader.plan.build.spi.LoadPlanTreePrinter] (pool-2-thread-1) 
  LoadPlan(entity=blog.thoughts.on.java.jpa21.entity.graph.model.Order) 
    - Returns 
      - EntityReturnImpl(
          entity=blog.thoughts.on.java.jpa21.entity.graph.model.Order, 
          querySpaceUid=<gen:0>, 
          path=blog.thoughts.on.java.jpa21.entity.graph.model.Order) 
        - CollectionAttributeFetchImpl(
            collection=blog.thoughts.on.java.jpa21.entity.graph.model.Order.items, 
            querySpaceUid=<gen:1>, 
            path=blog.thoughts.on.java.jpa21.entity.graph.model.Order.items)
          - (collection element) CollectionFetchableElementEntityGraph(
              entity=blog.thoughts.on.java.jpa21.entity.graph.model.OrderItem, 
              querySpaceUid=<gen:2>, 
              path=blog.thoughts.on.java.jpa21.entity.graph.model.Order.items.<elements>) 
            - EntityAttributeFetchImpl(entity=blog.thoughts.on.java.jpa21.entity.graph.model.Product,
                querySpaceUid=<gen:3>, 
                path=blog.thoughts.on.java.jpa21.entity.graph.model.Order.items.<elements>.product) 
    - QuerySpaces 
      - EntityQuerySpaceImpl(uid=<gen:0>, entity=blog.thoughts.on.java.jpa21.entity.graph.model.Order)
        - SQL table alias mapping - order0_ 
        - alias suffix - 0_ 
        - suffixed key columns - {id1_2_0_} 
        - JOIN (JoinDefinedByMetadata(items)) : <gen:0> -> <gen:1> 
          - CollectionQuerySpaceImpl(uid=<gen:1>, 
              collection=blog.thoughts.on.java.jpa21.entity.graph.model.Order.items) 
            - SQL table alias mapping - items1_ 
            - alias suffix - 1_ 
            - suffixed key columns - {order_id4_2_1_} 
            - entity-element alias suffix - 2_ 
            - 2_entity-element suffixed key columns - id1_0_2_ 
            - JOIN (JoinDefinedByMetadata(elements)) : <gen:1> -> <gen:2> 
              - EntityQuerySpaceImpl(uid=<gen:2>, 
                  entity=blog.thoughts.on.java.jpa21.entity.graph.model.OrderItem) 
                - SQL table alias mapping - items1_ 
                - alias suffix - 2_ 
                - suffixed key columns - {id1_0_2_}
                - JOIN (JoinDefinedByMetadata(product)) : <gen:2> -> <gen:3> 
                  - EntityQuerySpaceImpl(uid=<gen:3>, 
                      entity=blog.thoughts.on.java.jpa21.entity.graph.model.Product) 
                    - SQL table alias mapping - product2_ 
                    - alias suffix - 3_ 
                    - suffixed key columns - {id1_1_3_}
17:34:51,311 DEBUG [org.hibernate.loader.entity.plan.EntityLoader] (pool-2-thread-1) 
  Static select for entity blog.thoughts.on.java.jpa21.entity.graph.model.Order [NONE:-1]: 
  select order0_.id as id1_2_0_, 
    order0_.orderNumber as orderNum2_2_0_, 
    order0_.version as version3_2_0_, 
    items1_.order_id as order_id4_2_1_, 
    items1_.id as id1_0_1_, 
    items1_.id as id1_0_2_, 
    items1_.order_id as order_id4_0_2_, 
    items1_.product_id as product_5_0_2_, 
    items1_.quantity as quantity2_0_2_, 
    items1_.version as version3_0_2_, 
    product2_.id as id1_1_3_, 
    product2_.name as name2_1_3_, 
    product2_.version as version3_1_3_ 
  from purchase_order order0_ 
    left outer join OrderItem items1_ on order0_.id=items1_.order_id 
    left outer join Product product2_ on items1_.product_id=product2_.id 
  where order0_.id=?

仅初始化一个关系对于博客文章来说已经足够了,但在实际项目中,您很可能希望构建更复杂的图形。所以让我们这样做吧。

当然,您可以提供一组*@NamedAttributeNode注解来获取同一实体的多个属性,并且您可以使用@NamedSubGraph来定义额外级别实体的获取行为。我在以下代码片段中使用它不仅获取所有相关的OrderItem实体,还获取每个OrderItem 的Product*实体:

@Entity
@Table(name = "purchase_order")
@NamedEntityGraph(
  name = "graph.Order.items", 
  attributeNodes = @NamedAttributeNode(value = "items", subgraph = "items"), 
  subgraphs = @NamedSubgraph(name = "items", attributeNodes = @NamedAttributeNode("product")))
public class Order implements Serializable {
    ...
}

如您所见,@NamedSubGraph的定义与*@NamedEntityGraph的定义非常相似。然后,您可以在@NamedAttributeNode*注解中引用此子图来定义此特定属性的获取行为。

这些注解的组合允许您定义复杂的实体图,您可以使用它们来初始化您在用例中使用的所有关系并避免 n+1 选择问题。如果你想在运行时动态指定你的实体图 ,你也可以通过 Java API 来实现。

2.3. 一个一个地更新实体

如果你以面向对象的方式思考,一个一个地更新实体感觉很自然。您只需获取要更新的实体并调用一些 setter 方法来更改它们的属性,就像您对任何其他对象所做的那样。

如果您只更改几个实体,则此方法效果很好。但是,当您使用实体列表时,它会变得非常低效,这是您可以在日志文件中轻松发现的第三个性能问题。您只需查找一堆看起来完全相同的 SQL UPDATE 语句,如您在以下日志文件中所见:

22:58:05,829 DEBUG SQL:92 - select 
  product0_.id as id1_1_, 
  product0_.name as name2_1_, 
  product0_.price as price3_1_, 
  product0_.version as version4_1_ from Product product0_
22:58:05,883 DEBUG SQL:92 - update Product set name=?, price=?, version=? where id=? and version=?
22:58:05,889 DEBUG SQL:92 - update Product set name=?, price=?, version=? where id=? and version=?
22:58:05,891 DEBUG SQL:92 - update Product set name=?, price=?, version=? where id=? and version=?
22:58:05,893 DEBUG SQL:92 - update Product set name=?, price=?, version=? where id=? and version=?
22:58:05,900 DEBUG SQL:92 - update Product set name=?, price=?, version=? where id=? and version=?

数据库记录的关系表示比面向对象的表示更适合这些用例。使用 SQL,您可以只编写一个 SQL 语句来更新您想要更改的所有记录。

如果您使用 JPQL、本机 SQLCriteriaUpdate API ,则可以对 Hibernate 执行相同的操作。所有 3 个都非常相似,所以让我们在此示例中使用 JPQL。

您可以按照您从 SQL 了解的类似方式定义 JPQL UPDATE 语句。您只需在 WHERE 语句中定义要更新的实体、如何更改其属性值以及限制受影响的实体。

您可以在以下代码片段中看到它的示例,其中我将所有产品的价格提高了 10%:

em.createQuery("UPDATE Product p SET p.price = p.price*0.1").executeUpdate();

Hibernate根据JPQL语句创建一条SQL UPDATE语句,发送给执行更新操作的数据库。

很明显,如果您必须更新大量实体,这种方法要快得多。但它也有一个缺点。Hibernate 不知道哪些实体受到更新操作的影响,也不会更新其一级缓存。因此,您应该确保不要在同一 Hibernate 会话中使用 JPQL 语句读取和更新实体,否则您必须将其分离以将其从缓存中删除。