В первой части мы рассмотрели концепцию кэша второго уровня и способы его конфигурации. Во второй части рассмотрим стратегии кэширования и конфигурацию EhCache.
Пример проекта в котором Вы можете посмотреть работу кэша и поэксперементировать находится на GitHub.
Стратегии кэширования
Под стратегией понимается, что можно делать над объектом кэша – нашей сущностью: изменять, удалять, вставлять, читать, давайте рассмотрим их:
- read-only — самая простая стратегия, кэш может только читаться, операции обновления (update) и удаления (delete) не разрешены, однако можно вставлять новые данные (insert) отлично подходит к кэшированию различных справочников, например наименование регионов, городов, улиц … и т. д.
- nonstrict read-write — данные этого кэша могут меняться, возможен конкурентный доступ к одному и тому же объекту. Может произойти ситуация когда в кэше содержатся не последняя измененная сущность, т. е. данные сущности в кэше могут быть не равны данным в базе данных. Отсюда следует, что нужно избегать конкурентного доступа, точнее одни и те же записи не должны редактировать 2 пользователя. Приведу пример: идеальный случаи это когда кадровики редактируют только свои данные по работникам: 1-й кадровик с 1 по 200 таб. номер, 2-й кадровик с 201 по 400 таб. номер и т. д. Но все же если есть конкурентный доступ к сущности, то изменения сущности должны происходить мгновенно. Не имеет никаких блокировок, поэтому есть шанс возвратить устаревшие данные из кэша.
- read-write — в целом похож на nonstrict read-write, позволяет более гибко настроить конкурентный доступ, поведение кэша зависит от настройки transaction isolation уровня базы данных, т. е. поведение изменения данных в кэше копирует поведение транзакции. Использует блокировки, но асинхронно. Максимум, чего можно выжать — это «repeatable read transaction isolation» уровень базы данных. Применяется для не кластерного кэша, используется в основном для предотвращения считывания устаревших данных с кэша.
- transactional — изменения в кэше и изменения в базе данных, полностью записываются в одной транзакции. У предыдущих двух стратегий была отложенная запись кэша, т. е. при изменении сущности, с начала, происходит блокировка в кэше (soft locked), после применения транзакции с некоторым опозданием выполняется замена старого значения в кэше, новым. Уровень этого кэша — это «serializable transaction isolation» уровень базы данных. Применяется только для распределенных кэшей, в управляемых транзакциях JTA. Полностью исключается чтение устаревших данных из кэша.
Обратите внимание что, чем менее строгую стратегию для кэша вы выбираете, тем больше затраты ресурсов у кэша второго уровня. Hibernate имеет стратегию кэша по умолчанию, для этого нужно использовать свойство hibernate.cache.default_cache_concurrency_strategy в файле настроек hibernate.cfg.xml, для примера сделаем стратегию кэша по умолчанию read-write:
<property name="hibernate.cache.default_cache_concurrency_strategy">read-write</property>
Как настроить Ehcache?
Приведу пример конфигурационного файла ehcache.xml:
<?xml version="1.0" encoding="utf-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="ehcache.xsd" updateCheck="true" monitoring="autodetect" dynamicConfig="true"> <diskStore path="java.io.tmpdir"/> <defaultCache maxElementsInMemory="1" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" /> <cache name="Article" maxElementsInMemory="500" eternal="true" timeToIdleSeconds="0" timeToLiveSeconds="100" overflowToDisk="false" /> </ehcache>
В этом файле, Вы можете индивидуально изменить конфигурации кэша для отдельных сущностей. Например, мы изменили конфигурацию кэша для сущности Article явно задав настройки.
Название атрибута | Описание |
diskStore | Используется для определения пути, по которому создастся файловое хранилище для кэша на диске |
name | Имя региона кэша. Обязателен для элемента cache. |
maxElementsInMemory | Максимальное количество объектов сущностей, которые должны храниться всегда в памяти кэша. |
eternal | Могут ли храниться объекты кэша в памяти ”бесконечно” долго. – true да, false нет. |
timeToIdleSeconds | Промежуток времени обращения к объекту кэша, измеряется в секундах, если в течение этого времени не было обращения к объекту кэша, то сущность выгружается из кэша. Если поставить значение в 0, то объект кэша будет находиться в памяти “бесконечно” долго. |
timeToLiveSeconds | Промежуток времени в секундах, в течение которого объект кэша может находиться в кэше, после истечения этого промежутка, объект выгружается из памяти кэша. Если поставить значение в 0, то объект кэша будет находиться в памяти “бесконечно” долго. |
overflowToDisk | Если превысило количество объектов кэша в памяти больше значения maxElementsInMemory, то выгружать ли данные объектов кэша, на диск в “файлы подкачки”. true да, false нет. |
diskExpiryThreadIntervalSeconds | Сколько секунд по времени после помещения объекта кэша в “файл подкачки”, он может там находиться. |
memoryStoreEvictionPolicy | Параметр отвечает за процесс вытеснения объектов кэша, из кэша. Очередь, первым пришел, первым ушел — FIFO. По максимальному времени не использования объекта кэша в кэше – LRU. По количеству использования объектов кэша, т. е. как часто используется объект в кэше, и наименьше используемый объект кэша, выгружается из него – LFU. |
maxElementsOnDisk | Максимальное количество объектов кэша, которое может содержаться в “файле подкачки”. |
diskPersistent | Допустим, вы перезапускаете свое приложение, нужно ли сохранять кэш на диске и после перезапуска восстанавливать кэш, true да, false нет. |
Очищение кэша второго уровня (Evict)
Для того что удалить полностью кэш второго уровня, мы можем использовать следующий код:
@Autowired private SessionFactory sessionFactory; public void evictAll() { SessionFactory sf = currentSession().getSessionFactory(); Cache cache = sf.getCache(); cache.evictQueryRegions(); cache.evictDefaultQueryRegion(); cache.evictCollectionRegions(); cache.evictEntityRegions(); } protected Session currentSession() { return sessionFactory.getCurrentSession(); }
Если мы хотим удалить одну сущность, все сущности или коллекцию из кэша, мы можем сделать это так:
SessionFactory sf = currentSession().getSessionFactory(); cache.evictEntity(Cat.class, catId); //evict a particular Cat cache.evictEntityRegion(Cat.class); //evict all Cats cache.evictCollection("Cat.kittens", catId); //evict a particular collection of kittens cache.evictCollectionRegion("Cat.kittens"); //evict all kitten collections
Заключение
Разные типы данных требуют разных политик кэширования: Различия в соотношении операции чтения и записи, различия в размерах таблиц в БД, и некоторые таблицы пошарены с другими внешними приложениями. Кэш второго уровня конфигурируется на уровне класса или коллекции. Это позволит Вам, например, включить кэширование для классов, для которых операции чтения преобладают и выключить, для данных, которые представляют финансовые показатели.
Политики кэширования определяются следующими шагами:
■ На каких классах включить кэширование
■ Определиться со стратегией кэширования
■ Политики в отношении времени жизни кэша (смотри настройки EhCache)
■ Физический формат кэша (память, файлы-индексы, кластеры и репликации)
Кэширование полезно если:
- Вы пишите в БД только через Hibernate (это необходимо для единого пути обнаружения и инвалидации данных)
- Вы часто читаете объекты
- Вы имеете одну ноду, и не имеете репликацию. Иначе, Вам необходимо будет реплицирвоать сам кэш (например, с помощью JGroups) что добавит сложности и затруднит масштабирование.
Когда работает кэш?
- Когда вы вызываете методы
session.get()
илиsession.load()
для объекта, который до этого размещен в кэше. Кэш это хранилище, в котором ID это ключ и свойства сущности являются значением. Так что получить сущность из кэша возможно только по его ID. - Когда Ваши ассоциации загружаются lazy (или eager с селектами вместо joins)
Не работает когда:
- Если Вы делаете выборку не по ID. Если Вы делаете запрос как:
from Authors where name = :name
, то тогда запрос не попадет в кэш. - Когда вы используете HQL (даже если задавать
where id = ?
). - Если в маппинге проставлено
fetch="join"
, это означает что надо выгружать ассоциацию с помощью джойнов везде, вместо отдельных запросов селекта. Кэш работает, только если используетсяfetch="select"
.
Пример проекта в котором Вы можете посмотреть работу кэша и поэксперементировать находится на GitHub.