«1. Введение
В этом кратком руководстве мы рассмотрим кэш планов запросов, предоставляемый Hibernate, и его влияние на производительность.
2. Кэш плана запроса
Каждый запрос JPQL или запрос Criteria анализируется в абстрактное синтаксическое дерево (AST) перед выполнением, чтобы Hibernate мог сгенерировать инструкцию SQL. Поскольку компиляция запросов требует времени, Hibernate предоставляет QueryPlanCache для повышения производительности.
Для нативных запросов Hibernate извлекает информацию об именованных параметрах и типе возврата запроса и сохраняет ее в ParameterMetadata.
При каждом выполнении Hibernate сначала проверяет кеш плана, и только если план недоступен, он генерирует новый план и сохраняет план выполнения в кеше для дальнейшего использования.
3. Конфигурация
Конфигурация кэша планов запросов управляется следующими свойствами:
-
hibernate.query.plan_cache_max_size — управляет максимальным количеством записей в кэше планов (по умолчанию 2048) hibernate.query .plan_parameter_metadata_max_size — управляет количеством экземпляров ParameterMetadata в кеше (по умолчанию 128)
Таким образом, если наше приложение выполняет больше запросов, чем размер кеша плана запросов, Hibernate придется тратить дополнительное время на компиляцию запросов. Следовательно, общее время выполнения запроса увеличится.
4. Настройка тестового примера
Как говорят в отрасли, когда речь идет о производительности, никогда не верьте заявлениям. Итак, давайте проверим, как меняется время компиляции запроса при изменении настроек кеша.
4.1. Классы сущностей, задействованные в тестировании
Давайте начнем с рассмотрения сущностей, которые мы будем использовать в нашем примере, DeptEmployee и Department:
@Entity
public class DeptEmployee {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String employeeNumber;
private String title;
private String name;
@ManyToOne
private Department department;
// standard getters and setters
}
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String name;
@OneToMany(mappedBy="department")
private List<DeptEmployee> employees;
// standard getters and setters
}
4.2. Запросы Hibernate, задействованные в тесте
Нас интересует только измерение общего времени компиляции запроса, поэтому мы можем выбрать любую комбинацию действительных запросов HQL для нашего теста.
-
В этой статье мы будем использовать следующие три запроса:
session.createQuery("SELECT e FROM DeptEmployee e " +
"JOIN e.department WHERE e.department.name = :deptName")
.setMaxResults(30)
.setHint(QueryHints.HINT_FETCH_SIZE, 30);
-
findEmployeesByDepartmentName
session.createQuery("SELECT e FROM DeptEmployee e " +
"WHERE e.title = :designation")
.setHint(QueryHints.SPEC_HINT_TIMEOUT, 1000);
-
findEmployeesByDesignation
session.createQuery("SELECT e.department FROM DeptEmployee e " +
"JOIN e.department WHERE e.employeeNumber = :empId");
findDepartmentOfAnEmployee
5. Измерение влияния на производительность
5.1. Настройка кода бенчмарка
@State(Scope.Thread)
public static class QueryPlanCacheBenchMarkState {
@Param({"1", "2", "3"})
public int planCacheSize;
public Session session;
@Setup
public void stateSetup() throws IOException {
session = initSession(planCacheSize);
}
private Session initSession(int planCacheSize) throws IOException {
Properties properties = HibernateUtil.getProperties();
properties.put("hibernate.query.plan_cache_max_size", planCacheSize);
properties.put("hibernate.query.plan_parameter_metadata_max_size", planCacheSize);
SessionFactory sessionFactory = HibernateUtil.getSessionFactoryByProperties(properties);
return sessionFactory.openSession();
}
//teardown...
}
Мы будем варьировать размер кеша от одного до трех — после этого все три наших запроса уже будут в кеше. Поэтому увеличивать его дальше нет смысла:
5.2. Тестируемый код
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(1)
@Warmup(iterations = 2)
@Measurement(iterations = 5)
public void givenQueryPlanCacheSize_thenCompileQueries(
QueryPlanCacheBenchMarkState state, Blackhole blackhole) {
Query query1 = findEmployeesByDepartmentNameQuery(state.session);
Query query2 = findEmployeesByDesignationQuery(state.session);
Query query3 = findDepartmentOfAnEmployeeQuery(state.session);
blackhole.consume(query1);
blackhole.consume(query2);
blackhole.consume(query3);
}
Далее давайте посмотрим на код теста, который используется для измерения среднего времени, затрачиваемого Hibernate на компиляцию запросов:
Обратите внимание, что мы использовали JMH для написания нашего теста.
5.3. Результаты тестов
Теперь давайте визуализируем график времени компиляции и размера кэша, который мы подготовили, запустив приведенный выше тест: время компиляции.
Для кэша размером один среднее время компиляции является максимальным и составляет 709 микросекунд, затем оно уменьшается до 409 микросекунд для размера кэша два и до 0,637 микросекунд для размера кэша три.
6. Использование статистики Hibernate
Для наблюдения за эффективностью кэша плана запросов Hibernate предоставляет следующие методы через интерфейс статистики:
-
getQueryPlanCacheHitCount getQueryPlanCacheMissCount count низкий, то большинство запросов обслуживаются из самого кеша, а не компилируются снова и снова.
7. Заключение
В этой статье мы узнали, что такое кеш планов запросов в Hibernate и как он влияет на общую производительность приложения. В целом, мы должны попытаться сохранить размер кеша плана запросов в соответствии с количеством запросов, выполняемых в приложении.
Как всегда, исходный код этого руководства доступен на GitHub.
«