Кэширование в Hibernate

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

Идея кэширования (не только в Hibernate) основывается на мнении, что из всех данных, доступных для обработки, работа ведётся только над некоторым небольшим набором и если ускорить доступ к этому набору, то в среднем программа будет работать быстрее. Говоря конкретно о Hibernate, доступ к базе занимает на порядке больше времени, чем доступ к объекту в памяти JVM. И поэтому, если какое-то время хранить в памяти загруженные из БД объекты, то при их повторном запросе Hibernate сможет вернуть их гораздо быстрее.

Кэш первого уровня

С объектом Session, а точнее с persistence context, в Hibernate всегда связан кэш первого уровня. При помещении объекта в persistence context, то есть при его загрузке из БД или сохранении, объект так же автоматически будет помещён в кэш первого уровня и это невозможно отключить. Соответственно, при запросах того же самого объекта несколько раз в рамках одного persistence context, запрос в БД будет выполнен один раз, а всё остальные загрузки будут выполнены из кэша.

Session session = sessionFactory.openSession();
session.beginTransaction();


// Database will be queried
System.out.println(session.get(Person.class, 123456L));


// Cached object will be returned
System.out.println(session.get(Person.class, 123456L));


session.getTransaction().commit();
session.close();

В примере выше только первый вызов get() инициирует запрос к базе, второй вызов будет обслужен уже из кэша и обращения к базе не произойдёт.

Интересно поведение кэша первого уровня при использовании ленивой загрузки. При загрузке объекта методом load() или объекта с лениво загружаемыми полями, лениво загружаемые данные в кэш не попадут. При обращении к данным будет выполнен запрос в базу и данные будут загружены и в объект и в кэш. А вот следующая попытка лениво загрузить объект приведёт к тому, что объект сразу вернут из кэша и уже полностью загруженным.

session = sessionFactory.openSession();
session.beginTransaction();


// Database is not queried, only reference is returned
Person p1 = session.load(Person.class, 123456L);


// Query triggered, object is filled with data
System.out.println(p1);


// Cached, fully populated object will be returned
Person p2=session.load(Person.class, 123456L);


System.out.println(p2);


session.getTransaction().commit();
session.close();

Кэш второго уровня

Если кэш первого уровня существует только на уровне сессии и persistence context, то кэш второго уровня находится выше — на уровне SessionFactory и, следовательно, один и тот же кэш доступен одновременно в нескольких persistence context. Кэш второго уровня требует некоторой настройки и поэтому не включен по умолчанию. Настройка кэша заключается в конфигурировании реализации кэша и разрешения сущностям быть закэшированными.

Конфигурирование кэша

Hibernate не реализует сам никакого in-memory сache, а использует существующие реализации кэшей. Раньше Hibernate самостоятельно поддерживал интерфейс с этими кэшами, но сейчас существует JCache и корректнее будет использовать этот интерфейс. Реализаций у JCache множество, но я выберу ehcache, как одну из самых распространённых.

В первую очередь надо добавить поддержку JCache и ehcache в зависимости:

<properties>
<hibernate.version>5.2.0.Final</hibernate.version>
<ehcache.version>3.1.1</ehcache.version>
</properties>


<dependencies>
  <dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>${ehcache.version}</version>
  </dependency>


  <dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>${hibernate.version}</version>
  </dependency>


  <dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-jcache</artifactId>
    <version>${hibernate.version}</version>
  </dependency>


</dependencies>

Затем настроить hibernate на использование ehcache для кэширования:

<property name="hibernate.cache.region.factory_class">org.hibernate.cache.jcache.JCacheRegionFactory</property>
<property name="hibernate.javax.cache.provider">org.ehcache.jsr107.EhcacheCachingProvider</property>

В первой строке мы говорим Hibernate, что хотим использовать JCache интерфейс, а во второй строке выбираем конкретную реализацию JCache: ehcache.

Наконец, включим кэш второго уровня:

<property name="hibernate.cache.use_second_level_cache">true</property>

@Cacheable и @Cache

@Cacheable это аннотация JPA и позволяет объекту быть закэшированным. Hibernate поддерживает эту аннотацию в том же ключе.

@Entity
@Cacheable
public class Person extends AbstractIdentifiableObject {
    @Getter
    @Setter
    private String firstName;
    
    //Other fields
}

@Cache это аннотация Hibernate, настраивающая тонкости кэширования объекта в кэше второго уровня Hibernate. Аннотации @Cacheable  достаточно, чтобы объект начал кэшироваться с настройками по умолчанию. При этом @Cache использованная без @Cacheable не разрешит кэширование объекта.

@Cache  принимает три параметра:

  • include, имеющий по умолчанию значение all и означающий кэширование всего объекта. Второе возможное значение, non-lazy, запрещает кэширование лениво загружаемых объектов. Кэш первого уровня не обращает внимания на эту директиву и всегда кэширует лениво загружаемые объекты.
  • region позволяет задать имя региона кэша для хранения сущности. Регион можно представить как разные кэши или разные части кэша, имеющие разные настройки на уровне реализации кэша. Например, я мог бы создать в конфигурации ehcache два региона, один с краткосрочным хранением объектов, другой с долгосрочным и отправлять часто изменяющиеся объекты в первый регион, а все остальные во второй.
  • usage задаёт стратегию одновременного доступа к объектам.

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

Стратегий одновременного доступа к объектам в кэше в hibernate существует четыре:

  • translactional — полноценное разделение транзакций. Каждая сессия и каждая транзакция видят объекты, как если бы только они с ним работали последовательно одна транзакция за другой. Плата за это — блокировки и потеря производительности.
  • read-write — полноценный доступ к одной конкретной записи и разделение её состояния между транзакциями. Однако суммарное состояние нескольких объектов в разных транзакциях может отличаться.
  • nonstrict-read-write — аналогичен read-write, но изменения объектов могут запаздывать и транзакции могут видеть старые версии объектов. Рекомендуется использовать в случаях, когда одновременное обновление объектов маловероятно и не может привести к проблемам.
  • read-only — объекты кэшируются только для чтения и изменение удаляет их из кэша.

Список выше отсортирован по нарастанию производительности, transactional стратегия самая медленная, read-only самая быстрая. Недостатком read-only стратегии является её бесполезность, в случае если объекты постоянно изменяются, так как в этом случае они не будут задерживаться в кэше.

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

    
session = sessionFactory.openSession();
session.beginTransaction();


// Database will be queried
System.out.println(session.get(Person.class, 3L));


session.getTransaction().commit();
session.close();


session = sessionFactory.openSession();
session.beginTransaction();


// Database will not be queried, 2nd level cache will provide the data
System.out.println(session.get(Person.class, 3L));


session.getTransaction().commit();
session.close();

Кэш запросов

Кэши первого и второго уровней работают с объектами загружаемыми по id. Но в дикой природе к базе чаще выполняются запросы с условиями, чем загружаются какие-то заранее известные объекты:

session.createCriteria(Passport.class)
  .add(Restrictions.eq("series", "AS"))
  .uniqueResult()

И результат выполнения таких запросов тоже может потребоваться кэшировать. Например если вы делаете поисковый сайт по автозапчастям, то можете кэшировать запросы пользователей, которые, скорее всего, ищут одни запчасти гораздо чаще других. У кэша запросов есть и своя цена — Hibernate будет вынужден отслеживать сущности закешированные с определённым запросом и выкидывать запрос из кэша, если кто-то поменяет значение сущности. То есть для кэша запросов стратегия параллельного доступа всегда read-only.

Чтобы включить кэш запросов надо настроить внешний кэш, так же как и для кэша второго уровня, и разрешить Hibernate кэшировать запросы:

<property name="hibernate.cache.use_query_cache">true</property>

Но даже с этим разрешением Hibernate не будет кэшировать все запросы, а только те, кэширование которых явно запрошено методом setCacheable()

session = sessionFactory.openSession();
session.beginTransaction();


// Database will be queried
System.out.println(session.createCriteria(Passport.class)
  .add(Restrictions.eq("series", "AS"))
  .setCacheable(true)
  .uniqueResult());


session.getTransaction().commit();
session.close();


session = sessionFactory.openSession();
session.beginTransaction();


// Database will not be queries, query cache will provide the data
System.out.println(session.createCriteria(Passport.class)
  .add(Restrictions.eq("series", "AS"))
  .setCacheable(true)
  .uniqueResult());


session.getTransaction().commit();
session.close();

Кэш запросов, так же как и кэш второго уровня, существует на уровне SessionFactory и доступен во всех persistence context.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *