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 для нашего теста.
Для целей этой статьи мы будем использовать следующие три запроса:
найтиEmployeesByDepartmentName
session.createQuery("SELECT e FROM DeptEmployee e " +
"JOIN e.department WHERE e.department.name = :deptName")
.setMaxResults(30)
.setHint(QueryHints.HINT_FETCH_SIZE, 30);
findEmployeesByDesignation
session.createQuery("SELECT e FROM DeptEmployee e " +
"WHERE e.title = :designation")
.setHint(QueryHints.SPEC_HINT_TIMEOUT, 1000);
найтиDepartmentOfAnEmployee
session.createQuery("SELECT e.department FROM DeptEmployee e " +
"JOIN e.department WHERE e.employeeNumber = :empId");
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. Тестируемый код
Далее давайте взглянем на тестовый код, используемый для измерения среднего времени, затрачиваемого Hibernate на компиляцию запросов:
@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);
}
Обратите внимание, что мы использовали JMH для написания нашего теста.
5.3. Сравнительные результаты
Теперь давайте визуализируем график зависимости времени компиляции от размера кэша, который мы подготовили, запустив приведенный выше тест:
Как мы можем ясно видеть на графике, увеличение количества запросов, которые Hibernate может кэшировать, соответственно сокращает время компиляции .
Для размера кеша, равного одному, среднее время компиляции является максимальным и составляет 709 микросекунд, затем оно уменьшается до 409 микросекунд для размера кеша, равного двум, и до 0,637 мкс для размера кеша, равного трем.
6. Использование статистики гибернации
Для мониторинга эффективности кеша планов запросов Hibernate предоставляет следующие методы через интерфейс статистики :
getQueryPlanCacheHitCount
getQueryPlanCacheMissCount
Таким образом, если количество попаданий велико, а количество промахов мало, то большинство запросов обслуживаются из самого кеша, а не компилируются снова и снова.
7. Заключение
В этой статье мы узнали, что такое кеш планов запросов в Hibernate и как он может повлиять на общую производительность приложения. В целом, мы должны попытаться сохранить размер кеша плана запросов в соответствии с количеством запросов, выполняемых в приложении.
Как всегда, исходный код этого руководства доступен на GitHub .