Никогда не пишите свое решение по кешированию

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

Важно! Никогда не пиши свое решение по кешированию. Эта задача имеет столько подводных камней, что тебе и не снилось.

Проблема 1 — сброс кэша. Иногда происходят события, когда нужно удалить объект из кэша или обновить его в нем. Единственный способ сделать это грамотно — пропускать все запросы к базе через движок кэша. Иначе тебе каждый раз придется явно указывать кэшу, какие объекты в нем стоит удалить или обновить.

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

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

Проблема 3 — различные стратегии. Как показывает практика, для разных объектов эффективны различные стратегии хранения и обновления в кэше. Эффективная система кэширования не может обойтись одой стратегией для всех объектов.

Проблема 4 — эффективное хранение объектов. Нельзя просто хранить объекты в кэше. Объекты слишком часто содержат ссылки на другие объекты и т. п. Такими темпами тебе не понадобится сборщик мусора: ему просто будет нечего удалять.

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

На выходе ты получишь целую виртуальную СУБД в памяти, которая должна быстро работать и потреблять мало памяти.

Кеширование в базе данных

Кроме кеширования прямо в Java-программе еще часто организовывают кеширование прямо в базе данных.

Там есть четыре больших подхода:

Подход первый — денормализация базы данных. SQL-сервер у себя в памяти хранит данные не так, как они храниться в таблицах.

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

Второй подход — кэширование запросов. И результатов запросов.

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

Этот подход может быть очень эффективным при участии человека, который может проанализировать запросы и помочь СУБД понять, как их лучше кэшировать.

Третий подход — база данных в памяти.

Еще один часто используемый подход. Между сервером и СУБД ставится еще одна база, которая хранит все свои данные только в памяти. Ее еще называют In-Memory-DB. Если у тебя много разных серверов обращаются к одной базе данных, то с помощью In-Memory-DB можно организовать кэширование, ориентированное на тип конкретного сервера.

Пример:

Подход 4 — кластер баз данных. Несколько read-only баз.

Еще одно решение — использование кластера: несколько СУБД одного типа содержат идентичные данные. При этом читать данные можно из всех баз, а писать — только в одну. Которая потом синхронизируется с остальными базами.

Это очень хорошее решение, потому что его легко конфигурировать и оно работает на практике. Обычно на один запрос к базе на изменение данных к ней приходит 10-100 запросов на чтение данных.

Виды кеширования в Hibernate

Hibernate поддерживает три уровня кэширования:

  • Кеширование на уровне сессии (Session)
  • Кеширование на уровне SessionFactory
  • Кеширование запросов (и их результатов)

Эту системы можно попробовать представить в виде такого рисунка:

Самый простой вид кеширования (его еще называют кэшем первого уровня) реализован на уровне Hibernate-сессии. Hibernate всегда по умолчанию использует этот кэш и его нельзя отключить.

Давай сразу рассмотрим следующий пример:


Employee director1 = session.get(Employee.class, 4);
Employee director2 = session.get(Employee.class, 4);
 
assertTrue(director1 == director2);

Может показаться, что тут будет выполнено два запроса в базу, однако это не так. После первого запроса в базу объект Employee будет закэширован. И если ты снова выполнишь запрос объекта в той же сессии, то Hibernate вернет тот же Java-объект.

Тот же объект — это значит, что даже ссылки на объекты будут идентичными. Это реально один и тот же объект.

При использовании методов save(), update(), saveOrUpdate(), load(), get(), list(), iterate() и scroll() всегда будет задействован кэш первого уровня. Собственно, тут нечего больше добавить.

Кэширование второго уровня

Если кэш первого уровня привязан к объекту сессии, то кэш второго уровня привязан к объекту SessionFactory. Что означает, что видимость объектов в этом кэше гораздо шире, чем в кэше первого уровня.

Пример:


Session session = factory.openSession();
Employee director1 = session.get(Employee.class, 4);
session.close();
 
Session session = factory.openSession();
Employee director2 = session.get(Employee.class, 4);
session.close();
 
assertTrue(director1 != director2);
assertTrue(director1.equals(director2));

В этом примере будет выполнено два запроса в базу. Hibernate вернет идентичные объекты, но это будет не тот же объект — они будут иметь разные ссылки.

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

Чтобы его включить, нужно в файле hibernate.cfg.xml написать такие строчки:


<property name="hibernate.cache.provider_class" value="net.sf.ehcache.hibernate.SingletEhCacheProvider"/>
<property name="hibernate.cache.use_second_level_cache" value="true"/>

После включения кэширования второго уровня поведение Hibernate немного изменится:


Session session = factory.openSession();
Employee director1 = session.get(Employee.class, 4);
session.close();
 
Session session = factory.openSession();
Employee director2 = session.get(Employee.class, 4);
session.close();
 
assertTrue(director1 == director2);

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