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

В новом переводе от команды Spring АйО вы узнаете про 7 основных техник оптимизации кеширования в Spring Boot, которые могут помочь значительно улучшить производительность. От выбора идеальных кандидатов для кеширования до реализации асинхронного кеша и мониторинга метрик кеша.


Определение идеальных кандидатов для оптимальной производительности

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

  • Данные, к которым идут частые обращения: данные, к которым приложение обращается часто и по многу раз являются хорошими кандидатами для кеширования. 

  • Дорогостоящие загрузки данных или вычисления: данные, которые требуют значительного времени или вычислительных ресурсов для их получения или обработки. 

  • Статические или редко изменяемые данные: данные, которые меняются нечасто, что дает нам гарантию того, что закешированные данные останутся валидными на более долгий период времени.  

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

  • Предсказуемые шаблоны: данные, которые следуют предсказуемым шаблонам считывания, позволяя более эффективно управлять кешированием.  

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

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

Очистка кеша

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

Я рекомендую следующие подходы к управлению инвалидации кеша в Spring Boot приложении: 

1. Политики удаления

Существуют знаменитые политики удаления: 

  • Least Recently Used — LRU: в первую очередь удаляет элементы, использованные раньше остальных.

  • Least Frequently Used — LFU: в первую очередь удаляет элементы, использованные наименее часто.

  • First In, First Out — FIFO: в первую очередь удаляет элементы, добавленные в кеш раньше всех.

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

2. Очистка на основе времени

Определение интервала времени жизни (time‑to‑live — TTL), который удаляет данные из кеша после определенного периода времени, отличается для каждого кеш-провайдера. Например, в случае использования Redis для кеширования нашего Spring Boot приложения, мы можем задать время жизни, используя такой конфиг:  

spring.cache.redis.time-to-live=10m 

Если ваш кеш-провайдер не поддерживает параметр time-to-live, вы можете реализовать его, используя аннотацию @CacheEvict и шедулер, как показано ниже: 

@CacheEvict(value = "cache1", allEntries = true)
@Scheduled(fixedRateString = "${your.config.key.for.ttl.in.milli}")
public void emptyCache1() {
  // Flushing cache, we don't need to write any code here except for a descriptive log!
} 

3. Кастомизированные политики удаления  

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

  • @CacheEvict: удалить один или все блоки закешированных данных из кеша. 

  • @CachePut: обновить блоки данных новыми значениями. 

  • CacheManager: Мы можем реализовать кастомизированную политику удаления используя CacheManager, имеющийся в Spring, и интерфейсы Cache. Для этой цели могут использоваться такие методы как  evict(), put(), или clear().  Мы также можем получить доступ непосредственно к кеш-провайдеру, чтобы использовать больше доступной функциональности при помощи метода getNativeCache(). 

Наиболее важный момент касательно кастомизированных политик очистки — это найти правильное место и условия для удаления данных.

Кеширование в зависимости от условия

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

Аннотации @Cacheable и @CachePut содержат атрибуты condition и unless, которые позволяют нам задавать условия для кеширования данных: 

  • Condition: задает выражение на SpEL (Spring Expression Language), которое должно вычисляться как true, чтобы данные закешировались или обновились. 

  • Unless: задает выражение на SpEL, которое должно вычисляться как false, чтобы данные закешировались или обновились. 

Чтобы лучше понять сказанное, посмотрим на следующий код: 

@Cacheable(value = "employeeByName", condition = "#result.size() > 10", 
           unless = "#result.size() < 1000")
public List<Employee> employeesByName(String name) {
  // Method logic to retrieve data 
  return someEmployeeList; 
} 

В этом коде список сотрудников будет закеширован только если размер результирующего списка будет больше 10 и меньше 1000.

Последним важным моментом здесь является то, что, как и в предыдущем разделе, мы можем реализовать зависящее от условия кеширование программным путем, используя CacheManager и Cache interfaces. Этот способ дает больше гибкости и контроля над поведением кеширования.

Распределенный кеш и локальный кеш

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

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

Что такое локальный кеш? 

Локальный кеш — это механизм кеширования, в котором данные сохраняются в оперативной памяти на той же машине или инстансе, где работает приложение. Некоторые хорошо известные библиотеки для локального кеширования — это Ehcache, Caffeine и Guava Cache

Локальное кеширование дает преимущество невероятно быстрого доступа к закешированным данным, поскольку позволяет избегать влияния задержек/долгого времени ожидания сети и непроизводительных издержек, связанных с получением данных удаленно (как при использовании распределенного кеша). Локальный кеш обычно проще в настройке и управлении, чем распределенный кеш и не требует дополнительной инфраструктуры

Когда надо использовать локальный, а когда распределенный кеш?

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

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

Реализация локального кеширования в Spring Boot 

Spring Boot поддерживает локальное кеширование через различные провайдеры in‑memory кеша, такие как Ehcache, Caffeine или ConcurrentHashMap. Единственное, что необходимо добавить — это требуемые зависимости, кроме того, нам надо включить кеширование в нашем Spring Boot приложении. Например, чтобы реализовать локальное кеширование с использованием Caffeine, нам надо добавить вот эти зависимости: 

<dependency> 
  <groupId>org.springframework.boot</groupId> 
  <artifactId>spring-boot-starter-cache</artifactId> 
</dependency>

<dependency> 
  <groupId>com.github.ben-manes.caffeine</groupId> 
  <artifactId>caffeine</artifactId>
</dependency> 

Затем надо включить кеширование, используя аннотацию @EnableCaching:

@SpringBootApplication 
@EnableCaching 
public class Application { 
  public static void main(String[] args) { 
    SpringApplication.run(Application.class, args); 
  } 
} 

В добавление к обычным Spring Cache конфигам, мы также можем сконфигурировать кеш от Caffeine: 

spring: 
   cache:
      caffeine:
         spec: maximumSize=500,expireAfterAccess=10m 

Кастомизированные стратегии генерации ключа

Алгоритм генерации ключа по умолчанию в Spring cache обычно работает так: 

  • Если не дано никаких параметров, верни 0. 

  • Если дан только один параметр, верни этот инстанс.  

  • Если дано больше параметров, верни ключ, вычисленный из хэшей всех параметров.

Этот подход хорошо работает для объектов с натуральными ключами, если в  hashCode() это отражено.  

Но для некоторых сценариев стратегия генерации ключа по умолчанию не работает должным образом: 

  • Нам нужны значимые ключи.  

  • В методах присутствуют многочисленные параметры одного и того же типа.

  • В методах присутствуют опциональные или Null параметры.

  • Нам необходимо включить контекстно-зависимые данные, такие как  locale, tenet ID, или user role в ключ, чтобы сделать его уникальным.

Spring Cache предлагает два подхода для задания кастомизированной стратегии генерации ключа: 

  • Задайте SpEL (Spring Expression Language) выражение атрибуту key, которое должно быть вычислено, чтобы получить новый ключ:

@CachePut(value = "phonebook", key = "#phoneNumber.name") 
PhoneNumber create(PhoneNumber phoneNumber) { 
  return phonebookRepository.insert(phoneNumber);
} 
  •  Определите бин, который реализует интерфейс KeyGenerator и затем присвойте его атрибуту keyGenerator: 

@Component("customKeyGenerator") 
public class CustomKeyGenerator implements KeyGenerator { 
  @Override 
  public Object generate(Object target, Method method, Object... params) { 
    return "UNIQUE_KEY"; 
  } 
} 

/////// 
@CachePut(value = "phonebook", keyGenerator = "customKeyGenerator") 
PhoneNumber create(PhoneNumber phoneNumber) { 
  return phonebookRepository.insert(phoneNumber);
} 

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

Асинхронный кеш

Как вы могли заметить, API абстракции Spring cache является блокирующей и синхронной, и если вы используете стэк WebFlux со Spring Cache, то использование аннотаций Spring Cache, таких как @Cacheable или @CachePut, приведет к кэшированию объектов-оберток реактора (Mono или Flux). В таком случае у вас есть три подхода:  

  • Вызвать метод cache() на типе реактора и аннотировать этот метод Spring Cache аннотациями. 

  • Использовать асинхронный API, поставленный кеш-провайдером (если это поддерживается) и управлять кешем программным путем.

  • Реализовать асинхронную обертку вокруг API кеширования и сделать ее асинхронной (если ваш кеш-провайдер этого не поддерживает).   

Однако, после релиза Spring Framework 6.2, если кеш-провайдер поддерживает асинхронное кеширование для проектов на WebFlux (например, Caffeine Cache), то декларативная инфраструктура кеширования Spring выявляет реактивные сигнатуры методов, например, возвращающие значение типа Reactor Mono или Flux, и обрабатывает такие методы особым образом для асинхронного кеширования производимых ими значений, вместо того чтобы пытаться закешировать сами инстансы Reactive Streams Publisher. Это требует настройки внутри целевого кеш-провайдера, например,  CaffeineCacheManager должен быть установлен в setAsyncCacheMode(true). Конфиг будет предельно простым: 

@Configuration 
@EnableCaching 
public class CacheConfig { 
  @Bean 
  public CacheManager cacheManager() { 
    final CaffeineCacheManager cacheManager = new CaffeineCacheManager(); 
    cacheManager.setCaffeine(buildCaffeineCache()); 
    cacheManager.setAsyncCacheMode(true); // <-- 
    return cacheManager; 
  } 
} 

Мониторинг кеша для нахождения узких мест

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

Наиболее важные метрики для мониторинга следующие:  

  • Cache Hit Rate: соотношение попаданий в кеш к общему числу обращений к кешу показывает эффективное кеширование, при этом малое количество попаданий означает, что кеш используется неэффективно.    

  • Cache Miss Rate: Соотношение непопаданий в кеш к общему числу запросов к кешу означает, что кеш зачастую не может обеспечить выдачу запрошенных данных, возможно по причине малого размера кеша или плохого управления ключами.  

  • Cache Eviction Rate: частота удаления блоков информации из кеша. Если это значение является высоким, это означает, что размер кеша слишком мал или политика удаления не очень хорошо подходит к существующему шаблону доступа.  

  • Memory Usage: Количество памяти, используемое кешем.  

  • Latency: время, требуемое для извлечения данных из кеша.  

  • Error Rates: метрики, относящиеся к нагрузке на кеш-сервера, такие как количество запросов в секунду.

Как мониторить метрики кеша в Spring Boot 

Spring Boot Actuator автоматически конфигурирует  Micrometer для всех доступных инстансов кеша при старте. Нам необходимо зарегистрировать кеши, созданные “на лету” или программным путем после фазы старта. Посмотреть список поддерживаемых провайдеров можно в документации

Прежде всего, нам надо добавить зависимости актуатора и микрометра: 

<dependency> 
  <groupId>org.springframework.boot</groupId> 
  <artifactId>spring-boot-starter-actuator</artifactId> 
</dependency> 

<dependency> 
  <groupId>io.micrometer</groupId> 
  <artifactId>micrometer-registry-prometheus</artifactId>
</dependency> 

Затем сделать доступными эндпоинты актуатора: 

management.endpoints.web.exposure.include=* 

Теперь мы можем видеть список сконфигурированных кешей, используя эндпоинт /actuator/caches, а для метрик кеша можно использовать следующие: 

/actuator/metrics/cache.gets 
/actuator/metrics/cache.puts 
/actuator/metrics/cache.evictions 
/actuator/metrics/cache.removals 

Вывод: оптимизируйте кеширование в Spring Boot 

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

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Ждем всех, присоединяйтесь

Комментарии (2)


  1. TerraV
    16.07.2024 13:15

    Работает медленно? Добавь кэш.

    Работает неправильно? Убери кэш.

    В 9 случаях из 10 использование кэша неоправдано и маскирует неправильный код или кривую архитектуру.

    Правильная реализация кэша это искусство.

    Даже если вы реализовали кэш идеально, не факт что не найдется индуса/джуна, который его не "улучшит" пока вы спите / в отпуске.


    1. excentro
      16.07.2024 13:15
      +1

      Истину глаголишь :)