Contents

Hibernate 中的查询计划缓存简介

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. 测试中涉及的实体类

让我们先看看我们将在示例中使用的实体DeptEmployeeDepartment

@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 查询的任意组合。

出于本文的目的,我们将使用以下三个查询:

  • findEmployeesByDepartment
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);
  • findDepartmentOfAnEmployee
session.createQuery("SELECT e.department FROM DeptEmployee e " +
  "JOIN e.department WHERE e.employeeNumber = :empId");

5. 衡量绩效影响

5.1. 基准代码设置

我们将缓存大小从 1 更改为 3  - 之后,我们的所有三个查询都将已经在缓存中。因此,进一步增加它没有意义:

@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. 基准测试结果

现在,让我们通过运行上述基准测试来可视化编译时间与缓存大小的关系图:

/uploads/hibernate_query_plan_cache/1.png

正如我们在图中清楚地看到的那样,增加 Hibernate 允许缓存的查询数量会减少编译时间

对于大小为 1 的缓存,平均编译时间最高,为 709 微秒,然后对于大小为 2 的缓存减少到 409 微秒,对于大小为 3 的缓存,一直到 0.637 微秒。

6. 使用休眠统计

为了监控查询计划缓存的有效性,Hibernate 通过 *Statistics *接口公开了以下方法:

  • getQueryPlanCacheHitCount
  • getQueryPlanCacheMissCount

因此,如果命中计数高而未命中计数低,则大多数查询都是从缓存本身提供的,而不是一遍又一遍地编译。