что такое кэш java

Кеширование данных — Java Spring

Многократно вычитывая одни и те же данные, встает вопрос оптимизации, данные не меняются или редко меняются, это различные справочники и др. информация, т.е. функция получения данных по ключу — детерминирована. Тут наверно все понимают — нужен Кеш! Зачем всякий раз повторно выполнять поиск данных или вычисление?

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

Кеш в Spring

Далее все поступают примерно одинаково, в Java используют различные HasMap, ConcurrentMap и др. В Spring тоже для это есть решение, простое, удобное, эффективное. Я думаю что в большинстве случаев это поможет в решении задачи. И так, все что нужно, это включить кеш и аннотировать функцию.

Делаем кеш доступным

Кешируем данные поиска функции

В аннотации указывается название кеша и есть еще другие параметры. Работает как и ожидается так, первый раз код выполняется, результат поиска помещается в кеш по ключу (в данном случае name) и последующие вызовы код уже не выполняется, а данные извлекаются из кеша.

Пример реализации репозитория «Person» с использованием кеша

Проверяю что получилось

В тесте вызываю два раза

, первый раз происходит вызов, поиск, в второй раз результат берется уже из кеша. Это видно в консоли

что такое кэш java. Смотреть фото что такое кэш java. Смотреть картинку что такое кэш java. Картинка про что такое кэш java. Фото что такое кэш java

Удобно, можно точечно оптимизировать существующий функционал. Если в функции более одного аргумента, то можно указать имя параметра, какой использовать в качестве ключа.

Есть и более сложные схемы получения ключа, это в документации.

Но конечно встанет вопрос, как обновить данные в кеше? Для этой цели есть две аннотации.

Первая это @CachePut

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

Добавлю в репозиторий два метода: удаления и добавления Person

Выполню поиск Person, удалю, добавлю, опять поиск, но по прежнему буду получать одно и тоже лицо из кеша, пока не вызову «findByNameAndPut»

что такое кэш java. Смотреть фото что такое кэш java. Смотреть картинку что такое кэш java. Картинка про что такое кэш java. Фото что такое кэш java

Другая аннотация это @CacheEvict

Позволяет не просто посещать хранилище кеша, но и выселять. Этот процесс полезен для удаления устаревших или неиспользуемых данных из кеша.

По умолчанию Spring для кеша использует — ConcurrentMapCache, если есть свой отличный класс для организации кеша, то это возможно указать в CacheManager

Там же указываются имена кешей, их может быть несколько. В xml конфигурации это указывается так:

Источник

Hibernate cache

Довольно часто в java приложениях с целью снижения нагрузки на БД используют кеш. Не много людей реально понимают как работает кеш под капотом, добавить просто аннотацию не всегда достаточно, нужно понимать как работает система. Поэтому этой статье я попытаюсь раскрыть тему про то, как работает кеш популярного ORM фреймворка. Итак, для начала немного теории.

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

Кеш первого уровня всегда привязан к объекту сессии. Hibernate всегда по умолчанию использует этот кеш и его нельзя отключить. Давайте сразу рассмотрим следующий код:

Возможно, Вы ожидаете, что будет выполнено 2 запроса в БД? Это не так. В этом примере будет выполнен 1 запрос в базу, несмотря на то, что делается 2 вызова load(), так как эти вызовы происходят в контексте одной сессии. Во время второй попытки загрузить план с тем же идентификатором будет использован кеш сессии.
Один важный момент — при использовании метода load() Hibernate не выгружает из БД данные до тех пор пока они не потребуются. Иными словами — в момент, когда осуществляется первый вызов load, мы получаем прокси объект или сами данные в случае, если данные уже были в кеше сессии. Поэтому в коде присутствует getName() чтобы 100% вытянуть данные из БД. Тут также открывается прекрасная возможность для потенциальной оптимизации. В случае прокси объекта мы можем связать два объекта не делая запрос в базу, в отличии от метода get(). При использовании методов save(), update(), saveOrUpdate(), load(), get(), list(), iterate(), scroll() всегда будет задействован кеш первого уровня. Собственно, тут нечего больше добавить.

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

Если кеш первого уровня привязан к объекту сессии, то кеш второго уровня привязан к объекту-фабрике сессий (Session Factory object). Что как бы подразумевает, что видимость этого кеша гораздо шире кеша первого уровня. Пример:

В данном примере будет выполнено 2 запроса в базу, это связано с тем, что по умолчанию кеш второго уровня отключен. Для включения необходимо добавить следующие строки в Вашем конфигурационном файле JPA (persistence.xml):

Только после всех этих манипуляций кеш второго уровня будет включен и в примере выше будет выполнен только 1 запрос в базу.
Еще одна важная деталь про кеш второго уровня про которую стоило бы упомянуть — хибернейт не хранит сами объекты Ваших классов. Он хранит информацию в виде массивов строк, чисел и т. д. И идентификатор объекта выступает указателем на эту информацию. Концептуально это нечто вроде Map, в которой id объекта — ключ, а массивы данных — значение. Приблизительно можно представить себе это так:

Что есть очень разумно, учитывая сколько лишней памяти занимает каждый объект.
Помимо вышесказанного, следует помнить — зависимости Вашего класса по умолчанию также не кешируются. Например, если рассмотреть класс выше — SharedDoc, то при выборке коллекция users будет доставаться из БД, а не из кеша второго уровня. Если Вы хотите также кешировать и зависимости, то класс должен выглядеть так:

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

Кеш запросов

Перепишем первый пример так:

Результаты такого рода запросов не сохраняются ни кешом первого, ни второго уровня. Это как раз то место, где можно использовать кеш запросов. Он тоже по умолчанию отключен. Для включения нужно добавить следующую строку в конфигурационный файл:

а также переписать пример выше добавив после создания объекта Query (то же справедливо и для Criteria):

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

Стратегии кеширования
Cache region

Регион или область — это логический разделитель памяти вашего кеша. Для каждого региона можна настроить свою политику кеширования (для EhCache в том же ehcache.xml). Если регион не указан, то используется регион по умолчанию, который имеет полное имя вашего класса для которого применяется кеширование. В коде выглядит так:

А для кеша запросов так:

Что еще нужно знать?

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

В дополнение фабрика сессий также может генерировать и сохранять статистику использования всех объектов, регионов, зависимостей в кеше:

Для этого есть объекты Statistics для фабрики и SessionStatistics для сессии.

Методы сессии:
flush() — синхронизирует объекты сессии с БД и в то же время обновляет сам кеш сессии.
evict() — нужен для удаления объекта из кеша cессии.
contains() — определяет находится ли объект в кеше сессии или нет.
clear() — очищает весь кеш.

Источник

Spring Cache: от подключения кэширования за 1 минуту до гибкой настройки кэш-менеджера

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

что такое кэш java. Смотреть фото что такое кэш java. Смотреть картинку что такое кэш java. Картинка про что такое кэш java. Фото что такое кэш java

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

Почему я говорю «поручили»? Потому что кэширование, как правило, есть смысл применять в больших, высоконагруженных проектах, с десятками тысяч запросов в минуту. В таких проектах, чтобы не перегружать базу, как правило, кэшируют обращения к репозиторию. Особенно если известно, что данные из какой-нибудь мастер-системы обновляются с некоторой периодичностью. Сами мы такие проекты не пишем, мы на них работаем. Если же проект маленький и перегрузки ему не грозят, тогда, конечно, лучше ничего не кэшировать — всегда свежие данные всегда лучше периодически обновляемых.

Обычно в обучающих постах докладчик сначала лезет под капот, начинает копаться в кишочках технологии, чем немало утомляет читателя, а уж потом, когда тот пролистал без дела добрую половину статьи и ничего не понял, повествует, как это работает. У нас всё будет иначе. Сначала мы делаем так, чтобы заработало, и желательно, с приложением наименьших усилий, а уж потом, если интересно, Вы сможете заглянуть под капот кэширования, посмотреть изнутри сам бин и тонко настроить кэширование. Но даже если Вы этого не сделаете (а это начинается с 6 пункта), Ваше кэширование будет работать и так.

Мы создадим проект, в котором разберём все те аспекты кэширования, которые я обещал. В конце, как обычно, будет ссылка на сам проект.

0. Создание проекта

Мы создадим очень простой проект, в котором мы сможем брать сущность из базы данных. Я добавил в проект Lombok, Spring Cache, Spring Data JPA и H2. Хотя, вполне можно обойтись только Spring Cache.

У нас будет только одна сущность, назовём её User.

Добавим репозиторий и сервис:

Когда мы заходим в сервисный метод get(), мы пишем об этом в лог.

Подключим к проекту Spring Cache.

1. Кэширование возвращаемого результата

Что делает Spring Cache? Spring Cache просто кэширует возвращаемый результат для определённых входных параметров. Давайте это проверим. Мы поставим аннотацию @Cacheable над сервисным методом get(), чтобы кэшировать возвращаемые данные. Дадим этой аннотации название «users» (далее мы разберём, зачем это делается, отдельно).

Для того, чтобы проверить, как это работает, напишем простой тест.

Если над классом стоит своя аннотация @SpringBootTest, для такого класса каждый раз заново поднимается контекст. Поскольку контекст может подниматься 5 секунд, а может 40 секунд, это в любом случае очень сильно тормозит процесс тестирования. При этом, разницы в контексте обычно нет никакой, и при запуске каждой группы тестов в пределах одного класса нет необходимости заново запускать контекст. Если же мы ставим только одну аннотацию, скажем, над абстрактным классом, как в нашем случае, это позволяет поднимать контекст только один раз.

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

Что делает наш тест? Он создаёт двоих юзеров и потом по 2 раза вытаскивает их из базы. Как мы помним, мы поместили аннотацию @Cacheable, которая будет кэшировать возвращаемые значения. После получения объекта из метода get() мы выводим объект в лог. Также, мы выводим в лог информацию о каждом посещении приложением метода get().

Запустим тест. Вот что мы получаем в консоль.

Как мы видим, первые два раза мы действительно сходили в метод get() и реально получили юзера из базы. Во всех остальных случаях, реального захода в метод не было, приложение брало закэшированные данные по ключу (в данном случае, это id).

2. Объявление ключа для кэширования

Бывают ситуации, когда в кэшируемый метод приходит несколько параметров. В таком случае, бывает нужно определить параметр, по которому будет происходить кэширование. Добавим в пример метод, который будет сохранять в базу сущность, собранную по параметрам, но если сущность с таким именем уже есть, мы не будем её сохранять. Для этого мы определим параметр name как ключ для кэширования. Выглядеть это будет так:

Напишем соответствующий тест:

Мы попытаемся создать троих пользователей, для двоих из которых будет совпадать имя

и для двоих из которых будет совпадать email

В методе создания мы логируем каждый факт обращения к методу, а также, мы будем логировать все сущности, которые этот метод нам вернул. Результат будет таким:

Мы видим, что фактически приложение вызывало метод 3 раза, а заходило в него только два раза. Один раз для метода совпадал ключ, и он просто возвращал закэшированное значение.

3. Принудительное кэширование. @CachePut

Бывают ситуации, когда мы хотим кэшировать возвращаемое значение для какой-то сущности, но в то же время, нам нужно обновить кэш. Для таких нужд существует аннотация @CachePut. Оно пропускает приложение в метод, при этом, обновляя кэш для возвращаемого значения, даже если оно уже закэшировано.

Добавим пару методов, в которых мы будем сохранять юзера. Один из них мы пометим обычной аннотацией @Cacheable, второй — @CachePut.

Первый метод будет просто возвращать закэшированные значения, второй — принудительно обновлять кэш. Кэширование будет осуществляться по ключу #user.name. Напишем соответствующий тест.

По той логике, которая уже описывалась, при первом сохранении пользователя с именем «Vasya» через метод createOrReturnCached() далее мы будем получать кэшированную сущность, при этом, в сам метод приложение заходить не будет. Если же мы вызовем метод createAndRefreshCache(), кэшированная сущность для ключа с именем «Vasya» перезапишется в кэше. Выполним тест и посмотрим, что будет выведено в консоль.

Мы видим, что user1 благополучно записался в базу и кэш. При повторной попытке записать юзера с таким же именем мы получаем закэшированный результат выполнения первого обращения (user2, для которого id такой же, как у user1, что говорит нам о том, что юзер не был записан, и это просто кэш). Далее, мы пишем третьего пользователя через второй метод, который даже при имеющемся закэшированном результате всё равно вызвал метод и записал в кэш новый результат. Это user3. Как мы видим, у него уже новый id. После чего, мы вызываем первый метод, который берёт новый кэш, добавленный user3.

4. Удаление из кэша. @CacheEvict

Иногда возникает необходимость жёстко обновить какие-то данные в кэше. Например, сущность уже удалена из базы, но она по-прежнему доступна из кэша. Для сохранения консистентности данных, нам необходимо хотя бы не хранить в кэше удалённые данные.

Добавим в сервис ещё пару методов.

Первый будет просто удалять пользователя, второй тоже будет его удалять, но мы пометим его аннотацией @CacheEvict. Добавим тест, который будет создавать двух юзеров, после чего, одного будет удалять через простой метод, а второго — через аннотируемый метод. После чего, мы достанем этих юзеров через метод get().

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

Мы видим, что приложение благополучно сходило оба раза в метод get() и Spring закэшировал эти сущности. Далее, мы удалили их через разные методы. Первый мы удалили обычным путём, и закэшированное значение осталось, поэтому когда мы попытались получить юзера под id 1, нам это удалось. Когда же мы попытались получить юзера 2, метод вернул нам EntityNotFoundException — такого юзера в кэше не оказалось.

5. Группировка настроек. @Caching

Иногда один метод требует нескольких настроек кэширования. Для этих целей используется аннотация @Caching. Выглядеть это может приблизительно так:

Это единственный способ группировать аннотации. Если Вы попытаетесь нагородить что-то вроде

то IDEA сообщит Вам, что так нельзя.

6. Гибкая настройка. CacheManager

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

Для таких задач существует CacheManager. Он существует везде, где есть Spring Cache. Когда мы добавили аннотацию @EnableCache, такой кэш менеджер автоматически будет создан Spring. Мы можем убедиться в этом, если заавтовайрим ApplicationContext и вскроем его на брейкпоинте. Среди прочих бинов, будет и бин «cacheManager».

что такое кэш java. Смотреть фото что такое кэш java. Смотреть картинку что такое кэш java. Картинка про что такое кэш java. Фото что такое кэш java

Я остановил приложение на этапе, когда уже два юзера были созданы и помещены в кэш. Если мы вызовем нужный нам бин через Evaluate Expression, то мы увидим, что такой бин действительно есть, в нём есть ConcurentMapCache с ключом «users» и значением ConcurrentHashMap, в которой уже лежат закэшированные юзеры.

что такое кэш java. Смотреть фото что такое кэш java. Смотреть картинку что такое кэш java. Картинка про что такое кэш java. Фото что такое кэш java

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

Осталось только выбрать, какой именно кэш-менеджер мы будем использовать, потому что их предостаточно. Я не буду перечислять все кэш-менеджеры, достаточно будет знать, что есть такие:

Итак, досоздадим наш кэш-менеджер.

Наш кэш-менеджер готов.

7. Настройка кэша. Время жизни, максимальный размер и проч.

Для этого нам потребуется довольно популярная библиотека Google Guava. Я взял последнюю.

При создании кэш-менеджера переопределим метод createConcurrentMapCache, в котором вызовем CacheBuilder от Guava. В процессе нам будет предложено настроить кэш-менеджер при помощи инициализации следующих методов:

Определим в менеджере время жизни записи. Чтобы долго не ждать, выставим 1 секунду.

Напишем соответствующий такому случаю тест.

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

Логи показывают, что сначала мы создали юзера, потом попытались ещё одного, но поскольку данные были закэшированы, мы получили их из кэша (в обоих случаях — при сохранении и при получении из базы). Потом протух кэш, о чём сообщает нам запись о фактическом сохранении и фактическом получении юзера.

8. Подведу итог

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

Источник

Java-модель памяти (часть 2)

Привет, Хабр! Представляю вашему вниманию перевод второй части статьи «Java Memory Model» автора Jakob Jenkov. Первая часть тут.

Аппаратная архитектура памяти

Современная аппаратная архитектура памяти несколько отличается от внутренней Java-модели памяти. Важно понимать аппаратную архитектуру, чтобы понять, как с ней работает Java-модель. В этом разделе описывается общая аппаратная архитектура памяти, а в следующем разделе описывается, как с ней работает Java.

Вот упрощенная схема аппаратной архитектуры современного компьютера:
что такое кэш java. Смотреть фото что такое кэш java. Смотреть картинку что такое кэш java. Картинка про что такое кэш java. Фото что такое кэш java
Современный компьютер часто имеет 2 или более процессоров. Некоторые из этих процессоров также могут иметь несколько ядер. На таких компьютерах возможно одновременное выполнение нескольких потоков. Каждый процессор (прим. переводчика — тут и далее под процессором автор вероятно подразумевает ядро процессора или одноядерный процессор) способен запускать один поток в любой момент времени. Это означает, что если ваше Java-приложение является многопоточным, то внутри вашей программы может быть запущен одновременно один поток на один процессор.

Каждый процессор содержит набор регистров, которые, по существу, находятся в его памяти. Он может выполнять операции над данными регистрах намного быстрее, чем в над данными, которые находятся в основной памяти компьютера (ОЗУ). Это связано с тем, что процессор может получить доступ к этим регистрам гораздо быстрее.

Каждый ЦП также может иметь слой кэш-памяти. Фактически, большинство современных процессоров его имеют. Процессор может получить доступ к своей кэш-памяти намного быстрее, чем к основной памяти, но, как правило, не так быстро, как к своим внутренним регистрам. Таким образом, скорость доступа к кэш-памяти находится где-то между скоростями доступа к внутренним регистрам и к основной памяти. Некоторые процессоры могут иметь многоуровневый кэш, но это не так важно знать, чтобы понять, как Java-модель памяти взаимодействует с аппаратной памятью. Важно знать, что процессоры могут иметь некоторый уровень кэш-памяти.

Компьютер также содержит область основной памяти (ОЗУ). Все процессоры могут получить доступ к основной памяти. Основная область памяти обычно намного больше, чем кэш-память процессоров.

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

Данные, хранящиеся в кэш-памяти, обычно сбрасываются обратно в основную память, когда процессору необходимо сохранить в кэш-памяти что-то еще. Кэш может очищать свою память и записывать в неё новые данные одновременно. Процессор не должен читать/записывать полный кэш каждый раз, когда он обновляется. Обычно кэш обновляется небольшими блоками памяти, называемыми «строками кэша». Одна или несколько строк кэша могут быть считаны в кэш-память, и одна или более строк кэша могут быть сброшены назад в основную память.

Совмещение Java-модели памяти и аппаратной архитектуры памяти

Как уже упоминалось, Java-модель памяти и аппаратная архитектура памяти различны. Аппаратная архитектура не различает стеки потоков и кучу. На оборудовании стек потоков и куча (heap) находятся в основной памяти. Части стеков и кучи потоков могут иногда присутствовать в кэшах и внутренних регистрах ЦП. Это показано на диаграмме:
что такое кэш java. Смотреть фото что такое кэш java. Смотреть картинку что такое кэш java. Картинка про что такое кэш java. Фото что такое кэш java
Когда объекты и переменные могут храниться в различных областях памяти компьютера, могут возникнуть определенные проблемы. Вот две основные:
• Видимость изменений, которые произвёл поток над общими переменными.
• Состояние гонки при чтении, проверке и записи общих переменных.
Обе эти проблемы будут объяснены в следующих разделах.

Видимость общих объектов

Если два или более потока делят между собой объект без надлежащего использования volatile-объявления или синхронизации, то изменения общего объекта, сделанные одним потоком, могут быть невидимы для других потоков.

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

Следующая диаграмма иллюстрирует набросок этой ситуации. Один поток, работающий на левом ЦП, копирует в его кэш общий объект и изменяет значение переменной count на 2. Это изменение невидимо для других потоков, работающих на правом ЦП, поскольку обновление для count ещё не было сброшено обратно в основную память.
что такое кэш java. Смотреть фото что такое кэш java. Смотреть картинку что такое кэш java. Картинка про что такое кэш java. Фото что такое кэш java
Для того, чтобы решить эту проблему, вы можете использовать ключевое слово volatile при объявлении переменной. Оно может гарантировать, что данная переменная считывается непосредственно из основной памяти и всегда записывается обратно в основную память, когда обновляется.

Состояние гонки

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

Эта диаграмма иллюстрирует возникновение проблемы с состоянием гонки, которое описано выше:
что такое кэш java. Смотреть фото что такое кэш java. Смотреть картинку что такое кэш java. Картинка про что такое кэш java. Фото что такое кэш java
Для решения этой проблемы вы можете использовать синхронизированный блок Java. Синхронизированный блок гарантирует, что только один поток может войти в данный критический раздел кода в любой момент времени. Синхронизированные блоки также гарантируют, что все переменные, к которым обращаются внутри синхронизированного блока, будут считаны из основной памяти, и когда поток выйдет из синхронизированного блока, все обновленные переменные будут снова сброшены в основную память, независимо от того, объявлена ли переменная как volatile или нет.

Источник

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

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