Рубрики
Без рубрики

3 Общие проблемы с производительностью Гибернации и как их найти в файле Журнала

В файле журнала можно найти множество проблем с производительностью гибернации. В этой статье я покажу вам 3 общих и как вы можете их исправить.

Автор оригинала: thorben.janssen.

1. введение

Вы, вероятно, читали некоторые жалобы на плохую производительность гибернации или, возможно, сами боролись с некоторыми из них. Я использую Hibernate уже более 15 лет, и я столкнулся с более чем достаточным количеством этих проблем.

За эти годы я узнал, что этих проблем можно избежать, и что вы можете найти их много в своем файле журнала. В этом посте я хочу показать вам, как вы можете найти и исправить 3 из них.

2. Найдите и устраните проблемы с производительностью

2.1. Регистрируйте SQL-операторы в производстве

Первая проблема с производительностью чрезвычайно легко обнаруживается и часто игнорируется. Это ведение журнала операторов SQL в производственной среде.

Написание некоторых операторов журнала не кажется чем-то особенным, и есть много приложений, которые делают именно это. Но это крайне неэффективно, особенно через System.out.println , как это делает Hibernate, если вы установите параметр show_sql в конфигурации Hibernate в true :

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=?

В одном из моих проектов я улучшил производительность на 20% в течение нескольких минут, установив show_sql в false . Это то достижение, о котором вы хотели бы сообщить на следующей встрече. 🙂

Совершенно очевидно, как вы можете исправить эту проблему с производительностью. Просто откройте свою конфигурацию (например, persistence.xml файл) и установите для параметра show_sql значение false . В любом случае вам не нужна эта информация в производстве.

Но они могут понадобиться вам во время разработки. Если вы этого не сделаете, вы используете 2 различные конфигурации гибернации (чего делать не следует), вы также отключили ведение журнала операторов SQL. Решение для этого заключается в использовании 2 различных конфигураций log для разработки и производства, которые оптимизированы для конкретных требований среды выполнения.

Конфигурация разработки

Конфигурация разработки должна содержать как можно больше полезной информации, чтобы вы могли видеть, как Hibernate взаимодействует с базой данных. Поэтому вы должны, по крайней мере, зарегистрировать сгенерированные инструкции SQL в конфигурации разработки. Вы можете сделать это, активировав DEBUG сообщение для org.hibernate.SQL категория. Если вы также хотите видеть значения параметров привязки, вам необходимо установить уровень журнала 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. Вы можете сделать это, установив системное свойство 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 показано, сколько соединений JDBC и операторов Hibernate использовалось во время этого сеанса и сколько времени он потратил на это. Вы всегда должны смотреть на эти ценности и сравнивать их с вашими ожиданиями.

Если операторов намного больше, чем вы ожидали , у вас, скорее всего, есть наиболее распространенная проблема с производительностью , а затем+1 выберите проблему. Вы можете найти его почти во всех приложениях, и это может создать огромные проблемы с производительностью в большой базе данных. Я объясню эту проблему более подробно в следующем разделе.

Строки с 7 по 9 показывают, как Hibernate взаимодействовал с кэшем 2-го уровня. Это один из 3 кэшей Hibernate , и он хранит сущности независимым от сеанса способом. Если вы используете 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 является наиболее распространенной проблемой производительности. Многие разработчики обвиняют концепцию отображения ИЛИ в этой проблеме, и они не совсем ошибаются. Но вы можете легко избежать этого, если поймете, как Hibernate относится к лениво выбранным отношениям. Поэтому разработчик также виноват, потому что он несет ответственность за то, чтобы избежать подобных проблем. Поэтому позвольте мне сначала объяснить, почему существует эта проблема, а затем показать вам простой способ ее предотвратить. Если вы уже знакомы с проблемами выбора n+1, вы можете перейти непосредственно к решению .

Hibernate обеспечивает очень удобное сопоставление отношений между сущностями. Вам просто нужен атрибут с типом связанной сущности и несколькими аннотациями, чтобы определить его:

@Entity
@Table(name = "purchaseOrder")
public class Order implements Serializable {

    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private Set items = new HashSet();

    ...
}

Теперь, когда вы загружаете объект Order из базы данных, вам просто нужно вызвать метод GetItems () , чтобы получить все элементы этого заказа. Hibernate скрывает необходимые запросы к базе данных для получения связанных OrderItem сущностей из базы данных.

Когда вы начали с Hibernate, вы, вероятно, узнали, что вам следует использовать FetchType.LAZY для большинства отношений и что это значение по умолчанию для отношений “ко многим”. Это говорит Hibernate извлекать связанные сущности только в том случае, если вы используете атрибут, который отображает связь. Извлечение только тех данных, которые вам нужны, – это хорошая вещь в целом, но она также требует, чтобы Hibernate выполнял дополнительный запрос для инициализации каждой связи. Это может привести к огромному количеству запросов, если вы работаете со списком сущностей, как я делаю в следующем фрагменте кода:

List 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 и почему она может создавать огромные проблемы с производительностью.

Что еще хуже, так это то, что вы часто не распознаете его в небольшой тестовой базе данных, если вы не проверили статистику гибернации. Фрагмент кода требует всего нескольких десятков запросов, если тестовая база данных не содержит большого количества заказов. Но это будет совершенно по-другому, если вы используете свою производительную базу данных, которая содержит несколько тысяч из них.

Я уже говорил ранее, что вы можете легко избежать этих проблем. И это правда. Вам просто нужно инициализировать отношение OrderItem при выборе объектов Order из базы данных.

Но, пожалуйста, делайте это только в том случае, если вы используете отношения в своем бизнес-коде и не используете FetchType.СТРЕМИТСЯ всегда извлекать связанные сущности. Это просто заменяет вашу проблему 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 .

Теперь вы можете использовать граф сущностей для управления поведением выборки или конкретным запросом. Поэтому вам необходимо создать экземпляр Entity Graph на основе определения @NamedEntityGraph и предоставить его в качестве подсказки методу EntityManager.find() или вашему запросу. Я делаю это в следующем фрагменте кода, где я выбираю объект Order с идентификатором 1 из базы данных:

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=, 
          path=blog.thoughts.on.java.jpa21.entity.graph.model.Order) 
        - CollectionAttributeFetchImpl(
            collection=blog.thoughts.on.java.jpa21.entity.graph.model.Order.items, 
            querySpaceUid=, 
            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=, 
              path=blog.thoughts.on.java.jpa21.entity.graph.model.Order.items.) 
            - EntityAttributeFetchImpl(entity=blog.thoughts.on.java.jpa21.entity.graph.model.Product,
                querySpaceUid=, 
                path=blog.thoughts.on.java.jpa21.entity.graph.model.Order.items..product) 
    - QuerySpaces 
      - EntityQuerySpaceImpl(uid=, 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)) :  ->  
          - CollectionQuerySpaceImpl(uid=, 
              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)) :  ->  
              - EntityQuerySpaceImpl(uid=, 
                  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)) :  ->  
                  - EntityQuerySpaceImpl(uid=, 
                      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
сущности, но и Product
сущность для каждого OrderItem:

@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 {

    ...
}

Как вы можете видеть, определение a @NamedSubGraph очень похоже на определение a @NamedEntityGraph . Затем вы можете сослаться на этот подграф в аннотации @NamedAttributeNode , чтобы определить поведение выборки для этого конкретного атрибута.

Комбинация этих аннотаций позволяет определить сложные графики сущностей, которые можно использовать для инициализации всех отношений, используемых в вашем случае использования, и избежать проблем с выбором n+1. Если вы хотите указать свой граф сущностей динамически во время выполнения , вы можете сделать это также с помощью Java API.

2.3. Обновляйте Объекты Один за другим

Обновление сущностей одна за другой кажется очень естественным, если вы мыслите объектно-ориентированным образом. Вы просто получаете объекты, которые хотите обновить, и вызываете несколько методов настройки, чтобы изменить их атрибуты, как вы делаете это с любым другим объектом.

Этот подход отлично работает, если вы измените только несколько сущностей. Но это становится очень неэффективным, когда вы работаете со списком сущностей, и это третья проблема производительности, которую вы можете легко обнаружить в своем файле журнала. Вам просто нужно найти кучу инструкций обновления SQL, которые выглядят совершенно одинаково, как вы можете видеть в следующем файле журнала:

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, которая обновляет все записи, которые вы хотите изменить.

Вы можете сделать то же самое с Hibernate, если используете JPQL, собственный SQL или API обновления критериев|/. Все 3 очень похожи, поэтому давайте использовать JPQL в этом примере.

Вы можете определить оператор обновления JPQL таким же образом, как вы знаете его из SQL. Вы просто определяете, какую сущность вы хотите обновить, как изменить значения ее атрибутов и ограничить затронутые сущности в операторе WHERE. Вы можете увидеть пример этого в следующем фрагменте кода, где я увеличиваю цену всех продуктов на 10%:

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

Hibernate создает инструкцию обновления SQL на основе инструкции JPQL и отправляет ее в базу данных, которая выполняет операцию обновления.

Совершенно очевидно, что этот подход намного быстрее, если вам нужно обновить огромное количество объектов. Но у него есть и недостаток. Hibernate не знает, на какие объекты влияет операция обновления, и не обновляет свой кэш 1-го уровня. Поэтому вам следует убедиться, что вы не читаете и не обновляете сущность с помощью инструкции JPQL в течение одного сеанса гибернации, иначе вам придется отсоединить ее, чтобы удалить из кэша.

3. Резюме

В этом посте я показал вам 3 Проблемы с производительностью гибернации, которые вы можете найти в своих файлах журналов.

2 из них были вызваны огромным количеством SQL-операторов. Это распространенная причина проблем с производительностью, если вы работаете с Hibernate. Hibernate скрывает доступ к базе данных за своим API, и это часто затрудняет угадывание фактического количества операторов SQL. Поэтому вы всегда должны проверять выполняемые операторы SQL при внесении изменений в уровень сохраняемости.