Статьи

Как работает Hibernate Query Cache

Вступление

Теперь, когда я рассмотрел кэширование сущностей и коллекций , пришло время изучить, как работает Query Caching .

Кэш запросов строго связан с сущностями и устанавливает связь между критериями поиска и сущностями, выполняющими этот конкретный фильтр запросов. Как и другие функции Hibernate, Query Cache не так тривиален, как можно подумать.

Модель объекта

Для наших тестовых случаев мы будем использовать следующую модель предметной области:

postauthorquerycache

Объект Post имеет связь « многие к одному» с автором, и оба объекта хранятся в кэше второго уровня.

Включение кеша запросов

Кэш запросов по умолчанию отключен, и для его активации нам необходимо предоставить следующее свойство Hibernate:

1
2
properties.put("hibernate.cache.use_query_cache",
    Boolean.TRUE.toString());

Чтобы Hibernate кэшировал данный результат запроса, нам нужно явно установить атрибут запроса cachable при создании запроса.

Прочитанное кеширование

Кэш запросов является читаемым и, подобно стратегии параллелизма NONSTRICT_READ_WRITE , может аннулировать только устаревшие записи.

В следующем примере мы собираемся кэшировать следующий запрос:

1
2
3
4
5
6
7
8
9
private List<Post> getLatestPosts(Session session) {
    return (List<Post>) session.createQuery(
        "select p " +
        "from Post p " +
        "order by p.createdOn desc")
    .setMaxResults(10)
    .setCacheable(true)
    .list();
}

Во-первых, мы собираемся исследовать внутреннюю структуру Query Cache, используя следующий тестовый пример:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
doInTransaction(session -> {
    LOGGER.info(
        "Evict regions and run query");
    session.getSessionFactory()
        .getCache().evictAllRegions();
    assertEquals(1, getLatestPosts(session).size());
});
 
doInTransaction(session -> {
    LOGGER.info(
        "Check get entity is cached");
    Post post = (Post) session.get(Post.class, 1L);
});
 
doInTransaction(session -> {
    LOGGER.info(
        "Check query result is cached");
    assertEquals(1, getLatestPosts(session).size());
});

Этот тест генерирует следующий вывод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
QueryCacheTest - Evict regions and run query
 
StandardQueryCache - Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache       
EhcacheGeneralDataRegion - Element for key sql:
    select
       querycache0_.id as id1_1_,
       querycache0_.author_id as author_i4_1_,
       querycache0_.created_on as created_2_1_,
       querycache0_.name as name3_1_
    from
       Post querycache0_
    order by
       querycache0_.created_on desc;
    parameters: ;
    named parameters: {};
    max rows: 10;
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2
is null
StandardQueryCache - Query results were not found in cache
 
select
   querycache0_.id as id1_1_,
   querycache0_.author_id as author_i4_1_,
   querycache0_.created_on as created_2_1_,
   querycache0_.name as name3_1_
from
   Post querycache0_
order by
   querycache0_.created_on desc limit 10
    
StandardQueryCache - Caching query results in region: org.hibernate.cache.internal.StandardQueryCache; timestamp=5872026465492992
EhcacheGeneralDataRegion -
key:
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_
        from
           Post querycache0_
        order by
           querycache0_.created_on desc;
    parameters: ;
    named parameters: {};
    max rows: 10;
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2
value: [5872026465492992, 1]
 
JdbcTransaction - committed JDBC Connection
 
------------------------------------------------------------
 
QueryCacheTest - Check get entity is cached
 
JdbcTransaction - committed JDBC Connection
 
------------------------------------------------------------
 
QueryCacheTest - Check query is cached
 
StandardQueryCache - Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache
StandardQueryCache - Checking query spaces are up-to-date: [Post]
 
EhcacheGeneralDataRegion - key: Post
UpdateTimestampsCache - [Post] last update timestamp: 5872026465406976, result set timestamp: 5872026465492992
StandardQueryCache - Returning cached query results
 
JdbcTransaction - committed JDBC Connection
  • Все области кэша удалены, чтобы убедиться, что кэш пуст
  • После выполнения запроса Post кэш запросов проверяет ранее сохраненные результаты.
  • Поскольку запись в кэше отсутствует, запрос отправляется в базу данных.
  • Как выбранные объекты, так и результат запроса кэшируются
  • Затем мы проверяем, что сущность Post была сохранена в кэше второго уровня.
  • Последующий запрос запроса будет решен из кэша, не затрагивая базу данных

Параметры запроса

Параметры запроса встраиваются в ключ записи в кеш, как мы можем видеть в следующих примерах.

Основные типы

Во-первых, мы собираемся использовать базовую фильтрацию типов :

01
02
03
04
05
06
07
08
09
10
11
12
private List<Post> getLatestPostsByAuthorId(Session session) {
    return (List<Post>) session.createQuery(
        "select p " +
        "from Post p " +
        "join p.author a " +
        "where a.id = :authorId " +
        "order by p.createdOn desc")
    .setParameter("authorId", 1L)
    .setMaxResults(10)
    .setCacheable(true)
    .list();
}
1
2
3
4
5
doInTransaction(session -> {
    LOGGER.info("Query cache with basic type parameter");
    List<Post> posts = getLatestPostsByAuthorId(session);
    assertEquals(1, posts.size());
});

Запись Query Cache выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
EhcacheGeneralDataRegion -
key:
    sql:
        select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_
        from
           Post querycache0_
        inner join
           Author querycache1_
              on querycache0_.author_id=querycache1_.id
        where
           querycache1_.id=?
        order by
           querycache0_.created_on desc;
    parameters: ;
    named parameters: {authorId=1};
    max rows: 10;
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2
value: [5871781092679680, 1]

Параметр хранится в ключе записи кеша. Первым элементом значения записи в кеше всегда является метка времени выборки из результирующего набора. Следующие элементы являются идентификаторами сущностей, которые были возвращены этим запросом.

Типы сущностей

Мы также можем использовать типы сущностей в качестве параметров запроса:

01
02
03
04
05
06
07
08
09
10
11
12
13
private List<Post> getLatestPostsByAuthor(Session session) {
        Author author = (Author) session.get(Author.class, 1L);
    return (List<Post>) session.createQuery(
        "select p " +
        "from Post p " +
        "join p.author a " +
        "where a = :author " +
        "order by p.createdOn desc")
    .setParameter("author", author)
    .setMaxResults(10)
    .setCacheable(true)
    .list();
}
1
2
3
4
5
doInTransaction(session -> {
    LOGGER.info("Query cache with entity type parameter");
    List<Post> posts = getLatestPostsByAuthor(session);
    assertEquals(1, posts.size());
});

Запись в кеш аналогична нашему предыдущему примеру, поскольку Hibernate хранит только идентификатор объекта в ключе записи в кеш. Это имеет смысл, поскольку Hibernate уже кэширует сущность Author .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
EhcacheGeneralDataRegion -
key:
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_
        from
           Post querycache0_
        inner join
           Author querycache1_
              on querycache0_.author_id=querycache1_.id
        where
           querycache1_.id=?
        order by
           querycache0_.created_on desc;
    parameters: ;
    named parameters: {author=1};
    max rows: 10;
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2
value: [5871781092777984, 1]

консистенция

HQL / JPQL-запрос недействителен

Hibernate кэш второго уровня поддерживает строгую согласованность, и Query Cache ничем не отличается. Как и при очистке , Query Cache может сделать недействительными свои записи при изменении связанного табличного пространства. Каждый раз, когда мы сохраняем / удаляем / обновляем Entity , все записи в Query Cache, использующие эту конкретную таблицу, становятся недействительными

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
doInTransaction(session -> {
    Author author = (Author)
        session.get(Author.class, 1L);
    assertEquals(1, getLatestPosts(session).size());
 
    LOGGER.info("Insert a new Post");
    Post newPost = new Post("Hibernate Book", author);
    session.persist(newPost);
    session.flush();
 
    LOGGER.info("Query cache is invalidated");
    assertEquals(2, getLatestPosts(session).size());
});
 
doInTransaction(session -> {
    LOGGER.info("Check Query cache");
    assertEquals(2, getLatestPosts(session).size());
});

Этот тест добавит новое сообщение, а затем повторно запустит кешируемый запрос. Запуск этого теста дает следующий вывод:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
QueryCacheTest - Insert a new Post
 
insert
into
   Post
   (id, author_id, created_on, name)
values
   (default, 1, '2015-06-06 17:29:59.909', 'Hibernate Book')
 
UpdateTimestampsCache - Pre-invalidating space [Post], timestamp: 5872029941395456
EhcacheGeneralDataRegion - key: Post value: 5872029941395456
 
QueryCacheTest - Query cache is invalidated
StandardQueryCache - Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache
EhcacheGeneralDataRegion -
key:
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_
        from
           Post querycache0_
        order by
           querycache0_.created_on desc;
    parameters: ;
    named parameters: {};
    max rows: 10;
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2
     
StandardQueryCache - Checking query spaces are up-to-date: [Post]
EhcacheGeneralDataRegion - key: Post
UpdateTimestampsCache - [Post] last update timestamp: 5872029941395456, result set timestamp: 5872029695619072
StandardQueryCache - Cached query results were not up-to-date
 
select
   querycache0_.id as id1_1_,
   querycache0_.author_id as author_i4_1_,
   querycache0_.created_on as created_2_1_,
   querycache0_.name as name3_1_
from
   Post querycache0_
order by
   querycache0_.created_on desc limit 10
    
StandardQueryCache - Caching query results in region: org.hibernate.cache.internal.StandardQueryCache; timestamp=5872029695668224
EhcacheGeneralDataRegion -
key:
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_
        from
           Post querycache0_
        order by
           querycache0_.created_on desc;
    parameters: ;
    named parameters: {};
    max rows: 10;
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2
value: [5872029695668224, 2, 1]
 
JdbcTransaction - committed JDBC Connection
 
UpdateTimestampsCache - Invalidating space [Post], timestamp: 5872029695680512
EhcacheGeneralDataRegion - key: Post value: 5872029695680512
 
------------------------------------------------------------
 
QueryCacheTest - Check Query cache
 
StandardQueryCache - Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache
EhcacheGeneralDataRegion -
key:
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_
        from
           Post querycache0_
        order by
           querycache0_.created_on desc;
    parameters: ;
    named parameters: {};
    max rows: 10;
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2
         
StandardQueryCache - Checking query spaces are up-to-date: [Post]
EhcacheGeneralDataRegion - key: Post
UpdateTimestampsCache - [Post] last update timestamp: 5872029695680512, result set timestamp: 5872029695668224
StandardQueryCache - Cached query results were not up-to-date
 
select
   querycache0_.id as id1_1_,
   querycache0_.author_id as author_i4_1_,
   querycache0_.created_on as created_2_1_,
   querycache0_.name as name3_1_
from
   Post querycache0_
order by
   querycache0_.created_on desc limit 10
 
StandardQueryCache - Caching query results in region: org.hibernate.cache.internal.StandardQueryCache; timestamp=5872029695705088
EhcacheGeneralDataRegion -
key:
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_
        from
           Post querycache0_
        order by
           querycache0_.created_on desc;
    parameters: ;
    named parameters: {};
    max rows: 10;
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2
value: [5872029695705088, 2, 1]
 
JdbcTransaction - committed JDBC Connection
  • Как только Hibernate обнаруживает изменение состояния сущности , он предварительно аннулирует уязвимые области кэша запросов
  • Запись Query Cache не удаляется, но соответствующая метка времени обновляется.
  • Кэш запросов всегда проверяет временную метку ключа ввода и пропускает чтение его значения, если временная метка ключа новее, чем временная метка загрузки результирующего набора
  • Если текущий сеанс повторно запускает этот запрос, результат будет снова кэширован
  • Текущая транзакция базы данных фиксируется, и изменения распространяются от изоляции на уровне сеанса к общей согласованности чтения
  • Происходит фактическая аннулирование, и отметка времени записи в кэше снова обновляется

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

Неверный собственный запрос

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

01
02
03
04
05
06
07
08
09
10
11
doInTransaction(session -> {
    assertEquals(1, getLatestPosts(session).size());
 
    LOGGER.info("Execute native query");
    assertEquals(1, session.createSQLQuery(
        "update Author set name = '\"'||name||'\"' "
    ).executeUpdate());
 
    LOGGER.info("Check query cache is invalidated");
    assertEquals(1, getLatestPosts(session).size());
});

Тест генерирует следующий вывод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
QueryCacheTest - Execute native query
 
UpdateTimestampsCache - Pre-invalidating space [Author], timestamp: 5872035446091776
EhcacheGeneralDataRegion - key: Author value: 5872035446091776
UpdateTimestampsCache - Pre-invalidating space [Post], timestamp: 5872035446091776
EhcacheGeneralDataRegion - key: Post value: 5872035446091776
 
update
   Author
set
   name = '"'||name||'"'
 
QueryCacheTest - Check query cache is invalidated
 
StandardQueryCache - Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache
EhcacheGeneralDataRegion -
key:
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_
        from
           Post querycache0_
        order by
           querycache0_.created_on desc;
            parameters: ;
    named parameters: {};
    max rows: 10;
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2
     
StandardQueryCache - Checking query spaces are up-to-date: [Post]
EhcacheGeneralDataRegion - key: Post
UpdateTimestampsCache - [Post] last update timestamp: 5872035446091776, result set timestamp: 5872035200290816
StandardQueryCache - Cached query results were not up-to-date
 
select
   querycache0_.id as id1_1_,
   querycache0_.author_id as author_i4_1_,
   querycache0_.created_on as created_2_1_,
   querycache0_.name as name3_1_
from
   Post querycache0_
order by
   querycache0_.created_on desc limit 10
 
StandardQueryCache - Caching query results in region: org.hibernate.cache.internal.StandardQueryCache; timestamp=5872035200364544
EhcacheGeneralDataRegion -
key:
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_
        from
           Post querycache0_
        order by
           querycache0_.created_on desc;
    parameters: ;
    named parameters: {};
    max rows: 10;
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2
value: [5872035200364544, 1]
 
JdbcTransaction - committed JDBC Connection
 
UpdateTimestampsCache - Invalidating space [Post], timestamp: 5872035200372736
EhcacheGeneralDataRegion - key: Post value: 5872035200372736
UpdateTimestampsCache - Invalidating space [Author], timestamp: 5872035200372736
EhcacheGeneralDataRegion - key: Author value: 5872035200372736

Обе области Автор и Почтовый кеш были признаны недействительными, даже если была изменена только таблица Автор . Чтобы это исправить, нам нужно дать Hibernate знать, какие таблицы мы собираемся изменить.

Синхронизация областей собственного кэша запросов

Hibernate позволяет нам определять табличное пространство запроса с помощью подсказок по синхронизации запросов . При предоставлении этой информации Hibernate может сделать недействительными запрошенные области кэша:

01
02
03
04
05
06
07
08
09
10
11
12
doInTransaction(session -> {
    assertEquals(1, getLatestPosts(session).size());
 
    LOGGER.info("Execute native query with synchronization");
    assertEquals(1, session.createSQLQuery(
            "update Author set name = '\"'||name||'\"' "
    ).addSynchronizedEntityClass(Author.class)
    .executeUpdate());
 
    LOGGER.info("Check query cache is not invalidated");
    assertEquals(1, getLatestPosts(session).size());
});

Создается следующий вывод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
QueryCacheTest - Execute native query with synchronization
 
UpdateTimestampsCache - Pre-invalidating space [Author], timestamp: 5872036893995008
EhcacheGeneralDataRegion - key: Author value: 5872036893995008
 
update
   Author
set
   name = '"'||name||'"'
 
QueryCacheTest - Check query cache is not invalidated
 
StandardQueryCache - Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache
EhcacheGeneralDataRegion -
key:
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_
        from
           Post querycache0_
        order by
           querycache0_.created_on desc;
    parameters: ;
    named parameters: {};
    max rows: 10;
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2
 
StandardQueryCache - Checking query spaces are up-to-date: [Post]
EhcacheGeneralDataRegion - key: Post
UpdateTimestampsCache - [Post] last update timestamp: 5872036648169472, result set timestamp: 5872036648226816
StandardQueryCache - Returning cached query results
 
JdbcTransaction - committed JDBC Connection
 
UpdateTimestampsCache - Invalidating space [Author], timestamp: 5872036648263680
EhcacheGeneralDataRegion - key: Author value: 5872036648263680

Только предоставленное табличное пространство было признано недействительным, оставив кэш Post Query нетронутым. Смешивание собственных запросов и кеширования запросов возможно, но требует немного усердия.

Вывод

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

Код доступен на GitHub .