В статье о поддержке пользовательских типов в Hibernate упоминается о поддержке кэширования. В этой статье я постараюсь рассказать о кэшровании подробнее.
Идея кэширования (не только в Hibernate) основывается на мнении, что из всех данных, доступных для обработки, работа ведётся только над некоторым небольшим набором и если ускорить доступ к этому набору, то в среднем программа будет работать быстрее. Говоря конкретно о Hibernate, доступ к базе занимает на порядке больше времени, чем доступ к объекту в памяти JVM. И поэтому, если какое-то время хранить в памяти загруженные из БД объекты, то при их повторном запросе Hibernate сможет вернуть их гораздо быстрее.
Кэш первого уровня
С объектом Session, а точнее с persistence context, в Hibernate всегда связан кэш первого уровня. При помещении объекта в persistence context, то есть при его загрузке из БД или сохранении, объект так же автоматически будет помещён в кэш первого уровня и это невозможно отключить. Соответственно, при запросах того же самого объекта несколько раз в рамках одного persistence context, запрос в БД будет выполнен один раз, а всё остальные загрузки будут выполнены из кэша.
1
2
3
4
5
6
7
8
9
10
11
|
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() или объекта с лениво загружаемыми полями, лениво загружаемые данные в кэш не попадут. При обращении к данным будет выполнен запрос в базу и данные будут загружены и в объект и в кэш. А вот следующая попытка лениво загрузить объект приведёт к тому, что объект сразу вернут из кэша и уже полностью загруженным.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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 в зависимости:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
<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 для кэширования:
1
2
|
<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.
Наконец, включим кэш второго уровня:
1
|
<property name="hibernate.cache.use_second_level_cache">true</property>
|
@Cacheable и @Cache
@Cacheable это аннотация JPA и позволяет объекту быть закэшированным. Hibernate поддерживает эту аннотацию в том же ключе.
1
2
3
4
5
6
7
8
9
|
@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 и в коде сущностей, но не требует изменения кода запросов и управления сущностями:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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. Но в дикой природе к базе чаще выполняются запросы с условиями, чем загружаются какие-то заранее известные объекты:
1
2
3
|
session.createCriteria(Passport.class)
.add(Restrictions.eq("series", "AS"))
.uniqueResult()
|
И результат выполнения таких запросов тоже может потребоваться кэшировать. Например если вы делаете поисковый сайт по автозапчастям, то можете кэшировать запросы пользователей, которые, скорее всего, ищут одни запчасти гораздо чаще других. У кэша запросов есть и своя цена — Hibernate будет вынужден отслеживать сущности закешированные с определённым запросом и выкидывать запрос из кэша, если кто-то поменяет значение сущности. То есть для кэша запросов стратегия параллельного доступа всегда read-only.
Чтобы включить кэш запросов надо настроить внешний кэш, так же как и для кэша второго уровня, и разрешить Hibernate кэшировать запросы:
1
|
<property name="hibernate.cache.use_query_cache">true</property>
|
Но даже с этим разрешением Hibernate не будет кэшировать все запросы, а только те, кэширование которых явно запрошено методом setCacheable()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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.
Код пример доступен на github.