Статьи

Подводные камни спящих кэшей второго уровня / запросов

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

Кэш второго уровня Hibernate — это кэш прикладного уровня для хранения данных объектов. Кеш запросов — это отдельный кеш, в котором хранятся только результаты запроса.

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

Как работает кэш второго уровня?

Кэш второго уровня хранит данные сущностей, но НЕ сами сущности. Данные хранятся в «обезвоженном» формате, который выглядит как хэш-карта, где ключ — это идентификатор объекта, а значение — это список примитивных значений.

Вот пример того, как выглядит содержимое кэша второго уровня:

1
2
3
4
5
6
7
*-----------------------------------------*
|          Person Data Cache              |
|-----------------------------------------|
| 1 -> [ "John" , "Q" , "Public" , null ] |
| 2 -> [ "Joey" , "D" , "Public" 1   ] |
| 3 -> [ "Sara" , "N" , "Public" 1   ] |
*-----------------------------------------*

Кэш второго уровня заполняется, когда объект загружается с помощью Id из базы данных, используя, например, entityManager.find() , или при обходе ленивых инициализированных отношений.

Как работает кеш запросов?

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

1
2
3
4
5
*----------------------------------------------------------*
|                       Query Cache                        |                    
|----------------------------------------------------------|
| ["from Person where firstName=?", ["Joey"] ] -> [1, 2] ] |
*----------------------------------------------------------*

Некоторые запросы не возвращают сущности, а возвращают только примитивные значения. В этих случаях сами значения будут храниться в кеше запросов. Кэш запросов заполняется при выполнении кешируемого запроса JPQL / HQL.

Какова связь между двумя кешами?

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

Если кэш второго уровня содержит данные для данного идентификатора, он повторно гидратирует объект и возвращает его. Если кэш второго уровня не содержит результатов для этого конкретного идентификатора, то выдается запрос SQL для загрузки объекта из базы данных.

Как настроить два кэша в приложении

Первый шаг — включить банку hibernate-ehcache в classpath:

1
2
3
4
5
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-ehcache</artifactId>
    <version>SOME-HIBERNATE-VERSION</version>
</dependency>

Следующие параметры необходимо добавить в конфигурацию вашего EntityManagerFactory или SessionFactory :

1
2
3
4
<prop key="hibernate.cache.use_second_level_cache">true</prop>
<prop key="hibernate.cache.use_query_cache">true</prop>
<prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop>
<prop key="net.sf.ehcache.configurationResourceName">/your-cache-config.xml</prop>

Предпочитаю использовать EhCacheRegionFactory вместо SingletonEhCacheRegionFactory . Использование EhCacheRegionFactory означает, что Hibernate создаст отдельные области кэша для кэширования Hibernate вместо того, чтобы пытаться повторно использовать области кэша, определенные в другом месте приложения.

Следующим шагом является настройка параметров областей кэша в файле your-cache-config.xml :

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
<?xml version="1.0" ?>
             updateCheck="false"
       xsi:noNamespaceSchemaLocation="ehcache.xsd" name="yourCacheManager">
 
     <diskStore path="java.io.tmpdir"/>
 
     <cache name="yourEntityCache"
            maxEntriesLocalHeap="10000"
            eternal="false"
            overflowToDisk="false"
            timeToLiveSeconds="86400" />
 
     <cache name="org.hibernate.cache.internal.StandardQueryCache"
            maxElementsInMemory="10000"
            eternal="false
            timeToLiveSeconds="86400"
            overflowToDisk="false"
            memoryStoreEvictionPolicy="LRU" />
 
  <defaultCache
          maxElementsInMemory="10000"
          eternal="false"
          timeToLiveSeconds="86400"
          overflowToDisk="false"
          memoryStoreEvictionPolicy="LRU" />
</ehcache>

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

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

Использование кэша второго уровня

Кэш второго уровня теперь готов к использованию. Чтобы кэшировать объекты, аннотируйте их аннотацией @org.hibernate.annotations.Cache :

1
2
3
4
5
6
@Entity      
@Cache(usage=CacheConcurrencyStrategy.READ_ONLY,
     region="yourEntityCache")
public class SomeEntity {
    ...
}

Ассоциации могут также кэшироваться кэшем второго уровня, но по умолчанию это не делается. Чтобы включить кэширование ассоциации, нам нужно применить @Cache к самой ассоциации:

1
2
3
4
5
6
7
@Entity      
public class SomeEntity {
    @OneToMany
    @Cache(usage=CacheConcurrencyStrategy.READ_ONLY,
        region="yourCollectionRegion")
     private Set<OtherEntity> other;    
}

Использование кеша запросов

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

1
2
3
4
5
6
7
@NamedQuery(name="account.queryName",
   query="select acct from Account ...",
   hints={
       @QueryHint(name="org.hibernate.cacheable",
       value="true")
   }    
})

И вот как пометить запрос критерия как кэшированный:

1
2
3
List cats = session.createCriteria(Cat.class)
    .setCacheable(true)
    .list();

В следующем разделе рассматриваются некоторые подводные камни, с которыми вы можете столкнуться при попытке настроить эти два кэша. Это поведение, которое работает так, как задумано, но все же может вызывать удивление.

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

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

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

Например, если кешированный запрос возвратил 1000 идентификаторов сущностей, а не те сущности, которые были кешированы в кеше второго уровня, то 1000 выборок по идентификатору будут выданы базе данных.

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

Ошибка 2 — Ограничения кэширования при использовании вместе с @Inheritance

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

Например, это не будет работать:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
@Entity
@Inheritance
@Cache(CacheConcurrencyStrategy.READ_ONLY)
public class BaseEntity {
    ...
}
 
@Entity
@Cache(CacheConcurrencyStrategy.READ_WRITE)
public class SomeReadWriteEntity extends BaseEntity {
    ...
}
 
@Entity
@Cache(CacheConcurrencyStrategy.TRANSACTIONAL)
public class SomeTransactionalEntity extends BaseEntity {
    ...
}

В этом случае рассматривается только аннотация @Cache родительского класса, и все конкретные сущности имеют стратегию параллелизма READ_ONLY .

Ошибка 3 — Настройки кэша игнорируются при использовании одноэлементного кэша

Рекомендуется настроить фабрику области кэша как EhCacheRegionFactory и указать конфигурацию ehcache через net.sf.ehcache.configurationResourceName .

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

Проблема с фабрикой одноэлементного региона состоит в том, что, если другая часть приложения уже зарегистрировала кэш с именем по умолчанию в одноэлементном файле, это приводит к игнорированию файла конфигурации ehcache, передаваемого через net.sf.ehcache.configurationResourceName .

Вывод

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